From 02e051580c18a5a006201c3ab117de9a27cfa418 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 08:56:45 -0800 Subject: [PATCH 001/762] Separate publishing of test results for PRs from forks --- .github/actions/test/action.yml | 7 +++---- .github/workflows/publish-test-results.yml | 24 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-test-results.yml diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 8cab83ae9..d36b8b97f 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -79,10 +79,9 @@ runs: shell: bash run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger trx --results-directory "${{ env.RESULTS_NAME }}" - - name: Publish Test Results + - name: Upload Test Results if: ${{ !cancelled() }} - uses: phoenix-actions/test-reporting@v12 + uses: actions/upload-artifact@v4 with: - name: ${{ env.RESULTS_NAME }} + name: results-${{ env.RESULTS_NAME }} path: ${{ env.RESULTS_NAME }}/*.trx - reporter: dotnet-trx diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml new file mode 100644 index 000000000..843a13517 --- /dev/null +++ b/.github/workflows/publish-test-results.yml @@ -0,0 +1,24 @@ +name: Publish Test Results + +on: + workflow_run: + workflows: ['Build'] + types: + - completed + +permissions: + contents: read + actions: read + checks: write + +jobs: + report: + runs-on: ubuntu-latest + steps: + - name: Publish Test Results + uses: phoenix-actions/test-reporting@v12 + with: + artifact: /results-(.*)/ + name: '$1' + path: '*.trx' + reporter: dotnet-trx From ad60352bae5566c7b0e0f082277a1639b0ceae91 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 15:07:36 -0800 Subject: [PATCH 002/762] Ignore CA1825 --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index d91f68d99..6d6c3a13f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -203,6 +203,7 @@ dotnet_diagnostic.CA1819.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1823.severity = suggestion dotnet_diagnostic.CA1824.severity = suggestion +dotnet_diagnostic.CA1825.severity = suggestion dotnet_diagnostic.CA2000.severity = suggestion dotnet_diagnostic.CA2002.severity = suggestion dotnet_diagnostic.CA2007.severity = suggestion From 541d3307e1466b0353dc4149f502a4b62b4de616 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 15:07:56 -0800 Subject: [PATCH 003/762] Don't use TestCase for single test --- .../VersionAdapters/MacOsVersionAdapterFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs index 8bc8c9668..c191fa153 100644 --- a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters versionName.FullName.Should().Be("macOS " + versionString); } - [TestCase] + [Test] public void should_detect_server() { var fileContent = File.ReadAllText(GetTestPath("Files/macOS/SystemVersion.plist")); @@ -63,7 +63,7 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters versionName.Name.Should().Be("macOS Server"); } - [TestCase] + [Test] public void should_return_null_if_folder_doesnt_exist() { Mocker.GetMock() From 3259e6dc1019ed957d23ecf57c8d84fdc16a479a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 15:14:06 -0800 Subject: [PATCH 004/762] Download artifacts for Publish Test Results workflow --- .github/workflows/publish-test-results.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index 843a13517..d10a92364 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -13,12 +13,19 @@ permissions: jobs: report: + if: github.event.workflow_run.conclusion != 'skipped' runs-on: ubuntu-latest steps: + - name: Download Test Reports + uses: actions/download-artifact@v4 + with: + name: results-* + path: test-results + merge-multiple: true + - name: Publish Test Results uses: phoenix-actions/test-reporting@v12 with: - artifact: /results-(.*)/ - name: '$1' - path: '*.trx' + name: 'Test Results' + path: 'test-results/*.trx' reporter: dotnet-trx From ad0249c7db29efc9d97d182cfe001bb5e1efe43a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 15:14:13 -0800 Subject: [PATCH 005/762] Use publish-unit-test-result-action for test result reporting --- .github/actions/test/action.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index d36b8b97f..447f60975 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -79,9 +79,13 @@ runs: shell: bash run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger trx --results-directory "${{ env.RESULTS_NAME }}" - - name: Upload Test Results + - name: Publish Test Results if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 + uses: EnricoMi/publish-unit-test-result-action/composite@v2 with: - name: results-${{ env.RESULTS_NAME }} - path: ${{ env.RESULTS_NAME }}/*.trx + check_run: false + check_run_annotations: none + comment_mode: off + comment_title: ${{ env.RESULTS_NAME }} Test Results + large_files: true + files: ${{ env.RESULTS_NAME }}/*.trx From 118279892973988569c9c96a3fbdf0ac7cce7623 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 16:34:37 -0800 Subject: [PATCH 006/762] Actually run Windows integration tests --- .github/actions/test/action.yml | 7 +++++++ .github/workflows/build.yml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 447f60975..4dc7afe30 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -79,6 +79,13 @@ runs: shell: bash run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger trx --results-directory "${{ env.RESULTS_NAME }}" + - name: Upload Test Results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: results-${{ env.RESULTS_NAME }} + path: ${{ env.RESULTS_NAME }}/*.trx + - name: Publish Test Results if: ${{ !cancelled() }} uses: EnricoMi/publish-unit-test-result-action/composite@v2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3174a2e3..f901eddc9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -188,7 +188,7 @@ jobs: binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr - os: windows-latest artifact: tests-win-x64 - filter: TestCategory!=ManualTest&TestCategory=WINDOWS&TestCategory=IntegrationTest + filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest binary_artifact: build_windows binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr runs-on: ${{ matrix.os }} From d3226197332fed9e9e47919c71737e167ba80625 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 17:14:57 -0800 Subject: [PATCH 007/762] Temporarily disable deploy --- .github/workflows/build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f901eddc9..0f2bfd793 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -207,13 +207,13 @@ jobs: binary_artifact: ${{ matrix.binary_artifact }} binary_path: ${{ matrix.binary_path }} - deploy: - if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} - needs: [backend, unit_test, unit_test_postgres, integration_test] - secrets: inherit - uses: ./.github/workflows/deploy.yml - with: - framework: ${{ needs.backend.outputs.framework }} - branch: ${{ github.ref_name }} - major_version: ${{ needs.backend.outputs.major_version }} - version: ${{ needs.backend.outputs.version }} + # deploy: + # if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} + # needs: [backend, unit_test, unit_test_postgres, integration_test] + # secrets: inherit + # uses: ./.github/workflows/deploy.yml + # with: + # framework: ${{ needs.backend.outputs.framework }} + # branch: ${{ github.ref_name }} + # major_version: ${{ needs.backend.outputs.major_version }} + # version: ${{ needs.backend.outputs.version }} From 91f33c670e62d33c59191b7614c6e0dcde3e55e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 19:20:00 -0800 Subject: [PATCH 008/762] Fix post-build test reporting and report summary --- .github/actions/test/action.yml | 18 ++++++++---------- .github/workflows/publish-test-results.yml | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 4dc7afe30..65e928bb0 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -77,22 +77,20 @@ runs: - name: Run tests shell: bash - run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger trx --results-directory "${{ env.RESULTS_NAME }}" + run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" - name: Upload Test Results if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: results-${{ env.RESULTS_NAME }} - path: ${{ env.RESULTS_NAME }}/*.trx + path: TestResults/*.trx - name: Publish Test Results - if: ${{ !cancelled() }} - uses: EnricoMi/publish-unit-test-result-action/composite@v2 + uses: phoenix-actions/test-reporting@v12 with: - check_run: false - check_run_annotations: none - comment_mode: off - comment_title: ${{ env.RESULTS_NAME }} Test Results - large_files: true - files: ${{ env.RESULTS_NAME }}/*.trx + name: Test Results + output-to: step-summary + path: '*.trx' + reporter: dotnet-trx + working-directory: TestResults diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index d10a92364..5e6012559 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -13,19 +13,29 @@ permissions: jobs: report: - if: github.event.workflow_run.conclusion != 'skipped' + if: ${{ github.event.workflow_run.conclusion != 'skipped' && github.event.workflow_run.conclusion != 'cancelled' }} runs-on: ubuntu-latest steps: + - name: Check out + uses: actions/checkout@v3 + - name: Download Test Reports uses: actions/download-artifact@v4 with: - name: results-* path: test-results + pattern: results-* merge-multiple: true + repository: ${{ github.event.repository.owner.login }}/${{ github.event.repository.name }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Publish Test Results uses: phoenix-actions/test-reporting@v12 with: - name: 'Test Results' - path: 'test-results/*.trx' + list-suites: failed + list-tests: failed + name: Test Results + only-summary: true + path: '*.trx' reporter: dotnet-trx + working-directory: test-results From 53cf5308931069638c23925596a3fd8aaccc5d98 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 23:17:29 -0800 Subject: [PATCH 009/762] Fixed: Series posters flickering when width changes repeatedly Closes #6311 --- frontend/src/App/State/AppState.ts | 9 +++++++++ frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx | 9 +++++++-- ...DimensionsSelector.js => createDimensionsSelector.ts} | 3 ++- 3 files changed, 18 insertions(+), 3 deletions(-) rename frontend/src/Store/Selectors/{createDimensionsSelector.js => createDimensionsSelector.ts} (69%) diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index fcf1833ee..72aa0d7f0 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -44,7 +44,16 @@ export interface CustomFilter { filers: PropertyFilter[]; } +export interface AppSectionState { + dimensions: { + isSmallScreen: boolean; + width: number; + height: number; + }; +} + interface AppState { + app: AppSectionState; calendar: CalendarAppState; commands: CommandAppState; episodeFiles: EpisodeFilesAppState; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index 622445999..48e9674c0 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -202,13 +202,18 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { if (current) { const width = current.clientWidth; const padding = bodyPadding - 5; + const finalWidth = width - padding * 2; + + if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) { + return; + } setSize({ - width: width - padding * 2, + width: finalWidth, height: window.innerHeight, }); } - }, [isSmallScreen, scrollerRef, bounds]); + }, [isSmallScreen, size, scrollerRef, bounds]); useEffect(() => { const currentScrollerRef = scrollerRef.current as HTMLElement; diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.js b/frontend/src/Store/Selectors/createDimensionsSelector.ts similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.js rename to frontend/src/Store/Selectors/createDimensionsSelector.ts index ce26b2e2c..b9602cb02 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.js +++ b/frontend/src/Store/Selectors/createDimensionsSelector.ts @@ -1,8 +1,9 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state) => state.app.dimensions, + (state: AppState) => state.app.dimensions, (dimensions) => { return dimensions; } From fd17df0dd03a5feb088c3241a247eac20f0e8c6c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 12 Jan 2024 23:51:35 -0800 Subject: [PATCH 010/762] New: Optional directory setting for Aria2 Closes #6343 --- .../Http/XmlRpcRequestBuilder.cs | 4 ++++ .../Download/Clients/Aria2/Aria2Proxy.cs | 19 +++++++++++++++++-- .../Download/Clients/Aria2/Aria2Settings.cs | 3 +++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs index e03161702..e7ab0126d 100644 --- a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs @@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http { data = new XElement("base64", Convert.ToBase64String(bytes)); } + else if (value is Dictionary d) + { + data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value)))); + } else { throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}"); diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs index f141ef7ce..52f5a1bde 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Download.Extensions; @@ -97,8 +98,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddMagnet(Aria2Settings settings, string magnet) { - var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List { magnet }); + var options = new Dictionary(); + if (settings.Directory.IsNotNullOrWhiteSpace()) + { + options.Add("dir", settings.Directory); + } + + var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List { magnet }, options); var gid = response.GetStringResponse(); return gid; @@ -106,8 +113,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddTorrent(Aria2Settings settings, byte[] torrent) { - var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent); + // Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement. + var emptyListOfUris = new List(); + var options = new Dictionary(); + if (settings.Directory.IsNotNullOrWhiteSpace()) + { + options.Add("dir", settings.Directory); + } + + var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options); var gid = response.GetStringResponse(); return gid; diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs index 088f06f9e..f90ea6306 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs @@ -41,6 +41,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [FieldDefinition(4, Label = "SecretToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string SecretToken { get; set; } + [FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")] + public string Directory { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index e8d21e9d3..b4f04f39e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -368,6 +368,7 @@ "DotNetVersion": ".NET", "Download": "Download", "DownloadClient": "Download Client", + "DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}", "DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}", From d76a489be6ad56c2150e8cffedf864a1ce319f55 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 12 Jan 2024 00:33:08 +0000 Subject: [PATCH 011/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Fixer Co-authored-by: Oskari Lavinto Co-authored-by: Watashi Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 6 ++++-- src/NzbDrone.Core/Localization/Core/fr.json | 3 ++- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index fd71aec91..1629e4c2c 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1017,7 +1017,7 @@ "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", "Seasons": "Tuotantokaudet", "SearchAll": "Etsi kaikkia", - "SearchByTvdbId": "Voit setsiä myös sarjan TVDB ID:llä, esim. \"tvdb:71663\".", + "SearchByTvdbId": "Voit etsiä myös sarjan TVDB ID:llä, esim. \"tvdb:71663\".", "RootFoldersLoadError": "Virhe ladattaessa juurikansioita", "SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.", "Year": "Vuosi", @@ -1275,5 +1275,7 @@ "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin." + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", + "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", + "Table": "Taulukko" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c95ced8a4..706c980f8 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1867,5 +1867,6 @@ "NotificationsEmailSettingsRecipientAddressHelpText": "Liste séparée par des virgules des destinataires des courriels", "NotificationsSynologySettingsUpdateLibraryHelpText": "Appelle synoindex sur l'hôte local pour mettre à jour le fichier de bibliothèque", "NotificationsPushoverSettingsUserKey": "Clé utilisateur", - "NotificationsPushoverSettingsRetryHelpText": "Intervalle pour réessayer les alertes d'urgence, minimum 30 secondes" + "NotificationsPushoverSettingsRetryHelpText": "Intervalle pour réessayer les alertes d'urgence, minimum 30 secondes", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Utiliser la disposition du contenu configurée par qBittorrent, la disposition originale du torrent ou toujours créer un sous-dossier (qBittorrent 4.3.2+)" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index cd465795e..71d8590e5 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1478,7 +1478,7 @@ "Files": "文件", "SeriesDetailsOneEpisodeFile": "1个集文件", "UrlBase": "基本URL", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {1} 导入之前,已下载的文件会被从您的客户端中移除。", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {appName} 导入之前,已下载的文件会被从您的客户端中移除。", "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", "AutoRedownloadFailed": "重新下载失败", "AutoRedownloadFailedFromInteractiveSearch": "从交互式搜索中重新下载失败", From db6a6279832793bf0e0e68df906a30f2d3e2d42e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 11 Jan 2024 17:14:37 -0800 Subject: [PATCH 012/762] Don't write global.json while updating API docs --- .github/workflows/api_docs.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/api_docs.yml b/.github/workflows/api_docs.yml index c1ea11cc0..1fc69c0fa 100644 --- a/.github/workflows/api_docs.yml +++ b/.github/workflows/api_docs.yml @@ -31,12 +31,6 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v3 id: setup-dotnet - with: - dotnet-version: '6.0.x' - - - name: Create temporary global.json - run: | - echo '{"sdk":{"version": "${{ steps.setup-dotnet.outputs.dotnet-version }}" } }' > ./global.json - name: Create openapi.json run: ./docs.sh Linux From 79907c881cc92ce9ee3973d5cf21749fe5fc58da Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Sat, 13 Jan 2024 20:29:54 +0100 Subject: [PATCH 013/762] Add: New icon for deleted episodes with status missing from disk Signed-off-by: Stevie Robinson --- frontend/src/Activity/History/HistoryEventTypeCell.js | 8 ++++---- frontend/src/Helpers/Props/icons.js | 2 ++ src/NzbDrone.Core/Localization/Core/en.json | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.js index cce30c6e5..2f5ef6ee1 100644 --- a/frontend/src/Activity/History/HistoryEventTypeCell.js +++ b/frontend/src/Activity/History/HistoryEventTypeCell.js @@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './HistoryEventTypeCell.css'; -function getIconName(eventType) { +function getIconName(eventType, data) { switch (eventType) { case 'grabbed': return icons.DOWNLOADING; @@ -17,7 +17,7 @@ function getIconName(eventType) { case 'downloadFailed': return icons.DOWNLOADING; case 'episodeFileDeleted': - return icons.DELETE; + return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE; case 'episodeFileRenamed': return icons.ORGANIZE; case 'downloadIgnored': @@ -47,7 +47,7 @@ function getTooltip(eventType, data) { case 'downloadFailed': return translate('DownloadFailedEpisodeTooltip'); case 'episodeFileDeleted': - return translate('EpisodeFileDeletedTooltip'); + return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip'); case 'episodeFileRenamed': return translate('EpisodeFileRenamedTooltip'); case 'downloadIgnored': @@ -58,7 +58,7 @@ function getTooltip(eventType, data) { } function HistoryEventTypeCell({ eventType, data }) { - const iconName = getIconName(eventType); + const iconName = getIconName(eventType, data); const iconKind = getIconKind(eventType); const tooltip = getTooltip(eventType, data); diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 37e670731..0d60c30e8 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -55,6 +55,7 @@ import { faEye as fasEye, faFastBackward as fasFastBackward, faFastForward as fasFastForward, + faFileCircleQuestion as fasFileCircleQuestion, faFileExport as fasFileExport, faFileInvoice as farFileInvoice, faFilter as fasFilter, @@ -146,6 +147,7 @@ export const EXPORT = fasFileExport; export const EXTERNAL_LINK = fasExternalLinkAlt; export const FATAL = fasTimesCircle; export const FILE = farFile; +export const FILE_MISSING = fasFileCircleQuestion; export const FILTER = fasFilter; export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b4f04f39e..11033f983 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -418,10 +418,10 @@ "DownloadClientPneumaticSettingsNzbFolderHelpText": "This folder will need to be reachable from XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Strm Folder", "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Download first and last pieces first (qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsContentLayout": "Content Layout", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Download first and last pieces first (qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Initial state for torrents added to qBittorrent. Note that Forced Torrents do not abide by seed restrictions", "DownloadClientQbittorrentSettingsSequentialOrder": "Sequential Order", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Download in sequential order (qBittorrent 4.1.0+)", @@ -574,6 +574,7 @@ "EpisodeDownloaded": "Episode Downloaded", "EpisodeFileDeleted": "Episode File Deleted", "EpisodeFileDeletedTooltip": "Episode file deleted", + "EpisodeFileMissingTooltip": "Episode file missing", "EpisodeFileRenamed": "Episode File Renamed", "EpisodeFileRenamedTooltip": "Episode file renamed", "EpisodeFilesLoadError": "Unable to load episode files", From 7e011df2b2c66ede0a0fbb3507a6ab89a7a08673 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 14 Jan 2024 00:32:28 +0000 Subject: [PATCH 014/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Daniele Prevedello Co-authored-by: DimitriDR Co-authored-by: Fixer Co-authored-by: Havok Dan Co-authored-by: Oskari Lavinto Co-authored-by: Watashi Co-authored-by: Weblate Co-authored-by: hansaudun Co-authored-by: hcharbonnier Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 57 ++++- src/NzbDrone.Core/Localization/Core/fr.json | 9 +- src/NzbDrone.Core/Localization/Core/it.json | 213 +++++++++++++++++- .../Localization/Core/nb_NO.json | 6 +- .../Localization/Core/pt_BR.json | 2 +- 5 files changed, 272 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 1629e4c2c..efa40d251 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -155,11 +155,11 @@ "DeleteQualityProfile": "Poista laatuprofiili", "DeleteTagMessageText": "Haluatko varmasti poistaa tunnisteen \"{label}\"?", "DownloadClientRootFolderHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset juurikansioon \"{rootFolderPath}\", mutta ne tulisi tallentaa muualle.", - "EditImportListExclusion": "Poista poikkeussääntö", + "EditImportListExclusion": "Muokkaa tuontilistapoikkeusta", "EnableMediaInfoHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvataa levyn ja/tai verkon kuormitusta tarkistusten aikana.", "HistoryLoadError": "Virhe ladattaessa historiaa", "Import": "Tuo", - "DownloadClientQbittorrentSettingsContentLayout": "Sisällön asettelu", + "DownloadClientQbittorrentSettingsContentLayout": "Sisällön rakenne", "MoreInfo": "Lisätietoja", "Network": "Kanava/tuottaja", "OnGrab": "Kun julkaisu kaapataan", @@ -279,7 +279,7 @@ "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", "RecentChanges": "Viimeisimmät muutokset", "ReleaseProfileTagSeriesHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", - "ReleaseTitle": "Julkaisunimike", + "ReleaseTitle": "Julkaisun nimike", "Reload": "Lataa uudelleen", "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etsijaintien kartoitukset lataustyökalun asetukset.", "RemoveTagsAutomatically": "Poista tunnisteet automaattisesti", @@ -634,7 +634,7 @@ "Agenda": "Agenda", "AnEpisodeIsDownloading": "Jaksoa ladataan", "ListOptionsLoadError": "Virhe ladattaessa tuontilista-asetuksia", - "RemoveCompleted": "Poisto on valmis", + "RemoveCompleted": "Poisto on suoritettu", "ICalShowAsAllDayEvents": "Näytä koko päivän tapahtumina", "FailedToLoadTagsFromApi": "Tunnisteiden lataus API:sta epäonnistui", "FilterContains": "sisältää", @@ -715,7 +715,7 @@ "SelectSeries": "Valitse sarjoja", "SelectSeasonModalTitle": "{modalTitle} - Valitse tuotantokausi", "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", - "TimeLeft": "Jäljellä oleva aika", + "TimeLeft": "Aikaa jäljellä", "Time": "Aika", "UpdateAvailableHealthCheckMessage": "Uusi päivitys on saatavilla", "SupportedDownloadClientsMoreInfo": "Saat tietoja yksittäisistä lataustyökaluista painamalla niiden ohessa olevia lisätietopainikkeita.", @@ -730,7 +730,7 @@ "AuthenticationRequiredUsernameHelpTextWarning": "Syötä uusi käyttäjätunnus", "BlocklistLoadError": "Virhe ladattaessa estolistaa", "Database": "Tietokanta", - "LastWriteTime": "Edellinen tallennusaika", + "LastWriteTime": "Viimeksi tallennettu", "ChownGroupHelpTextWarning": "Toimii vain, jos {appName}in suorittava käyttäjä on tiedoston omistaja. On parempi varmistaa, että lataustyökalu käyttää samaa ryhmää kuin {appName}.", "IndexerSettingsSeasonPackSeedTimeHelpText": "Aika, joka tuotantokausipaketin sisältävää torrentia tulee jakaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi.", "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", @@ -1087,7 +1087,7 @@ "UtcAirDate": "UTC-esitysaika", "FileManagement": "Tiedostojen hallinta", "InteractiveImportNoEpisode": "Jokaiselle valitulle tiedostolle on valittava ainakin yksi jakso.", - "ApiKeyValidationHealthCheckMessage": "Muuta API-avaimesi pituudeksi ainakin {length}. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa.", + "ApiKeyValidationHealthCheckMessage": "Muuta API-avaimesi ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa.", "Conditions": "Ehdot", "MinimumCustomFormatScore": "Mukautetun muodon vähimmäispisteytys", "Period": "Piste", @@ -1148,7 +1148,7 @@ "DownloadPropersAndRepacksHelpTextWarning": "Käytä mukautettuja muotoja automaattisiin Proper- ja Repack-päivityksiin.", "EnableInteractiveSearchHelpText": "Profiilia käytetään manuaalihakuun.", "EpisodeTitleRequiredHelpText": "Viivästytä tuontia enintään kaksi vuorokautta, jos jakson nimeä käytetään nimeämiseen ja nimeä ei ole vielä julkaistu.", - "ExtraFileExtensionsHelpTextsExamples": "Esimerkkejä: '\"sub, .nfo\" tai \"sub,nfo\".", + "ExtraFileExtensionsHelpTextsExamples": "Esimerkiksi '\"sub, .nfo\" tai \"sub,nfo\".", "DeleteSelectedEpisodeFiles": "Poista valitut jaksotiedostot", "Grab": "Kaappaa", "ImportUsingScriptHelpText": "Kopioi tiedostot tuontia varten oman komentosarjan avulla (esim. transkoodausta varten).", @@ -1277,5 +1277,44 @@ "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", - "Table": "Taulukko" + "Table": "Taulukko", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Käytä ohitusta, kun julkaisun pisteytys on määritetyn mukautetun muodon vähimmäispisteytystä korkeampi.", + "Renamed": "Nimetty uudelleen", + "External": "Ulkoinen", + "MaintenanceRelease": "Huoltojulkaisu: korjauksia ja muita parannuksia. Lue lisää Githubin muutoshistoriasta.", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Määrittää käytetäänkö qBittorrentista määritettyä rakennetta, torrentin alkuperäistä rakennetta vai luodaanko uusi alikansio (qBittorrent 4.3.2+).", + "EditRemotePathMapping": "Muokkaa kartoitettua etäsijaintia", + "LastUsed": "Viimeksi käytetty", + "Manual": "Manuaalinen", + "MoveAutomatically": "Siirrä automaattisesti", + "MoveFiles": "Siirrä tiedostot", + "RejectionCount": "Hylkäysmäärä", + "SelectFolder": "Valitse kansio", + "OnHealthRestored": "Terveystilan vakautuessa", + "OnRename": "Uudelleennimeäminen", + "OnlyTorrent": "Vain Torrent", + "Presets": "Esiasetukset", + "Genres": "Lajityypit", + "OnlyUsenet": "Vain Usenet", + "Test": "Testaa", + "Organize": "Järjestä", + "Rating": "Arvio", + "Events": "Tapahtumat", + "InstanceNameHelpText": "Instanssin nimi välilehdellä ja järjestelmälokissa.", + "Metadata": "Metatiedot", + "OnHealthIssue": "Vakausongelmat", + "PasswordConfirmation": "Salasanan vahvistus", + "Peers": "Vertaiset", + "Save": "Tallenna", + "Seeders": "Jakajat", + "UpgradesAllowed": "Päivitykset sallitaan", + "OnUpgrade": "Päivitettäessä", + "UnmappedFilesOnly": "Vain kartoittamattomat tiedostot", + "Ignored": "Ohitettu", + "PreferAndUpgrade": "Suosi ja päivitä", + "Failed": "Epäonnistui", + "Implementation": "Toteutus", + "MediaManagement": "Median hallinta", + "Ok": "Ok", + "General": "Yleiset" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 706c980f8..ea581c05f 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1354,7 +1354,7 @@ "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} » ?", - "DeleteTag": "Supprimer la balise", + "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", @@ -1625,7 +1625,7 @@ "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": "Supports", + "IndexerHDBitsSettingsMediums": "Type de médias", "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.", @@ -1868,5 +1868,8 @@ "NotificationsSynologySettingsUpdateLibraryHelpText": "Appelle synoindex sur l'hôte local pour mettre à jour le fichier de bibliothèque", "NotificationsPushoverSettingsUserKey": "Clé utilisateur", "NotificationsPushoverSettingsRetryHelpText": "Intervalle pour réessayer les alertes d'urgence, minimum 30 secondes", - "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Utiliser la disposition du contenu configurée par qBittorrent, la disposition originale du torrent ou toujours créer un sous-dossier (qBittorrent 4.3.2+)" + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Utiliser la disposition du contenu configurée par qBittorrent, la disposition originale du torrent ou toujours créer un sous-dossier (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsContentLayout": "Disposition du contenu", + "NotificationsGotifySettingIncludeSeriesPoster": "Inclure l'affiche de la série", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Inclure l'affiche de la série dans le message" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index eba20be04..65dbc3e61 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -30,5 +30,216 @@ "ApplyTagsHelpTextHowToApplySeries": "Come applicare le etichette alle serie selezionate", "AddImportList": "Aggiungi lista da importare", "AddCondition": "Aggiungi Condizione", - "CloneCondition": "Duplica Condizione" + "CloneCondition": "Duplica Condizione", + "Ended": "Finito", + "BuiltIn": "Incluso", + "AppDataLocationHealthCheckMessage": "L'aggiornamento non sarà possibile per evitare la cancellazione di AppData durante l'aggiornamento", + "CountSeasons": "{count} Stagioni", + "HideAdvanced": "Nascondi Avanzate", + "Absolute": "Assoluto", + "AddANewPath": "Aggiungi nuovo percorso", + "AddConditionImplementation": "Aggiungi Condizione - {implementationName}", + "AddConnectionImplementation": "Aggiungi Connessione - {implementationName}", + "AddCustomFilter": "Aggiungi Filtro Personalizzato", + "AddDownloadClientImplementation": "Aggiungi un Client di Download - {implementationName}", + "AddExclusion": "Aggiungi Esclusione", + "AddIndexerImplementation": "Aggiungi indicizzatore - {implementationName}", + "AddNewSeries": "Aggiungi Nuova Serie", + "AddNewSeriesRootFolderHelpText": "La cartella '{folder}' verrà creata automaticamente", + "AddNewSeriesError": "Caricamento dei risultati della ricerca fallito, prova ancora.", + "AddSeriesWithTitle": "Aggiungi {title}", + "AddNewSeriesSearchForMissingEpisodes": "Avvia la ricerca degli episodi mancanti", + "AddToDownloadQueue": "Aggiungi alla coda dei download", + "AddedToDownloadQueue": "Aggiunto alla coda dei download", + "Airs": "Trasmesso", + "AirsTomorrowOn": "Domani alle {time} su {networkLabel}", + "AllFiles": "Tutti i File", + "AllSeriesInRootFolderHaveBeenImported": "Tutte le serie in {path} sono state importate", + "Any": "Qualunque", + "AppUpdated": "{appName} Aggiornato", + "AppUpdatedVersion": "{appName} è stato aggiornato alla versione `{version}`, per vedere le modifiche devi ricaricare {appName} ", + "ApplicationURL": "URL Applicazione", + "AuthenticationMethodHelpText": "Inserisci Username e Password per accedere a {appName}", + "BindAddressHelpText": "Indirizzi IP validi, localhost o '*' per tutte le interfacce", + "BeforeUpdate": "Prima dell'aggiornamento", + "CalendarFeed": "Feed calendario {appName}", + "CalendarOptions": "Opzioni del Calendario", + "ChooseImportMode": "Selezionare Metodo di Importazione", + "CollapseMultipleEpisodes": "Collassa Episodi Multipli", + "Conditions": "Condizioni", + "Continuing": "In Corso", + "CountSelectedFile": "{selectedCount} file selezionati", + "DeleteDownloadClient": "Cancella Client di Download", + "DefaultNameCopiedProfile": "{name} - Copia", + "DefaultNameCopiedSpecification": "{name} - Copia", + "DeleteCustomFormat": "Cancella Formato Personalizzato", + "DeleteEpisodeFile": "Cancella File Episodio", + "HiddenClickToShow": "Nascosto, premi per mostrare", + "ImportListStatusUnavailableHealthCheckMessage": "Liste non disponibili a causa di errori: {importListNames}", + "AnalyseVideoFiles": "Analizza i file video", + "Anime": "Anime", + "AgeWhenGrabbed": "Età (quando recuperato)", + "AuthenticationRequiredWarning": "Per prevenire accessi remoti non autorizzati, {appName} da ora richiede che l'autenticazione sia abilitata. Opzionalmente puoi disabilitare l'autenticazione per gli indirizzi di rete locali.", + "AutoTagging": "Tagging Automatico", + "BrowserReloadRequired": "Richiede il reload del Browser", + "Calendar": "Calendario", + "CertificateValidation": "Convalida del Certificato", + "CertificateValidationHelpText": "Cambia quanto rigorosamente vengono validati i certificati HTTPS. Non cambiare senza conoscerne i rischi.", + "Certification": "Certificazione", + "ChangeFileDate": "Cambiare la Data del File", + "ChooseAnotherFolder": "Scegli un'altra cartella", + "ChownGroup": "Gruppo chown", + "ChmodFolderHelpTextWarning": "Funziona solo se l'utente di {appName} è il proprietario del file. E' meglio assicurarsi che il client di download imposti i permessi correttamente.", + "CloneAutoTag": "Copia Auto Tag", + "Clone": "Copia", + "CloneCustomFormat": "Copia Formato Personalizzato", + "CloneIndexer": "Copia Indicizzatore", + "CloneProfile": "Copia Profilo", + "ContinuingOnly": "Solo In Corso", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "Non è disponibile nessun client di download", + "DownloadClientStatusSingleClientHealthCheckMessage": "Client per il download non disponibili per errori: {downloadClientNames}", + "AddAutoTag": "Aggiungi Etichetta Automatica", + "AbsoluteEpisodeNumbers": "Numeri Episodi Assoluti", + "Cancel": "Annulla", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Come applicare etichette ai client di download selezionati", + "ApplyTagsHelpTextHowToApplyImportLists": "Come applicare etichette alle liste di importazione selezionate", + "AddRootFolder": "Aggiungi Cartella Radice", + "AbsoluteEpisodeNumber": "Numero Episodio Assoluto", + "AddConditionError": "Non è stato possibile aggiungere una nuova condizione. Riprova.", + "AddConnection": "Aggiungi Connessione", + "AddAutoTagError": "Non è stato possibile aggiungere una nuova etichetta automatica. Riprova.", + "AddCustomFormat": "Aggiungi Formato Personalizzato", + "AddDownloadClient": "Aggiungi Client di Download", + "AddCustomFormatError": "Non riesco ad 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.", + "AddIndexer": "Aggiungi Indicizzatore", + "AddList": "Aggiungi Lista", + "AddListError": "Non riesco ad aggiungere una nuova lista, riprova.", + "AddNewRestriction": "Aggiungi una nuova restrizione", + "AddListExclusionError": "Non riesco ad aggiungere una nuova lista di esclusione, riprova.", + "AddImportListExclusion": "Cancellare la lista delle esclusioni", + "AddImportListExclusionError": "Non riesco ad aggiungere una nuova lista di esclusione, riprova.", + "AddNotificationError": "Impossibile aggiungere una nuova notifica, riprova.", + "AddQualityProfile": "Aggiungi un Profilo Qualità", + "AddReleaseProfile": "Aggiungi un Profilo Release", + "AddQualityProfileError": "Non riesco ad aggiungere un nuovo profilo di qualità, riprova.", + "AddRemotePathMapping": "Aggiungi Mappatura di un Percorso Remoto", + "AddRemotePathMappingError": "Non riesco ad aggiungere la mappatura di un nuovo percorso remoto, riprova.", + "AfterManualRefresh": "Dopo l'aggiornamento manuale", + "Analytics": "Statistiche", + "AnimeEpisodeFormat": "Formato Episodi Anime", + "ApplicationUrlHelpText": "L'URL esterno di questa applicazione, incluso http(s)://, porta e URL base", + "AuthBasic": "Base (Popup del Browser)", + "AuthForm": "Form (Pagina di Login)", + "Authentication": "Autenticazione", + "AuthenticationRequiredHelpText": "Cambia a quali richieste l'autenticazione verrà chiesta. Non cambiare se non comprendi i rischi.", + "AuthenticationRequired": "Autenticazione richiesta", + "BackupRetentionHelpText": "I backup più vecchi del periodo specificato saranno cancellati automaticamente", + "BackupIntervalHelpText": "Intervallo fra i backup automatici", + "BindAddress": "Indirizzo di Ascolto", + "BackupsLoadError": "Impossibile caricare i backup", + "ChangeFileDateHelpText": "Modifica la data dei file in importazione/rescan", + "ChmodFolder": "Permessi Cartella", + "ClientPriority": "Priorità Client", + "ConnectSettings": "Impostazioni Collegamento", + "CreateGroup": "Crea gruppo", + "DeleteEmptyFolders": "Cancella le cartelle vuote", + "Enabled": "Abilitato", + "UpdateMechanismHelpText": "Usa il sistema di aggiornamento interno di {appName} o uno script", + "AllResultsAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro applicato", + "EditSelectedDownloadClients": "Modifica i Client di Download Selezionati", + "EditSelectedImportLists": "Modifica le Liste di Importazione Selezionate", + "Age": "Età", + "All": "Tutti", + "AudioInfo": "Informazioni Audio", + "AudioLanguages": "Lingua Audio", + "DeleteBackup": "Cancella Backup", + "DownloadClientSortingHealthCheckMessage": "Il client di download {downloadClientName} ha l'ordinamento {sortingMode} abilitato per la categoria di {appName}. Dovresti disabilitare l'ordinamento nel tuo client di download per evitare problemi di importazione.", + "EnableAutomaticSearch": "Attiva la Ricerca Automatica", + "ImportListRootFolderMissingRootHealthCheckMessage": "Persa la cartella principale per l’importazione delle liste : {rootFolderInfo}", + "ImportListStatusAllUnavailableHealthCheckMessage": "Tutte le liste non sono disponibili a causa di errori", + "AppDataDirectory": "Cartella AppData", + "BackupFolderHelpText": "I percorsi relativi saranno nella cartella AppData di {appName}", + "AnalyticsEnabledHelpText": "Inviare informazioni anonime sull'utilizzo e sugli errori ai server di {appName}. Ciò include informazioni sul tuo browser, quali pagine dell'interfaccia di {appName} usi, la segnalazione di errori così come la versione del sistema operativo e del runtime. Utilizzeremo queste informazioni per dare priorità alle nuove funzioni e alle correzioni di bug.", + "Connection": "Connessione", + "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}", + "AllSeriesAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro", + "AlreadyInYourLibrary": "Già presente nella tua libreria", + "CollapseAll": "Collassa tutto", + "AlternateTitles": "Titolo alternativo", + "Always": "Sempre", + "AnEpisodeIsDownloading": "Un episodio è in download", + "Connections": "Connessioni", + "Agenda": "Agenda", + "DelayProfileProtocol": "Protocollo: {preferredProtocol}", + "CopyToClipboard": "Copia negli Appunti", + "ChownGroupHelpTextWarning": "Funziona solo se l'utente di {appName} è il proprietario del file. E' meglio assicurarsi che il client di download usi lo stesso gruppo di {appName}.", + "ClearBlocklist": "Svuota Lista dei Blocchi", + "Date": "Data", + "Dates": "Date", + "DelayMinutes": "{delay} Minuti", + "Delete": "Cancella", + "DeleteIndexer": "Cancella Indicizzatore", + "EnableInteractiveSearch": "Abilita la Ricerca Interattiva", + "AuthenticationMethod": "Metodo di Autenticazione", + "AuthenticationRequiredPasswordHelpTextWarning": "Inserisci la nuova password", + "AuthenticationRequiredUsernameHelpTextWarning": "Inserisci username", + "AuthenticationMethodHelpTextWarning": "Selezione un metodo di autenticazione valido", + "CalendarLegendEpisodeMissingTooltip": "Episodio trasmesso non scaricato sul disco", + "CalendarLegendEpisodeOnAirTooltip": "L'episodio viene trasmesso", + "CalendarLegendEpisodeUnairedTooltip": "Episodio non ancora trasmesso", + "CalendarLegendEpisodeUnmonitoredTooltip": "Episodio non monitorato", + "CalendarLegendSeriesFinaleTooltip": "Finale di serie o stagione", + "CalendarLegendSeriesPremiereTooltip": "Premiere della serie o stagione", + "DailyEpisodeTypeFormat": "Data ({format})", + "CalendarLegendEpisodeDownloadingTooltip": "L'episodio è in download", + "Automatic": "Automatico", + "CalendarLegendEpisodeDownloadedTooltip": "L'episodio è stato scaricato e salvato", + "CheckDownloadClientForDetails": "controlla il client di download per maggiori dettagli", + "ChmodFolderHelpText": "Octal, applicato durante l'importazione/rinomina verso cartelle e file (senza bits di esecuzione)", + "ClearBlocklistMessageText": "Sicuro di voler cancellare la Lista dei Blocchi?", + "AddRootFolderError": "Impossibile aggiungere cartella radice", + "AirsDateAtTimeOn": "il {date} alle {time} su {networkLabel}", + "AutomaticSearch": "Ricerca Automatica", + "ClickToChangeEpisode": "Click per cambiare episodio", + "ClickToChangeLanguage": "Click per cambiare lingua", + "ClickToChangeQuality": "Click per cambiare qualità", + "ClickToChangeSeries": "Click per cambiare serie", + "Close": "Chiudi", + "Component": "Componente", + "ConnectionLost": "Connessione Persa", + "ApiKey": "Chiave API", + "CountSelectedFiles": "{selectedCount} file selezionati", + "CountSeriesSelected": "{count} serie selezionate", + "CustomFilters": "Filtri Personalizzati", + "CustomFormat": "Formato Personalizzato", + "CustomFormatScore": "Formato Personalizzato Punteggio", + "CustomFormats": "Formati Personalizzati", + "CustomFormatsSettingsSummary": "Formati Personalizzati Impostazioni", + "Daily": "Giornalmente", + "DeleteCondition": "Cancella Condizione", + "DeleteEpisodeFromDisk": "Cancella episodio dal disco", + "DeleteNotification": "Cancella Notifica", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}.", + "Connect": "Collegamento", + "CustomFormatsSettings": "Formati Personalizzati Impostazioni", + "Condition": "Condizione", + "CalendarLoadError": "Impossibile caricare il calendario", + "CancelPendingTask": "Sei sicuro di voler cancellare questa operazione in sospeso?", + "CancelProcessing": "Annulla Elaborazione", + "Category": "Categoria", + "ChownGroupHelpText": "Nome del gruppo o gid. Usa gid per sistemi di file remoti.", + "Clear": "Cancella", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Conferma la nuova password", + "ClickToChangeSeason": "Click per cambiare stagione", + "AnalyseVideoFilesHelpText": "Estrai le informazioni video come risoluzione, durata e codec dai file. Questo richiede che {appName} legga delle parti dei file, ciò potrebbe causare un alto utilizzo del disco e della rete durante le scansioni.", + "DownloadClientStatusAllClientHealthCheckMessage": "Nessun client di download è disponibile a causa di errori" } diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index 83382909f..b35dd0ce5 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -3,5 +3,9 @@ "ApplyChanges": "Bekreft endringer", "AllTitles": "Alle titler", "AddAutoTag": "Legg til automatisk tagg", - "AddCondition": "Legg til betingelse" + "AddCondition": "Legg til betingelse", + "Add": "Legg til", + "Absolute": "Absolutt", + "Activity": "Aktivitet", + "About": "Om" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 1aca3ed6f..092947518 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -526,7 +526,7 @@ "DeleteRemotePathMappingMessageText": "Tem certeza de que deseja excluir este mapeamento de caminho remoto?", "DeleteSpecification": "Excluir Especificação", "DeleteSpecificationHelpText": "Tem certeza de que deseja excluir a especificação '{name}'?", - "DeleteTag": "Excluir Tag", + "DeleteTag": "Excluir Etiqueta", "DeleteTagMessageText": "Tem certeza de que deseja excluir a tag '{label}'?", "DisabledForLocalAddresses": "Desabilitado para endereços locais", "DoNotPrefer": "Não Preferir", From 637cb1711d51290022a8d03f841f26a979c3c6b1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jan 2024 09:43:14 -0800 Subject: [PATCH 015/762] Update PR template [skip ci] --- .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d8f748ad9..e4e399d91 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,13 @@ -#### Database Migration -YES | NO - #### Description A few sentences describing the overall goals of the pull request's commits. -#### Todos -- [ ] Tests -- [ ] Wiki Updates + +#### Screenshots for UI Changes + + +#### Database Migration +YES - ### | NO #### Issues Fixed or Closed by this PR - -* +* Closes # From 57bd6539c81c2cdbd12fd13c28218d70eaa9382a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jan 2024 09:44:37 -0800 Subject: [PATCH 016/762] Update bug_report.yml [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b79ba95b5..4e4eedb66 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Only bug reports for v4 will be accepted, older versions are EOL & unsupported.' +description: 'Only bug reports for v4 will be accepted, older versions are no longer receiving bug fixes and support issues will be closed immediately.' labels: ['needs-triage'] body: - type: checkboxes From 431c66c7c141addd97174d4fea8ce42b7125032f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jan 2024 14:09:27 -0500 Subject: [PATCH 017/762] Update PR Template [skip ci] --- .github/PULL_REQUEST_TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e4e399d91..25b1761f9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,15 @@ #### Description A few sentences describing the overall goals of the pull request's commits. + #### Screenshots for UI Changes #### Database Migration -YES - ### | NO +YES - ### #### Issues Fixed or Closed by this PR * Closes # + From 16e5ffa467f72e52c750143c835f6ee1c1c2460b Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sun, 14 Jan 2024 14:16:53 -0600 Subject: [PATCH 018/762] Update logging to indicate a hardlink is being attempted --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index eac485671..1f2f3cb9c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -95,7 +95,7 @@ namespace NzbDrone.Core.MediaFiles if (_configService.CopyUsingHardlinks) { - _logger.Debug("Hardlinking episode file: {0} to {1}", episodeFile.Path, filePath); + _logger.Debug("Attempting to hardlink episode file: {0} to {1}", episodeFile.Path, filePath); return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.HardLinkOrCopy, localEpisode); } From ee0048c768f3c12c10bbb0a3cb3221ebe3c95e5c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jan 2024 09:55:37 -0800 Subject: [PATCH 019/762] Fixed: Reprocessing multi-language file in Manual Import --- src/Sonarr.Api.V3/ManualImport/ManualImportController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index bb7b77258..65b7f3bf1 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.ManualImport // Only set the language/quality if they're unknown and languages were returned. // Languages won't be returned when reprocessing if the season/episode isn't filled in yet and we don't want to return no languages to the client. - if ((item.Languages.SingleOrDefault() ?? Language.Unknown) == Language.Unknown && processedItem.Languages.Any()) + if (item.Languages.Count <= 1 && (item.Languages.SingleOrDefault() ?? Language.Unknown) == Language.Unknown && processedItem.Languages.Any()) { item.Languages = processedItem.Languages; } From 0685896ed8263ef6d05a933acaf584e6f4aa9f92 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jan 2024 09:56:39 -0800 Subject: [PATCH 020/762] Fixed: Prevent selecting season or episode in Manual Import if series or episode is not selected Closes #6354 --- .../InteractiveImportModalContent.tsx | 96 +++++++++++-------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index d191e8d10..071ec650c 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -269,33 +269,6 @@ function InteractiveImportModalContent( const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] = useState(null); const [selectState, setSelectState] = useSelectState(); - const [bulkSelectOptions, setBulkSelectOptions] = useState([ - { - key: 'select', - value: translate('SelectDropdown'), - disabled: true, - }, - { - key: 'season', - value: translate('SelectSeason'), - }, - { - key: 'episode', - value: translate('SelectEpisodes'), - }, - { - key: 'quality', - value: translate('SelectQuality'), - }, - { - key: 'releaseGroup', - value: translate('SelectReleaseGroup'), - }, - { - key: 'language', - value: translate('SelectLanguage'), - }, - ]); const { allSelected, allUnselected, selectedState } = selectState; const previousIsDeleting = usePrevious(isDeleting); const dispatch = useDispatch(); @@ -318,19 +291,66 @@ function InteractiveImportModalContent( return getSelectedIds(selectedState); }, [selectedState]); + const bulkSelectOptions = useMemo(() => { + const { seasonSelectDisabled, episodeSelectDisabled } = items.reduce( + (acc, item) => { + if (!selectedIds.includes(item.id)) { + return acc; + } + + acc.seasonSelectDisabled ||= !item.series; + acc.episodeSelectDisabled ||= !item.seasonNumber; + + return acc; + }, + { + seasonSelectDisabled: false, + episodeSelectDisabled: false, + } + ); + + const options = [ + { + key: 'select', + value: translate('SelectDropdown'), + disabled: true, + }, + { + key: 'season', + value: translate('SelectSeason'), + disabled: seasonSelectDisabled, + }, + { + key: 'episode', + value: translate('SelectEpisodes'), + disabled: episodeSelectDisabled, + }, + { + key: 'quality', + value: translate('SelectQuality'), + }, + { + key: 'releaseGroup', + value: translate('SelectReleaseGroup'), + }, + { + key: 'language', + value: translate('SelectLanguage'), + }, + ]; + + if (allowSeriesChange) { + options.splice(1, 0, { + key: 'series', + value: translate('SelectSeries'), + }); + } + + return options; + }, [allowSeriesChange, items, selectedIds]); + useEffect( () => { - if (allowSeriesChange) { - const newBulkSelectOptions = [...bulkSelectOptions]; - - newBulkSelectOptions.splice(1, 0, { - key: 'series', - value: translate('SelectSeries'), - }); - - setBulkSelectOptions(newBulkSelectOptions); - } - if (initialSortKey) { const sortProps: { sortKey: string; sortDirection?: string } = { sortKey: initialSortKey, From ec91142c855571f44c06b7ce6ef753ee9b69a1d5 Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 15 Jan 2024 21:11:45 -0600 Subject: [PATCH 021/762] Fixed: Only use frames for Primary video stream for analysis (cherry picked from commit 581828b0dcfcd4aa1ae581b899f812071457c9ca) --- src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs | 4 ++-- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 296c7632e..58674b44d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo private readonly List _pixelFormats; public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8; - public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 9; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 10; private static readonly string[] ValidHdrColourPrimaries = { "bt2020" }; private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" }; @@ -117,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo // if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics)) { - var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = "-read_intervals \"%+#1\" -select_streams v" }); + var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = $"-read_intervals \"%+#1\" -select_streams v:{primaryVideoStream?.Index ?? 0}" }); mediaInfoModel.RawFrameData = frameOutput; frames = FFProbe.AnalyseFrameJson(frameOutput); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 1560150f0..791469fd6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -686,7 +686,7 @@ namespace NzbDrone.Core.Organizer new Dictionary(FileNameBuilderTokenEqualityComparer.Instance) { { MediaInfoVideoDynamicRangeToken, 5 }, - { MediaInfoVideoDynamicRangeTypeToken, 9 } + { MediaInfoVideoDynamicRangeTypeToken, 10 } }; private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) From 57445bbe57a84990e284ef97d42455a06587e1ee Mon Sep 17 00:00:00 2001 From: Rubicj Date: Mon, 15 Jan 2024 21:28:28 -0800 Subject: [PATCH 022/762] New: Added column in Queue Closes #6270 --- frontend/src/Activity/Queue/QueueRow.js | 11 +++++++++++ frontend/src/Store/Actions/queueActions.js | 6 ++++++ frontend/src/typings/Queue.ts | 1 + .../Download/Pending/PendingReleaseService.cs | 1 + .../Download/TrackedDownloads/TrackedDownload.cs | 1 + .../TrackedDownloads/TrackedDownloadService.cs | 1 + ...dCompletionTimeComparer.cs => DatetimeComparer.cs} | 2 +- src/NzbDrone.Core/Queue/Queue.cs | 1 + src/NzbDrone.Core/Queue/QueueService.cs | 3 ++- src/Sonarr.Api.V3/Queue/QueueController.cs | 11 +++++++++-- src/Sonarr.Api.V3/Queue/QueueResource.cs | 2 ++ 11 files changed, 36 insertions(+), 4 deletions(-) rename src/NzbDrone.Core/Queue/{EstimatedCompletionTimeComparer.cs => DatetimeComparer.cs} (90%) diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 1fab52bd9..95ff2527e 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -100,6 +100,7 @@ class QueueRow extends Component { outputPath, downloadClient, estimatedCompletionTime, + added, timeleft, size, sizeleft, @@ -362,6 +363,15 @@ class QueueRow extends Component { ); } + if (name === 'added') { + return ( + + ); + } + if (name === 'actions') { return ( translate('Added'), + isSortable: true, + isVisible: false + }, { name: 'progress', label: () => translate('Progress'), diff --git a/frontend/src/typings/Queue.ts b/frontend/src/typings/Queue.ts index 855173306..8fe114489 100644 --- a/frontend/src/typings/Queue.ts +++ b/frontend/src/typings/Queue.ts @@ -28,6 +28,7 @@ interface Queue extends ModelBase { sizeleft: number; timeleft: string; estimatedCompletionTime: string; + added?: string; status: string; trackedDownloadStatus: QueueTrackedDownloadStatus; trackedDownloadState: QueueTrackedDownloadState; diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index c4d862fca..6f16dfe3a 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -368,6 +368,7 @@ namespace NzbDrone.Core.Download.Pending RemoteEpisode = pendingRelease.RemoteEpisode, Timeleft = timeleft, EstimatedCompletionTime = ect, + Added = pendingRelease.Added, Status = pendingRelease.Reason.ToString(), Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol, Indexer = pendingRelease.RemoteEpisode.Release.Indexer diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 0b20d3f6f..05c25db81 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } + public DateTime? Added { get; set; } public bool IsTrackable { get; set; } public bool HasNotifiedManualInteractionRequired { get; set; } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index da0658895..bab0a35f9 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -135,6 +135,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EpisodeHistoryEventType.Grabbed); trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + trackedDownload.Added = grabbedEvent?.Date; if (parsedEpisodeInfo == null || trackedDownload.RemoteEpisode == null || diff --git a/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs b/src/NzbDrone.Core/Queue/DatetimeComparer.cs similarity index 90% rename from src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs rename to src/NzbDrone.Core/Queue/DatetimeComparer.cs index e8c52e1ab..e851d5a5f 100644 --- a/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs +++ b/src/NzbDrone.Core/Queue/DatetimeComparer.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace NzbDrone.Core.Queue { - public class EstimatedCompletionTimeComparer : IComparer + public class DatetimeComparer : IComparer { public int Compare(DateTime? x, DateTime? y) { diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index da94f8911..15ff7948a 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.Queue public decimal Sizeleft { get; set; } public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } + public DateTime? Added { get; set; } public string Status { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } public TrackedDownloadState? TrackedDownloadState { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index abd0742ad..6b4aadb4c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -79,7 +79,8 @@ namespace NzbDrone.Core.Queue Protocol = trackedDownload.Protocol, DownloadClient = trackedDownload.DownloadItem.DownloadClientInfo.Name, Indexer = trackedDownload.Indexer, - OutputPath = trackedDownload.DownloadItem.OutputPath.ToString() + OutputPath = trackedDownload.DownloadItem.OutputPath.ToString(), + Added = trackedDownload.Added }; queue.Id = HashConverter.GetHashInt31($"trackedDownload-{trackedDownload.DownloadClient}-{trackedDownload.DownloadItem.DownloadId}-ep{episode?.Id ?? 0}"); diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index c3e4c1d52..744fedda3 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -193,9 +193,16 @@ namespace Sonarr.Api.V3.Queue else if (pagingSpec.SortKey == "estimatedCompletionTime") { ordered = ascending - ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) + ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new DatetimeComparer()) : fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, - new EstimatedCompletionTimeComparer()); + new DatetimeComparer()); + } + else if (pagingSpec.SortKey == "added") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Added, new DatetimeComparer()) + : fullQueue.OrderByDescending(q => q.Added, + new DatetimeComparer()); } else if (pagingSpec.SortKey == "protocol") { diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index e56ee2511..6aaf3b1ed 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -29,6 +29,7 @@ namespace Sonarr.Api.V3.Queue public decimal Sizeleft { get; set; } public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } + public DateTime? Added { get; set; } public string Status { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } public TrackedDownloadState? TrackedDownloadState { get; set; } @@ -71,6 +72,7 @@ namespace Sonarr.Api.V3.Queue Sizeleft = model.Sizeleft, Timeleft = model.Timeleft, EstimatedCompletionTime = model.EstimatedCompletionTime, + Added = model.Added, Status = model.Status.FirstCharToLower(), TrackedDownloadStatus = model.TrackedDownloadStatus, TrackedDownloadState = model.TrackedDownloadState, From 1bba7e177b5b4173e620cd014ffdc231023309a0 Mon Sep 17 00:00:00 2001 From: Blair Noctis <4474501+nc7s@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:50:25 +0800 Subject: [PATCH 023/762] Fixed: Improve help text for download client priority Closes #6270 --- .../DownloadClients/EditDownloadClientModalContent.js | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 52493adf5..9fe0a2f25 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -133,7 +133,7 @@ class EditDownloadClientModalContent extends Component { Date: Sun, 14 Jan 2024 15:25:25 -0600 Subject: [PATCH 024/762] Improved http timeout handling --- .../Http/HttpClientFixture.cs | 10 +++++ .../Http/Dispatchers/ManagedHttpDispatcher.cs | 43 +++++++++++-------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 4fbd88a50..b27798a3e 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -131,6 +131,16 @@ namespace NzbDrone.Common.Test.Http response.Content.Should().NotBeNullOrWhiteSpace(); } + [Test] + public void should_throw_timeout_request() + { + var request = new HttpRequest($"https://{_httpBinHost}/delay/10"); + + request.RequestTimeout = new TimeSpan(0, 0, 5); + + Assert.ThrowsAsync(async () => await Subject.ExecuteAsync(request)); + } + [Test] public async Task should_execute_https_get() { diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 5281d74fe..9eb4cc1e4 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -102,31 +102,38 @@ namespace NzbDrone.Common.Http.Dispatchers var httpClient = GetClient(request.Url); - using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); + try { - byte[] data = null; - - try + using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); { - if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) + byte[] data = null; + + try { - await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token); + if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) + { + await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token); + } + else + { + data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token); + } } - else + catch (Exception ex) { - data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token); + throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null); } + + var headers = responseMessage.Headers.ToNameValueCollection(); + + headers.Add(responseMessage.Content.Headers.ToNameValueCollection()); + + return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version); } - catch (Exception ex) - { - throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null); - } - - var headers = responseMessage.Headers.ToNameValueCollection(); - - headers.Add(responseMessage.Content.Headers.ToNameValueCollection()); - - return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version); + } + catch (OperationCanceledException ex) when (cts.IsCancellationRequested) + { + throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null); } } From 091449d9bff9023ca27a85cc1048296f7d5ea37b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 10 Sep 2023 09:26:11 +0300 Subject: [PATCH 025/762] Throw download as failed for invalid magnet links --- src/NzbDrone.Core/Download/TorrentClientBase.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index dd653968c..978a0c058 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -225,9 +225,7 @@ namespace NzbDrone.Core.Download } catch (FormatException ex) { - _logger.Error(ex, "Failed to parse magnetlink for episode '{0}': '{1}'", remoteEpisode.Release.Title, magnetUrl); - - return null; + throw new ReleaseDownloadException(remoteEpisode.Release, "Failed to parse magnetlink for episode '{0}': '{1}'", ex, remoteEpisode.Release.Title, magnetUrl); } if (hash != null) From 666455f9b135df4424155102b58ba9324f1a671a Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Tue, 16 Jan 2024 06:52:40 +0100 Subject: [PATCH 026/762] New: Add 'zhtw' and 'yue' language codes as Chinese language Closes #6363 --- src/NzbDrone.Core/Parser/IsoLanguages.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index 6eab3bd2c..99d3c972c 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -64,7 +64,9 @@ namespace NzbDrone.Core.Parser { "cze", Language.Czech }, { "dut", Language.Dutch }, { "mac", Language.Macedonian }, - { "rum", Language.Romanian } + { "rum", Language.Romanian }, + { "yue", Language.Chinese }, + { "zhtw", Language.Chinese } }; public static IsoLanguage Find(string isoCode) @@ -86,6 +88,10 @@ namespace NzbDrone.Core.Parser return isoLanguages.FirstOrDefault(); } + else if (AlternateIsoCodeMappings.TryGetValue(isoCode, out var alternateLanguage)) + { + return Get(alternateLanguage); + } else if (langCode.Length == 3) { // Lookup ISO639-2T code @@ -96,10 +102,6 @@ namespace NzbDrone.Core.Parser return All.FirstOrDefault(l => l.ThreeLetterCode == langCode); } - else if (AlternateIsoCodeMappings.TryGetValue(isoCode, out var alternateLanguage)) - { - return Get(alternateLanguage); - } return null; } From 07fbb0d1f464513ed28721d6c91d428dd54818cf Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Tue, 16 Jan 2024 06:52:55 +0100 Subject: [PATCH 027/762] Add missing translation keys from Indexer Settings Signed-off-by: Stevie Robinson --- .../Indexers/BroadcastheNet/BroadcastheNetSettings.cs | 2 +- src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs | 2 +- src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs | 2 +- src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs | 2 +- src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs | 2 +- .../Indexers/TorrentRss/TorrentRssIndexerSettings.cs | 2 +- src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs | 2 +- src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 9 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index 00c153ceb..7f4491a2b 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [FieldDefinition(3)] public SeedCriteriaSettings SeedCriteria { get; set; } = new (); - [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 59eb7c13d..2ef02c7de 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.FileList [FieldDefinition(7)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(8, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index 1e35ca310..b5833789f 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(7)] public SeedCriteriaSettings SeedCriteria { get; set; } = new (); - [FieldDefinition(8, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index f1c3bb490..5c1271459 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents [FieldDefinition(2)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(3, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index f6c40e713..6983b2d67 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(4)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 6543b5a7f..898442bf7 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(4)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index 51e21b31c..d999d84ba 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(3)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index eb11a079c..6ed534bb2 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers.Torznab [FieldDefinition(8)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(9, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", Advanced = true)] + [FieldDefinition(9, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index d292b36e0..13dab7ab6 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -819,6 +819,8 @@ "IndexerSettingsMinimumSeeders": "Minimum Seeders", "IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.", "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", From e4b5d559df2d5f3d55e16aae5922509e84f31e64 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Tue, 16 Jan 2024 06:53:21 +0100 Subject: [PATCH 028/762] Sort Custom Filters Closes #6334 --- .../CustomFiltersModalContent.js | 34 ++++++++++--------- .../src/Components/Menu/FilterMenuContent.js | 32 ++++++++++------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 07660426e..28eb91599 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) { { - customFilters.map((customFilter) => { - return ( - - ); - }) + customFilters + .sort((a, b) => a.label.localeCompare(b.label)) + .map((customFilter) => { + return ( + + ); + }) }
diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js index 516fbb648..4ee406224 100644 --- a/frontend/src/Components/Menu/FilterMenuContent.js +++ b/frontend/src/Components/Menu/FilterMenuContent.js @@ -40,18 +40,26 @@ class FilterMenuContent extends Component { } { - customFilters.map((filter) => { - return ( - - {filter.label} - - ); - }) + customFilters.length > 0 ? + : + null + } + + { + customFilters + .sort((a, b) => a.label.localeCompare(b.label)) + .map((filter) => { + return ( + + {filter.label} + + ); + }) } { From 489f03441bafaea242a6425c36eefd95c38e62bb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 16 Jan 2024 07:54:27 +0200 Subject: [PATCH 029/762] Fixed: Filter history by multiple event types --- src/NzbDrone.Core/History/HistoryRepository.cs | 8 +++----- src/Sonarr.Api.V3/History/HistoryController.cs | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 7f3da4064..e51ef5eed 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -124,22 +124,20 @@ namespace NzbDrone.Core.History public PagingSpec GetPaged(PagingSpec pagingSpec, int[] languages, int[] qualities) { - pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery); + pagingSpec.Records = GetPagedRecords(PagedBuilder(languages, qualities), pagingSpec, PagedQuery); var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\""; - pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(EpisodeHistory)), pagingSpec, countTemplate); + pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(languages, qualities).Select(typeof(EpisodeHistory)), pagingSpec, countTemplate); return pagingSpec; } - private SqlBuilder PagedBuilder(PagingSpec pagingSpec, int[] languages, int[] qualities) + private SqlBuilder PagedBuilder(int[] languages, int[] qualities) { var builder = Builder() .Join((h, a) => h.SeriesId == a.Id) .Join((h, a) => h.EpisodeId == a.Id); - AddFilters(builder, pagingSpec); - if (languages is { Length: > 0 }) { builder.Where($"({BuildLanguageWhereClause(languages)})"); diff --git a/src/Sonarr.Api.V3/History/HistoryController.cs b/src/Sonarr.Api.V3/History/HistoryController.cs index 991b2bb21..02eae491a 100644 --- a/src/Sonarr.Api.V3/History/HistoryController.cs +++ b/src/Sonarr.Api.V3/History/HistoryController.cs @@ -62,15 +62,14 @@ namespace Sonarr.Api.V3.History [HttpGet] [Produces("application/json")] - public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, int? eventType, int? episodeId, string downloadId, [FromQuery] int[] seriesIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) + public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, [FromQuery(Name = "eventType")] int[] eventTypes, int? episodeId, string downloadId, [FromQuery] int[] seriesIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - if (eventType.HasValue) + if (eventTypes != null && eventTypes.Any()) { - var filterValue = (EpisodeHistoryEventType)eventType.Value; - pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); + pagingSpec.FilterExpressions.Add(v => eventTypes.Contains((int)v.EventType)); } if (episodeId.HasValue) From 736651324f7b675c22d4f5693be397226cfae846 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Tue, 16 Jan 2024 05:30:39 +0000 Subject: [PATCH 030/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 4c4f2e5f5..f5b873b23 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10324,6 +10324,11 @@ "format": "date-time", "nullable": true }, + "added": { + "type": "string", + "format": "date-time", + "nullable": true + }, "status": { "type": "string", "nullable": true From e1260d504eed9f401535e418fd5fc1fcc3614677 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 13 Jan 2024 17:10:00 -0800 Subject: [PATCH 031/762] Re-enable deploy --- .github/workflows/build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f2bfd793..f901eddc9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -207,13 +207,13 @@ jobs: binary_artifact: ${{ matrix.binary_artifact }} binary_path: ${{ matrix.binary_path }} - # deploy: - # if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} - # needs: [backend, unit_test, unit_test_postgres, integration_test] - # secrets: inherit - # uses: ./.github/workflows/deploy.yml - # with: - # framework: ${{ needs.backend.outputs.framework }} - # branch: ${{ github.ref_name }} - # major_version: ${{ needs.backend.outputs.major_version }} - # version: ${{ needs.backend.outputs.version }} + deploy: + if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} + needs: [backend, unit_test, unit_test_postgres, integration_test] + secrets: inherit + uses: ./.github/workflows/deploy.yml + with: + framework: ${{ needs.backend.outputs.framework }} + branch: ${{ github.ref_name }} + major_version: ${{ needs.backend.outputs.major_version }} + version: ${{ needs.backend.outputs.version }} From 9a7b5bf14e88e9196e23f29579e05fd5157cc8a3 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 16 Jan 2024 05:52:54 +0000 Subject: [PATCH 032/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Blair Noctis Co-authored-by: Dani Talens Co-authored-by: Havok Dan Co-authored-by: Oskari Lavinto Co-authored-by: Petr Vojar Co-authored-by: Weblate Co-authored-by: crayon3shawn Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_TW/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 74 +++++- src/NzbDrone.Core/Localization/Core/cs.json | 4 +- src/NzbDrone.Core/Localization/Core/fi.json | 233 +++++++++++++++--- src/NzbDrone.Core/Localization/Core/fr.json | 1 - .../Localization/Core/pt_BR.json | 5 +- .../Localization/Core/zh_CN.json | 83 ++++++- .../Localization/Core/zh_TW.json | 10 +- 7 files changed, 362 insertions(+), 48 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 085216c6d..0c878ceb3 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -420,5 +420,77 @@ "NotificationsAppriseSettingsConfigurationKeyHelpText": "Clau de configuració per a la solució d'emmagatzematge persistent. Deixeu-lo en blanc si s'utilitzen URL sense estat.", "FormatAgeMinute": "minut", "NotificationsAppriseSettingsStatelessUrlsHelpText": "Un o més URL separats per comes que identifiquen on s'ha d'enviar la notificació. Deixeu-lo en blanc si s'utilitza l'emmagatzematge persistent.", - "CopyUsingHardlinksSeriesHelpText": "Els enllaços durs permeten a {appName} importar torrents compartits a la carpeta de la sèrie sense ocupar espai de disc addicional ni copiar tot el contingut del fitxer. Els enllaços durs només funcionaran si la font i la destinació estan al mateix volum" + "CopyUsingHardlinksSeriesHelpText": "Els enllaços durs permeten a {appName} importar torrents compartits a la carpeta de la sèrie sense ocupar espai de disc addicional ni copiar tot el contingut del fitxer. Els enllaços durs només funcionaran si la font i la destinació estan al mateix volum", + "Events": "Esdeveniments", + "Name": "Nom", + "System": "Sistema", + "DeleteCustomFormat": "Suprimeix el format personalitzat", + "CutoffUnmet": "Tall no assolit", + "Database": "Base de dades", + "DeleteBackup": "Suprimeix la còpia de seguretat", + "Details": "Detalls", + "Indexers": "Indexadors", + "CutoffUnmetLoadError": "S'ha produït un error en carregar els elements de tall no assolits", + "CutoffUnmetNoItems": "No hi ha elements de tall no assolits", + "Daily": "Diari", + "DailyEpisodeFormat": "Format d'episodi diari", + "Dash": "Guió", + "Date": "Data", + "Day": "Dia", + "Debug": "Depuració", + "Dates": "Dates", + "DefaultCase": "Cas per defecte", + "DelayProfileProtocol": "Protocol: {preferredProtocol}", + "Delete": "Suprimeix", + "DeleteDelayProfile": "Suprimeix el perfil de retard", + "DeleteDelayProfileMessageText": "Esteu segur que voleu suprimir aquest perfil de retard?", + "DeleteDownloadClient": "Suprimeix el client de descàrrega", + "DeleteEmptyFolders": "Suprimeix les carpetes buides", + "DeleteEpisodeFile": "Suprimeix el fitxer de l'episodi", + "DeleteEpisodeFileMessage": "Esteu segur que voleu suprimir '{path}'?", + "DeleteEpisodeFromDisk": "Suprimeix l'episodi del disc", + "DeleteEpisodesFiles": "Suprimeix els {episodeFileCount} fitxers d'episodi", + "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", + "DelayProfile": "Perfil de retard", + "DeleteImportListExclusionMessageText": "Esteu segur que voleu suprimir aquesta exclusió de la llista d'importació?", + "DeleteReleaseProfile": "Suprimeix el perfil de llançament", + "Edit": "Edita", + "History": "Història", + "Negated": "Negat", + "Tags": "Etiquetes", + "Unmonitored": "No monitorat", + "Quality": "Qualitat", + "Wanted": "Demanat", + "Yes": "Si", + "Tasks": "Tasques", + "Disabled": "Desactivat", + "Languages": "Idiomes", + "Settings": "Configuració", + "General": "General", + "Missing": "Absents", + "Queue": "Cua", + "Metadata": "Metadada", + "DelayProfilesLoadError": "No es poden carregar els perfils de retard", + "DeleteIndexer": "Suprimeix l'indexador", + "DeleteNotification": "Suprimeix la notificació", + "DeleteQualityProfile": "Suprimeix el perfil de qualitat", + "DeleteReleaseProfileMessageText": "Esteu segur que voleu suprimir el perfil de llançament '{name}'?", + "Profiles": "Perfils", + "DeleteImportListExclusion": "Suprimeix l'exclusió de la llista d'importació", + "DailyEpisodeTypeDescription": "Episodis publicats diàriament o amb menys freqüència que utilitzen any-mes-dia (2023-08-04)", + "DailyEpisodeTypeFormat": "Data ({format})", + "DefaultDelayProfileSeries": "Aquest és el perfil predeterminat. S'aplica a totes les sèries que no tenen un perfil explícit.", + "DelayProfileSeriesTagsHelpText": "S'aplica a sèries amb almenys una etiqueta coincident", + "DeleteEmptySeriesFoldersHelpText": "Suprimeix les carpetes de sèries buides durant l'exploració del disc i quan s'esborren els fitxers de sèries", + "DelayProfiles": "Perfils de retard", + "Remove": "Elimina", + "Required": "Obligatori", + "Replace": "Substitueix", + "Series": "Sèries", + "Updates": "Actualitzacions", + "No": "No", + "Result": "Resultat" } diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 055a94fc3..3793855d8 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -318,5 +318,7 @@ "DeleteRootFolderMessageText": "Opravdu chcete smazat kořenový adresář s cestou '{path}'?", "EditSelectedDownloadClients": "Upravit vybrané klienty pro stahování", "EditSelectedImportLists": "Upravit vybrané seznamy k importu", - "FormatDateTime": "{formattedDate} {formattedTime}" + "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}." } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index efa40d251..b974bba3f 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -15,7 +15,7 @@ "AgeWhenGrabbed": "Ikä (kaappaushetkellä)", "GrabId": "Kaappauksen tunniste", "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", - "BrowserReloadRequired": "Selaimen sivupäivitys vaaditaan", + "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.", "RemotePathMappingHostHelpText": "Sama osoite, joka on määritty etälataustyökalulle.", "AudioLanguages": "Äänen kielet", @@ -38,7 +38,7 @@ "SelectLanguageModalTitle": "{modalTitle} - Valitse kieli", "SelectLanguage": "Valitse kieli", "SelectLanguages": "Valitse kielet", - "ShowRelativeDatesHelpText": "Näytä suhteutetut (tänään/eilen/yms.) absoluuttisten sijaan", + "ShowRelativeDatesHelpText": "Korvaa absoluuttiset päiväykset suhteellisilla päiväyksillä (tänään/eilen/yms.).", "SetPermissions": "Määritä käyttöoikeudet", "Style": "Ulkoasu", "SubtitleLanguages": "Tekstityskielet", @@ -46,7 +46,7 @@ "UiLanguage": "Käyttöliittymän kieli", "UiLanguageHelpText": "{appName}in käyttöliittymän kieli.", "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 skriptillä.", - "AddListExclusionSeriesHelpText": "Estä sarjan lisääminen {appName}iin listoilta", + "AddListExclusionSeriesHelpText": "Estä sarjan poiminta {appName}iin listoilta.", "AppUpdated": "{appName} on päivitetty", "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.", @@ -145,8 +145,8 @@ "CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?", "Clear": "Tyhjennä", "CollectionsLoadError": "Virhe ladattaessa kokoelmia", - "CreateEmptySeriesFolders": "Luo tyhjät sarjakansiot", - "CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.", + "CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot", + "CreateEmptySeriesFoldersHelpText": "Luo puuttuvien sarjojen kansiot kirjastotarkistusten yhteydessä.", "CustomFormatsLoadError": "Virhe ladattaessa mukautettuja muotoja", "Debug": "Vianselvitys", "DeleteDelayProfileMessageText": "Haluatko varmasti poistaa viiveprofiilin?", @@ -188,7 +188,7 @@ "IndexerValidationQuerySeasonEpisodesNotSupported": "Tietolähde ei tue nykyistä kyselyä. Tarkista tukeeko se kategorioita ja kausien/jaksojen etsintää.", "InfoUrl": "Tietojen URL", "InstanceName": "Instanssin nimi", - "InteractiveImportLoadError": "Virhe ladattaessa manuaalituonnin kohteita.", + "InteractiveImportLoadError": "Virhe ladattaessa manuaalisen tuonnin kohteita.", "InteractiveSearchResultsSeriesFailedErrorMessage": "Haku epäonnistui: {message}. Elokuvan tietojen päivittäminen ja kaikkien tarvittavien tietojen olemassaolon varmistus voi auttaa ennen uutta hakuyritystä.", "Large": "Suuri", "LibraryImportSeriesHeader": "Tuo sinulla jo olevat sarjat", @@ -223,7 +223,7 @@ "MissingEpisodes": "Puuttuvia jaksoja", "MonitorNewSeasons": "Valvo uusia kausia", "MonitorLastSeasonDescription": "Valvo kaikkia viimeisen kauden jaksoja.", - "MonitorNewSeasonsHelpText": "Mitä uusia kausia tulee valvoa automaattisesti.", + "MonitorNewSeasonsHelpText": "Uusien kausien automaattivalvonnan käytäntö.", "MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjan tiedostot kohteeseen \"{destinationRootFolder}\"?", "MoreDetails": "Lisätietoja", "MonitoredStatus": "Valvottu/tila", @@ -313,7 +313,7 @@ "ShowSeriesTitleHelpText": "Näytä sarjan nimi julisteen alla.", "SizeOnDisk": "Koko levyllä", "Sort": "Järjestys", - "SourceTitle": "Lähteen nimi", + "SourceTitle": "Lähteen nimike", "SourceRelativePath": "Lähteen suhteellinen sijainti", "Special": "Erikoisjakso", "Source": "Lähdekoodi", @@ -352,7 +352,7 @@ "Error": "Virhe", "Formats": "Muodot", "FailedToFetchUpdates": "Päivitysten nouto epäonnistui", - "HasMissingSeason": "Kausi puuttuu", + "HasMissingSeason": "Kausi(a) puuttuu", "ImportLists": "Tuontilistat", "IndexerStatusUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi: {indexerNames}", "Info": "Informatiivinen", @@ -378,7 +378,7 @@ "RemotePathMappingRemotePathHelpText": "Lataustyökalun käyttämän hakemiston juurisijainti", "Restart": "Käynnistä uudelleen", "SizeLimit": "Kokorajoitus", - "TestAllIndexers": "Testaa tietolähteet", + "TestAllIndexers": "Tietolähteiden testaus", "UnableToLoadBackups": "Varmuuskopioiden lataus epäonnistui", "UseSeasonFolderHelpText": "Lajittele jaksot tuotantokausikohtaisiin kansioihin.", "ApplyTagsHelpTextRemove": "- \"Poista\" tyhjentää syötetyt tunnisteet", @@ -425,7 +425,7 @@ "DownloadClientStatusAllClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä", "DownloadClientUTorrentTorrentStateError": "uTorrent ilmoittaa virheestä", "DownloadClientValidationUnableToConnectDetail": "Tarkista osoite ja portti.", - "DeleteEpisodesFilesHelpText": "Poista jaksotiedostot ja sarjan kansio", + "DeleteEpisodesFilesHelpText": "Poista jaksotiedostot ja sarjan kansio.", "EditCustomFormat": "Muokkaa mukautettua muotoa", "EditConditionImplementation": "Muokataan ehtoa - {implementationName}", "EditDownloadClientImplementation": "Muokataan lataustyökalua - {implementationName}", @@ -460,10 +460,10 @@ "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.", "IndexerStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi", "LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä lataustyökalun latausten tuontiin. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.", - "LibraryImportTips": "Joitakin vinkkejä, joiden avulla homma sujuu:", + "LibraryImportTips": "Muutama vinkki, joilla homma sujuu:", "ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein", "ListExclusionsLoadError": "Virhe ladattaessa listapoikkeuksia", - "ManualImportItemsLoadError": "Virhe ladattaessa manuaalituonnin kohteita.", + "ManualImportItemsLoadError": "Virhe ladattaessa manuaalisen tuonnin kohteita.", "MediaManagementSettingsSummary": "Tiedostojen nimeämisen, hallinnan ja juurikansioiden asetukset.", "Message": "Viesti", "MetadataSettings": "Metatietoasetukset", @@ -486,11 +486,11 @@ "Name": "Nimi", "NamingSettings": "Nimeämisasetukset", "NoEpisodeHistory": "Jaksohistoriaa ei ole", - "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, joiden yhteiskoko on {size}", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, joiden yhteiskoko on {size}.", "DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?", - "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö", + "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.", "DeleteSeriesModalHeader": "Poistetaan - {title}", - "DeletedSeriesDescription": "Sarja on poistettu TheTVDB.comista", + "DeletedSeriesDescription": "Sarja on poistettu TheTVDB.comista.", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", "NotificationStatusSingleClientHealthCheckMessage": "Ilmoitukset eivät ole ongelmien vuoksi käytettävissä: {notificationNames}", "NotificationsLoadError": "Virhe ladattaessa ilmoituksia", @@ -528,7 +528,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 uudelleenkäynnistyksen", + "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen.", "Runtime": "Kesto", "Season": "Kausi", "SeasonFolder": "Kausikohtaiset kansiot", @@ -536,13 +536,13 @@ "SeasonFinale": "Kauden päätösjakso", "SeasonDetails": "Kauden tiedot", "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} jaksoa on ladattu", - "SeasonInformation": "Kausitiedot", + "SeasonInformation": "Kauden tiedot", "SeasonPassTruncated": "Vain uusimmat 25 kautta näytetään. Muut ovat nähtävissä sarjan tiedoista.", "SeasonPremieresOnly": "Vain kausien pilottijaksot", "SeasonPremiere": "Kauden pilottijakso", "SelectDownloadClientModalTitle": "{modalTitle} - Valitse lataustyökalu", "SelectEpisodesModalTitle": "{modalTitle} - Valitse jakso(t)", - "SelectEpisodes": "Valitse jakso(ja)", + "SelectEpisodes": "Valitse jakso(t)", "Series": "Sarjat", "SeriesCannotBeFound": "Valitettavasti etsimääsi sarjaa ei löydy.", "SeriesDetailsNoEpisodeFiles": "Jaksotiedostoja ei ole", @@ -614,7 +614,7 @@ "EditSeriesModalHeader": "Muokataan - {title}", "EnableInteractiveSearch": "Käytä manuaalihakua", "EnableRssHelpText": "Käytetään {appName}in etsiessä julkaisuja ajoitetusti RSS-synkronoinnilla.", - "EnableSslHelpText": "Käyttöönotto vaatii uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", + "EnableSslHelpText": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeInfo": "Jakson tiedot", "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja", @@ -701,7 +701,7 @@ "EditSelectedIndexers": "Muokkaa valittuja sisältölähteitä", "ErrorRestoringBackup": "Virhe palautettaessa varmuuskopiota", "ErrorLoadingItem": "Virhe ladattaessa tätä kohdetta", - "FileBrowserPlaceholderText": "Aloita sijainnin kirjoittaminen tai valitse se alta", + "FileBrowserPlaceholderText": "Kirjoita sijainti tai selaa se alta", "FeatureRequests": "Kehitysehdotukset", "IndexerPriority": "Tietolähteiden painotus", "IndexerOptionsLoadError": "Virhe ladattaessa tietolähdeasetuksia", @@ -712,8 +712,8 @@ "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", - "SelectSeries": "Valitse sarjoja", - "SelectSeasonModalTitle": "{modalTitle} - Valitse tuotantokausi", + "SelectSeries": "Valitse sarja", + "SelectSeasonModalTitle": "{modalTitle} - Valitse kausi", "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", "TimeLeft": "Aikaa jäljellä", "Time": "Aika", @@ -737,7 +737,7 @@ "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kartoitukset ja säiliön tallennusmedian asetukset.", "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjan ja jakson tiedot toimittaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe).", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot toimittaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe)", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakorajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", "Yesterday": "Eilen", @@ -862,7 +862,7 @@ "DeletedReasonEpisodeMissingFromDisk": "{appName} ei löytänyt tiedostoa levyltä, joten sen kytkös tietokonnassa olevaan jaksoon purettiin.", "Details": "Tiedot", "DownloadClient": "Lataustyökalu", - "DisabledForLocalAddresses": "Ei käytetä paikallisille osoitteille", + "DisabledForLocalAddresses": "Ei käytössä paikallisille osoitteille", "DownloadClientDelugeValidationLabelPluginFailure": "Tunnisteen määritys epäonnistui", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorioiden käyttö edellyttää, että lataustyökalun \"{clientName}\" Label-lisäosa on käytössä.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisää median ominaisuuksia tunnisteina. Vihjeet ovat esimerkkejä.", @@ -894,7 +894,7 @@ "EndedSeriesDescription": "Uusia jaksoja tai kausia ei ole odotettavissa.", "EditSelectedSeries": "Muokkaa valittuja sarjoja", "EpisodeHistoryLoadError": "Virhe ladattaessa jaksohistoriaa", - "Ended": "Päättyi", + "Ended": "Päättynyt", "ExistingSeries": "Olemassa olevat sarjat", "FreeSpace": "Vapaa tila", "FirstDayOfWeek": "Viikon ensimmäinen päivä", @@ -928,16 +928,16 @@ "IndexerValidationUnableToConnect": "Tietolähdettä ei tavoiteta: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", "MassSearchCancelWarning": "Tätä ei ole mahdollista pysäyttää kuin käynnistämällä {appName}ia uudelleen tai poistamalla kaikki tietolähteet käytöstä.", - "MetadataSourceSettingsSeriesSummary": "Tietoja siitä, mistä {appName} hankkii sarjojen ja jaksojen tiedot.", + "MetadataSourceSettingsSeriesSummary": "Tietoja siitä, mistä {appName} saa sarjojen ja jaksojen tiedot.", "NoHistory": "Historiaa ei ole", "MustContainHelpText": "Julkaisun on sisällettävä ainakin yksi näistä termeistä (kirjainkoolla ei ole merkitystä).", - "NoEpisodesFoundForSelectedSeason": "valitulle kaidelle ei löytynyt jaksoja", + "NoEpisodesFoundForSelectedSeason": "Valitulle tuotantokaudelle ei löytynyt jaksoja.", "MonitorFutureEpisodesDescription": "Valvo jaksoja, joita ei ole vielä esitetty.", "MissingNoItems": "Ei puuttuvia kohteita", "Mode": "Tila", "NextExecution": "Seuraava suoritus", - "PreviewRename": "Esikatsele nimeämistä", - "NoSeasons": "Tuotantokausia ei ole", + "PreviewRename": "Nimeämisen esikatselu", + "NoSeasons": "Kausia ei ole", "NoMonitoredEpisodesSeason": "Kaudesta ei valvota jaksoja", "NoMonitoredEpisodes": "Sarjasta ei valvota jaksoja", "NoSeriesFoundImportOrAdd": "Sarjoja ei löytynyt. Aloita tuomalla olemassa olevia sarjoja tai lisäämällä uusia.", @@ -958,7 +958,7 @@ "Calendar": "Kalenteri", "AutoRedownloadFailed": "Uudelleenlataus epäonnistui", "ChooseImportMode": "Valitse tuontitila", - "ChooseAnotherFolder": "Valitse eri kansio", + "ChooseAnotherFolder": "Valitse muu kansio", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Lataustyökaluja ei ole käytettävissä", "Downloaded": "Ladattu", "Global": "Yleiset", @@ -970,12 +970,11 @@ "Location": "Sijainti", "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.", "ListTagsHelpText": "Tunnisteet, joilla tältä tuontilistalta lisätyt kohteet merkitään.", - "ManageEpisodes": "Hallitse jaksoja", + "ManageEpisodes": "Jaksojen hallinta", "ManageEpisodesSeason": "Hallitse tuotantokauden jaksotiedostoja", "ManageIndexers": "Hallitse tietolähteitä", "LocalPath": "Paikallinen sijainti", "NoChanges": "Muutoksia ei ole", - "PriorityHelpText": "Määritä useiden lataustyökalujen painotus. Tasaveroiset lataajat erotetaan Round-Robin-tekniikalla.", "PosterSize": "Julisteiden koko", "PosterOptions": "Julistenäkymän asetukset", "PreviousAiringDate": "Edellinen esitys: {date}", @@ -1015,7 +1014,7 @@ "ResetQualityDefinitions": "Palauta laatumääritykset", "RestartRequiredToApplyChanges": "{appName} on käynnistettävä muutosten käyttöönottamiseksi uudelleen. Haluatko tehdä sen nyt?", "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", - "Seasons": "Tuotantokaudet", + "Seasons": "Kaudet", "SearchAll": "Etsi kaikkia", "SearchByTvdbId": "Voit etsiä myös sarjan TVDB ID:llä, esim. \"tvdb:71663\".", "RootFoldersLoadError": "Virhe ladattaessa juurikansioita", @@ -1159,7 +1158,7 @@ "Directory": "Kansio", "PendingChangesDiscardChanges": "Hylkää muutokset ja poistu", "RecyclingBinHelpText": "Poistetut tiedostot siirretään tänne pysyvän poiston sijaan.", - "ReleaseSceneIndicatorAssumingTvdb": "Oletetuksena TVDB-numerointi.", + "ReleaseSceneIndicatorAssumingTvdb": "Oletuksena TVDB-numerointi.", "ReleaseSceneIndicatorMappedNotRequested": "Valittu jakso ei sisältynyt tähän hakuun.", "ReplaceWithSpaceDash": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki\"", "ReplaceWithSpaceDashSpace": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki välilyönti\"", @@ -1182,17 +1181,17 @@ "FormatAgeHour": "tunti", "FormatAgeMinute": "minuutti", "InteractiveImportNoSeries": "Jokaisen valitun tiedoston sarja on määritettävä.", - "InteractiveImportNoSeason": "Jokaiselle valitulle tiedostolle on määritettävä kausi.", + "InteractiveImportNoSeason": "Jokaiselle valitulle tiedostolle on määritettävä tuotantokausi.", "InteractiveSearchSeason": "Etsi kauden kaikkia jaksoja manuaalihaulla", "Space": "Välilyönti", "DownloadPropersAndRepacksHelpTextCustomFormat": "Käytä \"Älä suosi\" -valintaa suosiaksesi mukautettujen muotojen pisteytystä Proper- ja Repack-merkintöjä enemmän.", - "AuthenticationRequiredPasswordHelpTextWarning": "Syötä salasana", + "AuthenticationRequiredPasswordHelpTextWarning": "Syötä uusi salasana", "AuthenticationMethod": "Tunnistautumistapa", "AuthenticationMethodHelpTextWarning": "Valitse sopiva tunnistautumistapa", "Dash": "Yhdysmerkki", "MaximumSingleEpisodeAge": "Yksittäisen jakson enimmäisikä", "MinimumAgeHelpText": "Vain Usenet: NZB:n vähimmäisikä minuutteina, ennen niiden kaappausta. Tämän avulla uusille julkaisuille voidaan antaa aikaa levitä Usenet-palveluntarjoajalle.", - "MultiEpisodeStyle": "Useden jaksojen tyyli", + "MultiEpisodeStyle": "Useiden jaksojen tyyli", "Never": "Ei koskaan", "OrganizeNamingPattern": "Nimeämiskaava: \"{episodeFormat}\"", "PendingChangesMessage": "Olet tehnyt muutoksia, joita ei ole vielä tallennettu. Haluatko varmasti poistua sivulta?", @@ -1245,7 +1244,7 @@ "MultiEpisodeInvalidFormat": "Useita jaksoja: virheellinen kaava", "AutoRedownloadFailedFromInteractiveSearch": "Uudelleenlataus manuaalihausta epäonnistui", "Blocklist": "Estolista", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Hae automaattisesti ja pyri lataamaan eri julkaisu vaikka epäonnistunut julkaisu oli kaapattu manuaalihausta.", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi automaattisesti ja pyri lataamaan eri julkaisu vaikka epäonnistunut julkaisu oli kaapattu manuaalihausta.", "StandardEpisodeFormat": "Vakiojaksojen kaava", "SceneNumberNotVerified": "Kohtausnumeroa ei ole vielä vahvistettu", "Scene": "Kohtaus", @@ -1316,5 +1315,159 @@ "Implementation": "Toteutus", "MediaManagement": "Median hallinta", "Ok": "Ok", - "General": "Yleiset" + "General": "Yleiset", + "Folders": "Kansiot", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteissä käytettävät tietolähteet eivät ole käytettävissä hiljattaisten virheiden vuoksi", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Haussa käytettävät tietolähteet eivät ole käytettävissä hiljattaisten virheiden vuoksi", + "IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.", + "IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika", + "KeyboardShortcutsSaveSettings": "Tallenna asetukset", + "KeyboardShortcutsOpenModal": "Avaa tämä ruutu", + "ManageImportLists": "Tuontilistojen hallinta", + "ManageLists": "Listojen hallunta", + "MatchedToSeason": "Täsmätty kauteen", + "Min": "Vähimmäis", + "MultiSeason": "Useita kausia", + "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth -salasana.", + "NotificationsNtfySettingsAccessTokenHelpText": "Valinnainen tunnistepohjainen todennus. Ensisijainen ennen käyttäjätunnusta ja salasanaa.", + "NotificationsPlexSettingsAuthToken": "Todennustunniste", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Tunnistaud Plex.tv-palvelussa", + "Existing": "On jo olemassa", + "NotificationsTelegramSettingsChatId": "Keskustelu ID", + "NotificationsSlackSettingsUsernameHelpText": "Slack-julkaisulle käytettävä käyttäjätunnus", + "NotificationsTelegramSettingsSendSilently": "Lähetä äänettömästi", + "NotificationsTelegramSettingsTopicId": "Aiheen ID", + "ProcessingFolders": "Käsittelykansiot", + "Preferred": "Haluttu", + "SslCertPasswordHelpText": "Pfx-tiedoston salasana", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Vain luku", + "TestAllClients": "Lataustyökalujen testaus", + "TorrentDelayHelpText": "Minuuttiviive, joka odotetaan ennen julkaisun Torrent-kaappausta.", + "EditSelectedImportLists": "Muokkaa valittuja tuontilistoja", + "EnableAutomaticAdd": "Käytä automaattilisäystä", + "ExistingTag": "Tunniste on jo olemassa", + "Exception": "Poikkeus", + "Images": "Kuvat", + "SaveSettings": "Tallenna asetukset", + "EditReleaseProfile": "Muokkaa julkaisuprofiilia", + "EditRestriction": "Muokkaa rajoitusta", + "EnableMetadataHelpText": "Luo metatietotiedostot tälle metatietotyypille.", + "IndexerSettingsAnimeCategoriesHelpText": "Pudotusvalikko. Poista anime käytöstä jättämällä tyhjäksi.", + "IndexerValidationCloudFlareCaptchaExpired": "CloudFlaren CAPTCHA-tunniste on vanhentunut. Päivitä se.", + "KeyboardShortcutsConfirmModal": "Vastaa vahvistuskysymykseen hyväksyvästi", + "ManualImport": "Manuaalinen tuonti", + "MediaManagementSettings": "Medianhallinnan asetukset", + "Mechanism": "Mekanismi", + "Negate": "Kiellä", + "OneMinute": "1 minuutti", + "PostImportCategory": "Tuonnin jälkeinen kategoria", + "SelectSeason": "Valitse kausi", + "SelectReleaseGroup": "Aseta julkaisuryhmä", + "SendAnonymousUsageData": "Lähetä nimettömiä käyttötietoja", + "TorrentBlackholeSaveMagnetFiles": "Tallenna magnet-tiedostot", + "NotificationTriggersHelpText": "Valitse tämän ilmoituksen laukaisevat tapahtumat.", + "OneSeason": "1 kausi", + "OpenBrowserOnStart": "Avaa selain käynnistettäessä", + "Profiles": "Profiilit", + "ProxyBypassFilterHelpText": "Käytä aliverkkotunnusten erottimena pilkkua (,) ja jokerimerkkinä tähteä ja pistettä (*.). Esimerkkejä: www.esimerkki.fi,*.esimerkki.fi.", + "ProxyBadRequestHealthCheckMessage": "Välityspalvelintesti epäonnistui. Tilakoodi {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Välityspalvelintesti epäonnistui: {url}", + "Reason": "Syy", + "Security": "Suojaus", + "SslCertPassword": "SSL-varmenteen salasana", + "StandardEpisodeTypeDescription": "Numerointikaavalla SxxEyy julkaistut jaksot.", + "TorrentBlackholeSaveMagnetFilesExtension": "Tallennettujen magnet-tiedostojen pääte", + "NoEventsFound": "Tapahtumia ei löytynyt", + "ImportListExclusions": "Tuontilistojen poikkeukset", + "Logging": "Lokikirjaus", + "PartialSeason": "Osittainen kausi", + "ReleaseGroup": "Julkaisuryhmä", + "TestAll": "Kaikkien testaus", + "UrlBase": "URL-perusta", + "UsenetDelayHelpText": "Minuuttiviive, joka odotetaan ennen julkaisun Usenet-kaappausta.", + "EpisodeFileMissingTooltip": "Jaksotiedosto puuttuu", + "EpisodeNaming": "Jaksojen nimeäminen", + "NotificationsAppriseSettingsUsernameHelpText": "HTTP Basic Auth -käyttäjätunnus", + "NotificationsDiscordSettingsUsernameHelpText": "Julkaisuun käytettävä käyttäjätunnus. Oletusarvo on Discordin webhook-oletus.", + "NotificationsNtfySettingsPasswordHelpText": "Valinnainen salasana", + "NotificationsNtfySettingsUsernameHelpText": "Valinnainen käyttäjätunnus", + "NotificationsPlexValidationNoTvLibraryFound": "Ainakin yksi televisiokirjasto tarvitaan.", + "NotificationsTelegramSettingsBotToken": "Bottitunniste", + "EnableSsl": "Käytä RSS-yhteyttä", + "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", + "Folder": "Kansio", + "Links": "Linkit", + "Max": "Enimmäis", + "MaximumLimits": "Enimmäisrajoitukset", + "MinimumLimits": "Vähimmäisrajoitukset", + "NoDelay": "Ei viivettä", + "NotSeasonPack": "Ei ole kausikooste", + "OpenBrowserOnStartHelpText": " Avaa {appName}in verkkokäyttöliittymä verkkoselaimeen käynnistyksen yhteydessä.", + "Password": "Salasana", + "ReleaseProfileIndexerHelpText": "Määritä mitä tietolähdettä profiili koskee.", + "SeasonCount": "Kausien määrä", + "SeasonNumberToken": "Kausi {seasonNumber}", + "SeasonNumber": "Kauden numero", + "SeasonPack": "Kausikooste", + "SelectDropdown": "Valitse...", + "Socks4": "SOCKS4", + "Upcoming": "Tulossa", + "Username": "Käyttäjätunnus", + "UseProxy": "Käytä välityspalvelinta", + "UsenetDelay": "Usenet-viive", + "UsenetDelayTime": "Usenet-viive: {usenetDelay}", + "VideoCodec": "Videokoodekki", + "MinimumAge": "Vähimmäisikä", + "ReleaseGroups": "Julkaisuryhmät", + "DownloadClientQbittorrentTorrentStateStalled": "Lataus on jäätynyt yhteyksien puuttumksen vuoksi.", + "EnableProfileHelpText": "Käytä julkaisuprofiilia merkitsemällä tämä.", + "EnableRss": "Käytä RSS-syötettä", + "EpisodeFileDeletedTooltip": "Jaksotiedosto poistettiin", + "EpisodeHasNotAired": "Jaksoa ei ole esitetty", + "EpisodeMissingAbsoluteNumber": "Jaksolle ei ole absoluuttista jaksonumeroa", + "EpisodeNumbers": "Jaksojen numerointi", + "Retention": "Säilytys", + "ShortDateFormat": "Lyhyen päiväyksen esitys", + "Unknown": "Tuntematon", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi", + "OrganizeSelectedSeriesModalAlert": "Vinkki: Esikatsellaksesi nimeämistä, paina \"Peruuta\" ja valitse jokin sarjanimike ja paina tätä kuvaketta:", + "Socks5": "SOCKS5 (TOR-tuki)", + "EditMetadata": "Muokkaa metatietoa {metadataType}", + "EnableProfile": "Käytä profiilia", + "TorrentDelay": "Torrent-viive", + "FilterIs": "on", + "PreviewRenameSeason": "Esikatsele tämän kauden nimeämistä", + "InstallLatest": "Asenna uusin", + "ReleaseProfiles": "Julkaisuprofiilit", + "SetReleaseGroup": "Aseta julkaisuryhmä", + "NotificationsTraktSettingsRefreshToken": "Päivitä tunniste", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Huomioi, että kategorian vaihdon jälkeen alaryhmiin on lisättävä pakolliset/rajoitetut aliryhmäsäännöt vieraskielisten julkaisujen välttämiseksi.", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {indexerNames}", + "FullSeason": "Koko kausi", + "ShowRelativeDates": "Käytä suhteellisia päiväyksiä", + "ProxyPasswordHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", + "TestAllLists": "Kaikkien listojen testaus", + "DownloadClientValidationUnknownException": "Tuntematon poikkeus: {exception}", + "EnableHelpText": "Luo metatietotiedostot tälle metatietotyypille.", + "DownloadFailedEpisodeTooltip": "Jakson lataus epäonnistui", + "DownloadIgnoredEpisodeTooltip": "Jakson latausta ei huomioitu", + "ExpandAll": "Laajenna kaikki", + "Enable": "Käytä", + "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli kohtaat ongelmia näiden viestien tulkinnassa, tavoitat tukemme alla olevilla linkkeillä.", + "MegabytesPerMinute": "Megatavua minuutissa", + "MustContain": "Täytyy sisältää", + "NoLinks": "Linkkejä ei ole", + "Proxy": "Välityspalvelin", + "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", + "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai Trakt-listoilta, ja listapoikkeusten hallinta.", + "LongDateFormat": "Pitkän päiväyksen esitys", + "UnknownEventTooltip": "Tuntematon tapahtuma", + "UnknownDownloadState": "Tuntematon lataustila: {state}" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index ea581c05f..c5b4d17fb 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1143,7 +1143,6 @@ "PreviewRenameSeason": "Aperçu Renommer pour cette saison", "PreviousAiring": "Diffusion précédente", "PreviousAiringDate": "Diffusion précédente : {date}", - "PriorityHelpText": "Donnez la priorité à plusieurs clients de téléchargement. Round-Robin est utilisé pour les clients ayant la même priorité.", "ProcessingFolders": "Dossiers de traitement", "Protocol": "Protocole", "ProxyType": "Type de mandataire", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 092947518..474e6f0d3 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -717,7 +717,6 @@ "PreferredSize": "Tamanho Preferido", "PrefixedRange": "Faixa Prefixada", "Presets": "Definições", - "PriorityHelpText": "Priorize vários clientes de download. Round-Robin é usado para clientes com a mesma prioridade.", "PrioritySettings": "Prioridade: {priority}", "ProfilesSettingsSummary": "Qualidade, Atraso do Idioma e Perfis de lançamentos", "Proxy": "Proxy", @@ -1873,5 +1872,7 @@ "NotificationsSignalSettingsSenderNumberHelpText": "Número de telefone do registro do remetente no signal-api", "NotificationsSlackSettingsChannelHelpText": "Substitui o canal padrão para o webhook de entrada (#other-channel)", "NotificationsSynologySettingsUpdateLibraryHelpText": "Chame synoindex no localhost para atualizar um arquivo de biblioteca", - "NotificationsTelegramSettingsSendSilentlyHelpText": "Envia a mensagem silenciosamente. Os usuários receberão uma notificação sem som" + "NotificationsTelegramSettingsSendSilentlyHelpText": "Envia a mensagem silenciosamente. Os usuários receberão uma notificação sem som", + "EpisodeFileMissingTooltip": "Arquivo do episódio ausente", + "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 71d8590e5..344a284be 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -891,7 +891,6 @@ "Other": "其他", "PreferredProtocol": "首选协议", "Posters": "海报", - "PriorityHelpText": "优先考虑多个下载客户端,循环查询用于具有相同优先级的客户端。", "ProcessingFolders": "处理文件夹中", "QualitiesLoadError": "无法加载质量", "QualitiesHelpText": "列表中的质量排序越高优先级也越高。同组内的质量优先级相同。质量只有选中才标记为追踪", @@ -1710,5 +1709,85 @@ "NotificationsDiscordSettingsAvatar": "头像", "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth 密码", "NotificationsAppriseSettingsStatelessUrlsHelpText": "用逗号分隔的一个或多个URL,以标识应将通知发送到何处。如果使用持久化存储,则留空。", - "NotificationsEmailSettingsName": "邮箱" + "NotificationsEmailSettingsName": "邮箱", + "EpisodeFileMissingTooltip": "分集文件缺失", + "NotificationsAppriseSettingsTagsHelpText": "可选地,只对这些标签发送通知。", + "NotificationsCustomScriptSettingsArguments": "参数", + "NotificationsCustomScriptSettingsArgumentsHelpText": "传给脚本的参数", + "NotificationsCustomScriptSettingsProviderMessage": "测试中会将 EventType 设为 {eventTypeTest} 后执行脚本,请确保其能正确处理。", + "NotificationsDiscordSettingsAuthor": "作者", + "NotificationsDiscordSettingsAuthorHelpText": "覆盖此通知中出现的作者,留空则为实例名称。", + "NotificationsDiscordSettingsAvatarHelpText": "更改此集成的消息所使用的头像", + "NotificationsDiscordSettingsUsernameHelpText": "发送消息所用的用户名,默认使用 Discord webhook 的缺省值", + "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord 频道 webhook URL", + "NotificationsEmailSettingsCcAddressHelpText": "电子邮件的 Cc 对象,以逗号分开", + "NotificationsEmailSettingsCcAddress": "Cc 地址", + "NotificationsEmailSettingsBccAddressHelpText": "电子邮件的 Bcc 对象,以逗号分开", + "NotificationsEmailSettingsBccAddress": "Bcc 地址", + "NotificationsEmailSettingsFromAddress": "发件地址", + "NotificationsEmailSettingsRecipientAddress": "收件地址", + "NotificationsEmailSettingsRecipientAddressHelpText": "逗号分隔的收件地址", + "NotificationsEmailSettingsRequireEncryption": "要求加密", + "NotificationsEmailSettingsRequireEncryptionHelpText": "要求 SSL(仅 465 端口)或 STARTTLS(其他任意端口)", + "NotificationsEmailSettingsServer": "服务器", + "NotificationsEmailSettingsServerHelpText": "邮件服务器的主机名或 IP", + "NotificationsEmbySettingsSendNotifications": "发送通知", + "NotificationsEmbySettingsSendNotificationsHelpText": "由 MediaBrowser 向配置的提供方发送通知", + "NotificationsEmbySettingsUpdateLibraryHelpText": "在导入、重命名、删除时更新库?", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "在消息中包括剧集海报", + "NotificationsGotifySettingIncludeSeriesPoster": "包括剧集海报", + "NotificationsGotifySettingsAppTokenHelpText": "Gotify 生成的应用凭据", + "NotificationsGotifySettingsPriorityHelpText": "消息优先级", + "NotificationsGotifySettingsServer": "Gotify 服务器", + "NotificationsGotifySettingsServerHelpText": "Gotify 服务器 URL,包括 http(s):// 和端口(如果有)", + "NotificationsJoinSettingsApiKeyHelpText": "您 Join 账号设置中的 API Key(点击 Join API 按钮)。", + "NotificationsJoinSettingsDeviceIds": "设备 ID", + "NotificationsJoinSettingsDeviceNames": "设备名称", + "NotificationsJoinSettingsDeviceNamesHelpText": "通知要发给的设备的全名或其一部分,留空则发给所有设备。", + "NotificationsJoinSettingsNotificationPriority": "通知优先级", + "NotificationsJoinValidationInvalidDeviceId": "设备 ID 似乎无效。", + "NotificationsKodiSettingAlwaysUpdate": "总是更新", + "NotificationsKodiSettingAlwaysUpdateHelpText": "即使有视频正在播放也更新资源库?", + "NotificationsKodiSettingsCleanLibrary": "清理资源库", + "NotificationsKodiSettingsCleanLibraryHelpText": "更新后清理资源库", + "NotificationsKodiSettingsDisplayTime": "显示时间", + "NotificationsKodiSettingsDisplayTimeHelpText": "通知显示时长(秒)", + "NotificationsKodiSettingsGuiNotification": "图形界面通知", + "NotificationsKodiSettingsUpdateLibraryHelpText": "导入和重命名时更新资源库?", + "NotificationsMailgunSettingsApiKeyHelpText": "Mailgun 生成的 API Key", + "NotificationsMailgunSettingsSenderDomain": "发送域名", + "NotificationsMailgunSettingsUseEuEndpoint": "使用欧洲(EU)端点", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "启用则使用 Mailgun 的欧洲(EU)端点", + "NotificationsNotifiarrSettingsApiKeyHelpText": "个人资料中的 API Key", + "NotificationsNtfySettingsAccessToken": "访问令牌", + "NotificationsNtfySettingsAccessTokenHelpText": "可以选择令牌验证,并覆盖用户名、密码验证", + "NotificationsNtfySettingsClickUrl": "通知链接", + "NotificationsNtfySettingsClickUrlHelpText": "点击通知时打开的链接,可选", + "NotificationsNtfySettingsServerUrl": "服务器 URL", + "NotificationsNtfySettingsPasswordHelpText": "密码,可选", + "NotificationsPushBulletSettingSenderIdHelpText": "发送通知的设备 ID,使用 pushbullet.com 设备 URL 中的 device_iden 参数值,或者留空来自行发送", + "NotificationsPushBulletSettingsAccessToken": "", + "NotificationsPushBulletSettingsChannelTags": "频道标签", + "NotificationsPushBulletSettingsChannelTagsHelpText": "通知的目标频道标签列表", + "NotificationsPushBulletSettingsDeviceIds": "设备 ID", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "设备 ID 列表,留空发给所有设备", + "NotificationsPushcutSettingsApiKeyHelpText": "API Key 可在 Pushcut app 的账号视图中管理", + "NotificationsPushcutSettingsNotificationName": "通知名称", + "NotificationsPushcutSettingsNotificationNameHelpText": "Pushcut app 通知页中的通知名称", + "NotificationsJoinSettingsDeviceIdsHelpText": "弃用,请改用“设备名称”。通知要发给的设备的 ID,以括号分隔。留空则发给所有设备。", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "持久化存储方案的配置键名,若使用无状态 URL 则留空。", + "NotificationsAppriseSettingsStatelessUrls": "Apprise 的无状态 URL", + "NotificationsAppriseSettingsTags": "Apprise 标签", + "NotificationsNtfySettingsServerUrlHelpText": "留空使用公共服务器 {url}", + "NotificationsNtfySettingsTagsEmojis": "Ntfy 标签和 emoji", + "NotificationsNtfySettingsTagsEmojisHelpText": "附加标签或 emoji,可选", + "NotificationsNtfySettingsTopics": "话题", + "NotificationsNtfySettingsTopicsHelpText": "通知的目标话题,可选", + "NotificationsNtfySettingsUsernameHelpText": "用户名,可选", + "NotificationsNtfyValidationAuthorizationRequired": "需要授权", + "NotificationsPlexSettingsAuthToken": "验证令牌", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "使用 Plex.tv 验证身份", + "NotificationsPlexValidationNoTvLibraryFound": "需要至少一个电视资源库", + "NotificationsPushBulletSettingSenderId": "发送 ID", + "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index 368b194fe..3ee542f24 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -21,5 +21,13 @@ "Actions": "執行", "AddAutoTagError": "無法加入新的自動標籤,請重新嘗試。", "AddAutoTag": "新增自動標籤", - "AddCondition": "新增條件" + "AddCondition": "新增條件", + "AgeWhenGrabbed": "年齡(當獲取時)", + "AppUpdated": "{appName}已更新", + "ApplyChanges": "應用", + "AllTitles": "所有標題", + "ApplyTagsHelpTextHowToApplyDownloadClients": "如何將標籤套用在被選擇的下載客戶端", + "AddRootFolderError": "無法加進根目錄", + "ApplyTagsHelpTextAdd": "加入:將標籤加入已存在的標籤清單", + "ApplyTagsHelpTextHowToApplyImportLists": "如何套用標籤在所選擇的輸入清單" } From 738f5c58adcd0e5a2d3862a7e3b393d7b68312e9 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Tue, 16 Jan 2024 05:57:50 +0000 Subject: [PATCH 033/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index f5b873b23..730b3b9d5 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -2344,8 +2344,11 @@ "name": "eventType", "in": "query", "schema": { - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } } }, { From 0b0f21d0ac5f5b5695069f1dfb86cc28fcda09b6 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Tue, 16 Jan 2024 16:57:21 +0100 Subject: [PATCH 034/762] Update install.sh to not prompt for package installation Script will exit without input if a prereq package is missing --- distribution/debian/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/debian/install.sh b/distribution/debian/install.sh index aebd59600..87a0b0914 100644 --- a/distribution/debian/install.sh +++ b/distribution/debian/install.sh @@ -98,7 +98,7 @@ echo "Directories created" echo "" echo "Installing pre-requisite Packages" # shellcheck disable=SC2086 -apt update && apt install $app_prereq +apt update && apt install -y $app_prereq echo "" ARCH=$(dpkg --print-architecture) # get arch From 8dd3b45c90209136c0bd0a861061c6d20837d62f Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Fri, 19 Jan 2024 06:42:31 +0100 Subject: [PATCH 035/762] New: Drop commands table content before postgres migration Signed-off-by: Stevie Robinson --- .../188_postgres_update_timestamp_columns_to_with_timezone.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/Datastore/Migration/188_postgres_update_timestamp_columns_to_with_timezone.cs b/src/NzbDrone.Core/Datastore/Migration/188_postgres_update_timestamp_columns_to_with_timezone.cs index f8a865c4a..aae49ab68 100644 --- a/src/NzbDrone.Core/Datastore/Migration/188_postgres_update_timestamp_columns_to_with_timezone.cs +++ b/src/NzbDrone.Core/Datastore/Migration/188_postgres_update_timestamp_columns_to_with_timezone.cs @@ -8,6 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { + Delete.FromTable("Commands").AllRows(); + Alter.Table("Blocklist").AlterColumn("Date").AsDateTimeOffset().NotNullable(); Alter.Table("Blocklist").AlterColumn("PublishedDate").AsDateTimeOffset().Nullable(); Alter.Table("Commands").AlterColumn("QueuedAt").AsDateTimeOffset().NotNullable(); From bfd24da2d9fcad35c754c09d6d8433d56d4d5273 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 19 Jan 2024 07:42:51 +0200 Subject: [PATCH 036/762] Fixed: Importing Plex RSS lists with invalid items (#6374) --- .../ImportLists/Rss/Plex/PlexRssImportParser.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs index 2dda602cb..97c5a9d73 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs @@ -3,18 +3,19 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.ImportLists.Rss.Plex { public class PlexRssImportParser : RssImportBaseParser { + private readonly Logger _logger; private static readonly Regex ImdbIdRegex = new (@"(tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled); public PlexRssImportParser(Logger logger) : base(logger) { + _logger = logger; } protected override ImportListItemInfo ProcessItem(XElement item) @@ -53,7 +54,9 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex if (info.ImdbId.IsNullOrWhiteSpace() && info.TvdbId == 0 && info.TmdbId == 0) { - throw new UnsupportedFeedException("Each item in the RSS feed must have a guid element with a IMDB ID, TVDB ID or TMDB ID"); + _logger.Warn("Each item in the RSS feed must have a guid element with a IMDB ID, TVDB ID or TMDB ID: '{0}'", info.Title); + + return null; } return info; From c6bb6ad8788fb1c20ed466a495f2b47034947145 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Wed, 17 Jan 2024 23:08:01 +0100 Subject: [PATCH 037/762] Round off the seeded ratio when checking for removal candidates Signed-off-by: Stevie Robinson --- src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index c1d6f9f4f..e523992ad 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -620,7 +620,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) { - if (torrent.Ratio >= config.MaxRatio) + if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio) { return true; } From 2dbf5b5a71d5538128d0817884e30070e15f351e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 16 Jan 2024 21:39:03 +0200 Subject: [PATCH 038/762] Check Content-Type in FileList parser --- .../IndexerTests/FileListTests/FileListFixture.cs | 2 +- src/NzbDrone.Core/Indexers/FileList/FileListParser.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs index 995332eca..91128bd1a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests Mocker.GetMock() .Setup(o => o.ExecuteAsync(It.Is(v => v.Method == HttpMethod.Get))) - .Returns(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed))); + .Returns(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed))); var releases = await Subject.FetchRecent(); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index ee43829a7..2d0a16a8f 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -18,8 +18,6 @@ namespace NzbDrone.Core.Indexers.FileList public IList ParseResponse(IndexerResponse indexerResponse) { - var torrentInfos = new List(); - if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { throw new IndexerException(indexerResponse, @@ -27,6 +25,13 @@ namespace NzbDrone.Core.Indexers.FileList indexerResponse.HttpResponse.StatusCode); } + if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + throw new IndexerException(indexerResponse, "Unexpected response header '{0}' from indexer request, expected '{1}'", indexerResponse.HttpResponse.Headers.ContentType, HttpAccept.Json.Value); + } + + var torrentInfos = new List(); + var queryResults = JsonConvert.DeserializeObject>(indexerResponse.Content); foreach (var result in queryResults) From e792db4d3355fedd3ea9e35b3f5e1e30394d9ee3 Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 18 Jan 2024 18:51:43 -0600 Subject: [PATCH 039/762] New: Improve All Series call by using dictionary for stats iteration --- src/Sonarr.Api.V3/Series/SeriesController.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 1a920a6e0..14ee8f465 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -113,7 +113,7 @@ namespace Sonarr.Api.V3.Series } MapCoversToLocal(seriesResources.ToArray()); - LinkSeriesStatistics(seriesResources, seriesStats); + LinkSeriesStatistics(seriesResources, seriesStats.ToDictionary(x => x.SeriesId)); PopulateAlternateTitles(seriesResources); seriesResources.ForEach(LinkRootFolderPath); @@ -229,17 +229,14 @@ namespace Sonarr.Api.V3.Series LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id)); } - private void LinkSeriesStatistics(List resources, List seriesStatistics) + private void LinkSeriesStatistics(List resources, Dictionary seriesStatistics) { foreach (var series in resources) { - var stats = seriesStatistics.SingleOrDefault(ss => ss.SeriesId == series.Id); - if (stats == null) + if (seriesStatistics.TryGetValue(series.Id, out var stats)) { - continue; + LinkSeriesStatistics(series, stats); } - - LinkSeriesStatistics(series, stats); } } From 9884f6f282560ff2a0ea193e9306c6284cf8672c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 18 Jan 2024 21:43:51 -0800 Subject: [PATCH 040/762] Fixed: Icons on full color calendar events Closes #6331 --- .../src/Calendar/Events/CalendarEvent.css | 4 + frontend/src/Calendar/Events/CalendarEvent.js | 18 +++-- .../Calendar/Events/CalendarEventGroup.css | 9 +++ .../Events/CalendarEventGroup.css.d.ts | 1 + .../src/Calendar/Events/CalendarEventGroup.js | 78 ++++++++++--------- frontend/src/Calendar/Legend/Legend.js | 38 ++++++--- .../src/Calendar/Legend/LegendIconItem.css | 4 + .../src/Calendar/Legend/LegendIconItem.js | 15 ++-- frontend/src/Components/Icon.css | 8 -- frontend/src/Components/Icon.js | 6 +- frontend/src/Helpers/Props/icons.js | 6 ++ frontend/src/Styles/Themes/dark.js | 2 + frontend/src/Styles/Themes/light.js | 2 + frontend/src/typings/UiSettings.ts | 1 + 14 files changed, 118 insertions(+), 74 deletions(-) diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css index 0cf3900bf..679b4cc51 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.css +++ b/frontend/src/Calendar/Events/CalendarEvent.css @@ -52,6 +52,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2); .statusContainer { display: flex; align-items: center; + + &:global(.fullColor) { + filter: var(--calendarFullColorFilter) + } } .statusIcon { diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js index 122022773..1f9d59b2b 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.js +++ b/frontend/src/Calendar/Events/CalendarEvent.js @@ -102,7 +102,12 @@ class CalendarEvent extends Component { {series.title}
-
+
{ missingAbsoluteNumber ? : null @@ -150,7 +156,7 @@ class CalendarEvent extends Component { : null @@ -160,9 +166,8 @@ class CalendarEvent extends Component { episodeNumber === 1 && seasonNumber > 0 ? : null @@ -173,8 +178,8 @@ class CalendarEvent extends Component { finaleType ? : null @@ -187,7 +192,6 @@ class CalendarEvent extends Component { className={styles.statusIcon} name={icons.INFO} kind={kinds.PINK} - darken={fullColorEvents} title={translate('Special')} /> : null diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.css b/frontend/src/Calendar/Events/CalendarEventGroup.css index 68a12851d..c52e0192d 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroup.css +++ b/frontend/src/Calendar/Events/CalendarEventGroup.css @@ -50,6 +50,15 @@ margin-bottom: 5px; } +.statusContainer { + display: flex; + align-items: center; + + &:global(.fullColor) { + filter: var(--calendarFullColorFilter) + } +} + .statusIcon { margin-left: 3px; } diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts b/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts index 94f0d11ce..c527feff1 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts +++ b/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts @@ -16,6 +16,7 @@ interface CssExports { 'onAir': string; 'premiere': string; 'seriesTitle': string; + 'statusContainer': string; 'statusIcon': string; 'unaired': string; 'unmonitored': string; diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.js b/frontend/src/Calendar/Events/CalendarEventGroup.js index ea54c8237..2130c11f9 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroup.js +++ b/frontend/src/Calendar/Events/CalendarEventGroup.js @@ -145,45 +145,51 @@ class CalendarEventGroup extends Component { {series.title}
- { - isMissingAbsoluteNumber && - - } +
+ { + isMissingAbsoluteNumber && + + } - { - anyDownloading && - - } + { + anyDownloading && + + } - { - firstEpisode.episodeNumber === 1 && seasonNumber > 0 && - - } + { + firstEpisode.episodeNumber === 1 && seasonNumber > 0 && + + } - { - showFinaleIcon && - lastEpisode.finaleType ? - : null - } + { + showFinaleIcon && + lastEpisode.finaleType ? + : null + } +
diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js index 5fc84234f..f6e970e8b 100644 --- a/frontend/src/Calendar/Legend/Legend.js +++ b/frontend/src/Calendar/Legend/Legend.js @@ -22,9 +22,20 @@ function Legend(props) { if (showFinaleIcon) { iconsToShow.push( + ); + + iconsToShow.push( + ); @@ -33,10 +44,10 @@ function Legend(props) { if (showSpecialIcon) { iconsToShow.push( ); @@ -45,9 +56,10 @@ function Legend(props) { if (showCutoffUnmetIcon) { iconsToShow.push( ); @@ -112,10 +124,10 @@ function Legend(props) {
@@ -129,6 +141,12 @@ function Legend(props) { {iconsToShow[2]}
} + { + iconsToShow.length > 3 && +
+ {iconsToShow[3]} +
+ }
); } diff --git a/frontend/src/Calendar/Legend/LegendIconItem.css b/frontend/src/Calendar/Legend/LegendIconItem.css index 01db0ba5a..c6c12027d 100644 --- a/frontend/src/Calendar/Legend/LegendIconItem.css +++ b/frontend/src/Calendar/Legend/LegendIconItem.css @@ -7,4 +7,8 @@ .icon { margin-right: 5px; + + &:global(.fullColorEvents) { + filter: var(--calendarFullColorFilter) + } } diff --git a/frontend/src/Calendar/Legend/LegendIconItem.js b/frontend/src/Calendar/Legend/LegendIconItem.js index 5ce5f725b..b6bdeeff7 100644 --- a/frontend/src/Calendar/Legend/LegendIconItem.js +++ b/frontend/src/Calendar/Legend/LegendIconItem.js @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; @@ -6,9 +7,9 @@ import styles from './LegendIconItem.css'; function LegendIconItem(props) { const { name, + fullColorEvents, icon, kind, - darken, tooltip } = props; @@ -18,9 +19,11 @@ function LegendIconItem(props) { title={tooltip} > @@ -31,14 +34,10 @@ function LegendIconItem(props) { LegendIconItem.propTypes = { name: PropTypes.string.isRequired, + fullColorEvents: PropTypes.bool.isRequired, icon: PropTypes.object.isRequired, kind: PropTypes.string.isRequired, - darken: PropTypes.bool.isRequired, tooltip: PropTypes.string.isRequired }; -LegendIconItem.defaultProps = { - darken: false -}; - export default LegendIconItem; diff --git a/frontend/src/Components/Icon.css b/frontend/src/Components/Icon.css index 69ffc40f5..51c09226b 100644 --- a/frontend/src/Components/Icon.css +++ b/frontend/src/Components/Icon.css @@ -12,18 +12,10 @@ .info { color: var(--infoColor); - - &:global(.darken) { - color: color(var(--infoColor) shade(30%)); - } } .pink { color: var(--pink); - - &:global(.darken) { - color: color(var(--pink) shade(30%)); - } } .success { diff --git a/frontend/src/Components/Icon.js b/frontend/src/Components/Icon.js index 90842b039..d200b8c08 100644 --- a/frontend/src/Components/Icon.js +++ b/frontend/src/Components/Icon.js @@ -18,7 +18,6 @@ class Icon extends PureComponent { kind, size, title, - darken, isSpinning, ...otherProps } = this.props; @@ -27,8 +26,7 @@ class Icon extends PureComponent { Date: Sun, 14 Jan 2024 21:57:42 +0100 Subject: [PATCH 041/762] Translate backend: Autotagging + CF specs, Metadata + ImportLists Signed-off-by: Stevie Robinson --- .../Specifications/GenreSpecification.cs | 2 +- .../OriginalLanguageSpecification.cs | 2 +- .../QualityProfileSpecification.cs | 2 +- .../Specifications/RootFolderSpecification.cs | 2 +- .../Specifications/SeriesTypeSpecification.cs | 2 +- .../Specifications/StatusSpecification.cs | 2 +- .../Specifications/YearSpecification.cs | 4 +- .../Specifications/LanguageSpecification.cs | 2 +- .../Specifications/RegexSpecificationBase.cs | 2 +- .../Specifications/ResolutionSpecification.cs | 2 +- .../Specifications/SizeSpecification.cs | 4 +- .../Specifications/SourceSpecification.cs | 2 +- .../Consumers/Plex/PlexMetadataSettings.cs | 2 +- .../Roksbox/RoksboxMetadataSettings.cs | 8 +- .../Consumers/Wdtv/WdtvMetadataSettings.cs | 8 +- .../Consumers/Xbmc/XbmcMetadataSettings.cs | 16 +-- .../ImportLists/AniList/AniListImportBase.cs | 4 +- .../AniList/AniListSettingsBase.cs | 8 +- .../ImportLists/AniList/List/AniListImport.cs | 6 +- .../AniList/List/AniListSettings.cs | 24 ++-- .../ImportLists/Custom/CustomImport.cs | 6 +- .../ImportLists/Custom/CustomImportProxy.cs | 11 +- .../ImportLists/Custom/CustomSettings.cs | 2 +- .../ImportLists/HttpImportListBase.cs | 5 +- .../ImportLists/Imdb/ImdbListImport.cs | 6 +- .../ImportLists/Imdb/ImdbListSettings.cs | 2 +- .../ImportLists/ImportListBase.cs | 7 +- .../ImportLists/Plex/PlexImport.cs | 6 +- .../ImportLists/Plex/PlexListSettings.cs | 4 +- .../ImportLists/Rss/Plex/PlexRssImport.cs | 6 +- .../Rss/Plex/PlexRssImportSettings.cs | 2 +- .../ImportLists/Rss/RssImportBase.cs | 4 +- .../ImportLists/Rss/RssImportBaseSettings.cs | 2 +- .../ImportLists/Simkl/SimklImportBase.cs | 4 +- .../ImportLists/Simkl/SimklSettingsBase.cs | 10 +- .../ImportLists/Simkl/User/SimklUserImport.cs | 6 +- .../Simkl/User/SimklUserListType.cs | 12 +- .../Simkl/User/SimklUserSettings.cs | 4 +- .../ImportLists/Sonarr/SonarrImport.cs | 4 +- .../ImportLists/Sonarr/SonarrSettings.cs | 11 +- .../ImportLists/Sonarr/SonarrV3Proxy.cs | 13 +- .../ImportLists/Trakt/List/TraktListImport.cs | 7 +- .../Trakt/List/TraktListSettings.cs | 4 +- .../Trakt/Popular/TraktPopularImport.cs | 6 +- .../Trakt/Popular/TraktPopularListType.cs | 25 ++-- .../Trakt/Popular/TraktPopularSettings.cs | 8 +- .../ImportLists/Trakt/TraktImportBase.cs | 4 +- .../ImportLists/Trakt/TraktSettingsBase.cs | 14 +- .../ImportLists/Trakt/User/TraktUserImport.cs | 6 +- .../Trakt/User/TraktUserListType.cs | 6 +- .../Trakt/User/TraktUserSettings.cs | 8 +- .../Trakt/User/TraktUserWatchedListType.cs | 6 +- src/NzbDrone.Core/Localization/Core/en.json | 132 ++++++++++++++++++ src/Sonarr.Http/ClientSchema/SchemaBuilder.cs | 7 +- 54 files changed, 320 insertions(+), 144 deletions(-) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs index 032813f20..7baab4aac 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Genre"; - [FieldDefinition(1, Label = "Genre(s)", Type = FieldType.Tag)] + [FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)] public IEnumerable Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/OriginalLanguageSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/OriginalLanguageSpecification.cs index 7d725f3a6..4819ce69c 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/OriginalLanguageSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/OriginalLanguageSpecification.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Original Language"; - [FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))] + [FieldDefinition(1, Label = "AutoTaggingSpecificationOriginalLanguage", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs index 4ac2ef14b..35f9866c1 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Quality Profile"; - [FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)] + [FieldDefinition(1, Label = "AutoTaggingSpecificationQualityProfile", Type = FieldType.QualityProfile)] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/RootFolderSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/RootFolderSpecification.cs index 07e08290a..720327b55 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/RootFolderSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/RootFolderSpecification.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Root Folder"; - [FieldDefinition(1, Label = "Root Folder", Type = FieldType.RootFolder)] + [FieldDefinition(1, Label = "AutoTaggingSpecificationRootFolder", Type = FieldType.RootFolder)] public string Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/SeriesTypeSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/SeriesTypeSpecification.cs index 1116db529..2c6676137 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/SeriesTypeSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/SeriesTypeSpecification.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 2; public override string ImplementationName => "Series Type"; - [FieldDefinition(1, Label = "Series Type", Type = FieldType.Select, SelectOptions = typeof(SeriesTypes))] + [FieldDefinition(1, Label = "AutoTaggingSpecificationSeriesType", Type = FieldType.Select, SelectOptions = typeof(SeriesTypes))] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/StatusSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/StatusSpecification.cs index a77af9f36..454ccf2a2 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/StatusSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/StatusSpecification.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Status"; - [FieldDefinition(1, Label = "Status", Type = FieldType.Select, SelectOptions = typeof(SeriesStatusType))] + [FieldDefinition(1, Label = "AutoTaggingSpecificationStatus", Type = FieldType.Select, SelectOptions = typeof(SeriesStatusType))] public int Status { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/YearSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/YearSpecification.cs index f2f97aef5..1f04e65cd 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/YearSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/YearSpecification.cs @@ -23,10 +23,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications public override int Order => 1; public override string ImplementationName => "Year"; - [FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)] + [FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumYear", Type = FieldType.Number)] public int Min { get; set; } - [FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)] + [FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumYear", Type = FieldType.Number)] public int Max { get; set; } protected override bool IsSatisfiedByWithoutNegate(Series series) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs index 19114cc61..fcd5f5374 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.CustomFormats public override int Order => 3; public override string ImplementationName => "Language"; - [FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] + [FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs b/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs index e548d858f..1c133e383 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.CustomFormats protected Regex _regex; protected string _raw; - [FieldDefinition(1, Label = "Regular Expression", HelpText = "Custom Format RegEx is Case Insensitive")] + [FieldDefinition(1, Label = "CustomFormatsSpecificationRegularExpression", HelpText = "CustomFormatsSpecificationRegularExpressionHelpText")] public string Value { get => _raw; diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs index a49d19327..1eb90b953 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats public override int Order => 6; public override string ImplementationName => "Resolution"; - [FieldDefinition(1, Label = "Resolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))] + [FieldDefinition(1, Label = "CustomFormatsSpecificationResolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index 257904a30..3a1e404ec 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -20,10 +20,10 @@ namespace NzbDrone.Core.CustomFormats public override int Order => 8; public override string ImplementationName => "Size"; - [FieldDefinition(1, Label = "Minimum Size", HelpText = "Release must be greater than this size", Unit = "GB", Type = FieldType.Number)] + [FieldDefinition(1, Label = "CustomFormatsSpecificationMinimumSize", HelpText = "CustomFormatsSpecificationMinimumSizeHelpText", Unit = "GB", Type = FieldType.Number)] public double Min { get; set; } - [FieldDefinition(1, Label = "Maximum Size", HelpText = "Release must be less than or equal to this size", Unit = "GB", Type = FieldType.Number)] + [FieldDefinition(1, Label = "CustomFormatsSpecificationMaximumSize", HelpText = "CustomFormatsSpecificationMaximumSizeHelpText", Unit = "GB", Type = FieldType.Number)] public double Max { get; set; } protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs index 1c05c00b3..06c78e1ff 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats public override int Order => 5; public override string ImplementationName => "Source"; - [FieldDefinition(1, Label = "Source", Type = FieldType.Select, SelectOptions = typeof(QualitySource))] + [FieldDefinition(1, Label = "CustomFormatsSpecificationSource", Type = FieldType.Select, SelectOptions = typeof(QualitySource))] public int Value { get; set; } protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs index c2fb1fe80..de94f5153 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex SeriesPlexMatchFile = true; } - [FieldDefinition(0, Label = "Series Plex Match File", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Creates a .plexmatch file in the series folder")] + [FieldDefinition(0, Label = "MetadataPlexSettingsSeriesPlexMatchFile", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataPlexSettingsSeriesPlexMatchFileHelpText")] public bool SeriesPlexMatchFile { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs index 635303916..f612a1a1c 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -21,16 +21,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] + [FieldDefinition(0, Label = "MetadataSettingsEpisodeMetadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Series Title.jpg")] + [FieldDefinition(1, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Series Title.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season ##.jpg")] + [FieldDefinition(2, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season ##.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.jpg")] + [FieldDefinition(3, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs index f1797fbbd..e79ae09b4 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs @@ -21,16 +21,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] + [FieldDefinition(0, Label = "MetadataSettingsEpisodeMetadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "folder.jpg")] + [FieldDefinition(1, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "folder.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\folder.jpg")] + [FieldDefinition(2, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\folder.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.metathumb")] + [FieldDefinition(3, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.metathumb")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs index 12df64d52..312cde16d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs @@ -24,28 +24,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc EpisodeImages = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "tvshow.nfo with full series metadata")] + [FieldDefinition(0, Label = "MetadataSettingsSeriesMetadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataXmbcSettingsSeriesMetadataHelpText")] public bool SeriesMetadata { get; set; } - [FieldDefinition(1, Label = "Series Metadata Episode Guide", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Include JSON formatted episode guide element in tvshow.nfo (Requires 'Series Metadata')", Advanced = true)] + [FieldDefinition(1, Label = "MetadataSettingsSeriesMetadataEpisodeGuide", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText", Advanced = true)] public bool SeriesMetadataEpisodeGuide { get; set; } - [FieldDefinition(2, Label = "Series Metadata URL", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Include TheTVDB show URL in tvshow.nfo (can be combined with 'Series Metadata')", Advanced = true)] + [FieldDefinition(2, Label = "MetadataSettingsSeriesMetadataUrl", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataXmbcSettingsSeriesMetadataUrlHelpText", Advanced = true)] public bool SeriesMetadataUrl { get; set; } - [FieldDefinition(3, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = ".nfo")] + [FieldDefinition(3, Label = "MetadataSettingsEpisodeMetadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = ".nfo")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(4, Label = "Episode Metadata Image Thumbs", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Include image thumb tags in .nfo (Requires 'Episode Metadata')", Advanced = true)] + [FieldDefinition(4, Label = "MetadataSettingsEpisodeMetadataImageThumbs", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText", Advanced = true)] public bool EpisodeImageThumb { get; set; } - [FieldDefinition(5, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "fanart.jpg, poster.jpg, banner.jpg")] + [FieldDefinition(5, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "fanart.jpg, poster.jpg, banner.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(6, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "season##-poster.jpg, season##-banner.jpg, season-specials-poster.jpg, season-specials-banner.jpg")] + [FieldDefinition(6, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "season##-poster.jpg, season##-banner.jpg, season-specials-poster.jpg, season-specials-banner.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(7, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "-thumb.jpg")] + [FieldDefinition(7, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "-thumb.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs b/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs index 0a80675af..b5818775b 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -28,8 +29,9 @@ namespace NzbDrone.Core.ImportLists.AniList IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { _importListRepository = netImportRepository; } diff --git a/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs b/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs index dceb6a7d8..208a70bf0 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs @@ -42,16 +42,16 @@ namespace NzbDrone.Core.ImportLists.AniList public string BaseUrl { get; set; } - [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } - [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string RefreshToken { get; set; } - [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public DateTime Expires { get; set; } - [FieldDefinition(99, Label = "Authenticate with AniList", Type = FieldType.OAuth)] + [FieldDefinition(99, Label = "ImportListsAniListSettingsAuthenticateWithAniList", Type = FieldType.OAuth)] public string SignIn { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs index 0dc91e580..ad62eb1a3 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Http.CloudFlare; using NzbDrone.Core.ImportLists.Exceptions; using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -21,12 +22,13 @@ namespace NzbDrone.Core.ImportLists.AniList.List IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(netImportRepository, httpClient, importListStatusService, configService, parsingService, logger) + : base(netImportRepository, httpClient, importListStatusService, configService, parsingService, localizationService, logger) { } - public override string Name => "AniList List"; + public override string Name => _localizationService.GetLocalizedString("TypeOfList", new Dictionary { { "typeOfList", "AniList" } }); public override AniListRequestGenerator GetRequestGenerator() { diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs index c160892e6..3f6c34b14 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs @@ -31,40 +31,40 @@ namespace NzbDrone.Core.ImportLists.AniList.List protected override AbstractValidator Validator => new AniListSettingsValidator(); - [FieldDefinition(1, Label = "Username", HelpText = "Username for the List to import from")] + [FieldDefinition(1, Label = "Username", HelpText = "ImportListsAniListSettingsUsernameHelpText")] public string Username { get; set; } - [FieldDefinition(2, Label = "Import Watching", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: Currently Watching")] + [FieldDefinition(2, Label = "ImportListsAniListSettingsImportWatching", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportWatchingHelpText")] public bool ImportCurrent { get; set; } - [FieldDefinition(3, Label = "Import Planning", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: Planning to Watch")] + [FieldDefinition(3, Label = "ImportListsAniListSettingsImportPlanning", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPlanningHelpText")] public bool ImportPlanning { get; set; } - [FieldDefinition(4, Label = "Import Completed", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: Completed Watching")] + [FieldDefinition(4, Label = "ImportListsAniListSettingsImportCompleted", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCompletedHelpText")] public bool ImportCompleted { get; set; } - [FieldDefinition(5, Label = "Import Dropped", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: Dropped")] + [FieldDefinition(5, Label = "ImportListsAniListSettingsImportDropped", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportDroppedHelpText")] public bool ImportDropped { get; set; } - [FieldDefinition(6, Label = "Import Paused", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: On Hold")] + [FieldDefinition(6, Label = "ImportListsAniListSettingsImportPaused", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPausedHelpText")] public bool ImportPaused { get; set; } - [FieldDefinition(7, Label = "Import Repeating", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "List: Currently Rewatching")] + [FieldDefinition(7, Label = "ImportListsAniListSettingsImportRepeating", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportRepeatingHelpText")] public bool ImportRepeating { get; set; } - [FieldDefinition(8, Label = "Import Finished", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "Media: All episodes have aired")] + [FieldDefinition(8, Label = "ImportListsAniListSettingsImportFinished", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportFinishedHelpText")] public bool ImportFinished { get; set; } - [FieldDefinition(9, Label = "Import Releasing", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "Media: Currently airing new episodes")] + [FieldDefinition(9, Label = "ImportListsAniListSettingsImportReleasing", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportReleasingHelpText")] public bool ImportReleasing { get; set; } - [FieldDefinition(10, Label = "Import Not Yet Released", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "Media: Airing has not yet started")] + [FieldDefinition(10, Label = "ImportListsAniListSettingsImportNotYetReleased", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportNotYetReleasedHelpText")] public bool ImportUnreleased { get; set; } - [FieldDefinition(11, Label = "Import Cancelled", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "Media: Series is cancelled")] + [FieldDefinition(11, Label = "ImportListsAniListSettingsImportCancelled", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCancelledHelpText")] public bool ImportCancelled { get; set; } - [FieldDefinition(12, Label = "Import Hiatus", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "Media: Series on Hiatus")] + [FieldDefinition(12, Label = "ImportListsAniListSettingsImportHiatus", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportHiatusHelpText")] public bool ImportHiatus { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs index 29157be63..3d4645c49 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs @@ -4,6 +4,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -12,7 +13,7 @@ namespace NzbDrone.Core.ImportLists.Custom public class CustomImport : ImportListBase { private readonly ICustomImportProxy _customProxy; - public override string Name => "Custom List"; + public override string Name => _localizationService.GetLocalizedString("ImportListsCustomListSettingsName"); public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6); @@ -22,8 +23,9 @@ namespace NzbDrone.Core.ImportLists.Custom IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(importListStatusService, configService, parsingService, logger) + : base(importListStatusService, configService, parsingService, localizationService, logger) { _customProxy = customProxy; } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomImportProxy.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomImportProxy.cs index 8b7cfeb67..a47181b1e 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomImportProxy.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomImportProxy.cs @@ -10,6 +10,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Localization; namespace NzbDrone.Core.ImportLists.Custom { @@ -22,11 +23,13 @@ namespace NzbDrone.Core.ImportLists.Custom public class CustomImportProxy : ICustomImportProxy { private readonly IHttpClient _httpClient; + private readonly ILocalizationService _localizationService; private readonly Logger _logger; - public CustomImportProxy(IHttpClient httpClient, Logger logger) + public CustomImportProxy(IHttpClient httpClient, ILocalizationService localizationService, Logger logger) { _httpClient = httpClient; + _localizationService = localizationService; _logger = logger; } @@ -46,16 +49,16 @@ namespace NzbDrone.Core.ImportLists.Custom if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) { _logger.Error(ex, "There was an authorization issue. We cannot get the list from the provider."); - return new ValidationFailure("BaseUrl", "It seems we are unauthorized to make this request."); + return new ValidationFailure("BaseUrl", _localizationService.GetLocalizedString("ImportListsCustomListValidationAuthenticationFailure")); } _logger.Error(ex, "Unable to connect to import list."); - return new ValidationFailure("BaseUrl", $"We are unable to make the request to that URL. StatusCode: {ex.Response.StatusCode}"); + return new ValidationFailure("BaseUrl", _localizationService.GetLocalizedString("ImportListsCustomListValidationConnectionError", new Dictionary { { "exceptionStatusCode", ex.Response.StatusCode } })); } catch (Exception ex) { _logger.Error(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 new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("ImportListsValidationUnableToConnectException", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs index e756225ac..ef3e98f76 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.Custom BaseUrl = ""; } - [FieldDefinition(0, Label = "List URL", HelpText = "The URL for the series list")] + [FieldDefinition(0, Label = "ImportListsCustomListSettingsUrl", HelpText = "ImportListsCustomListSettingsUrlHelpText")] public string BaseUrl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs b/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs index 8a19ab64c..25057c2fd 100644 --- a/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Http.CloudFlare; using NzbDrone.Core.ImportLists.Exceptions; using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -31,8 +32,8 @@ namespace NzbDrone.Core.ImportLists public abstract IImportListRequestGenerator GetRequestGenerator(); public abstract IParseImportListResponse GetParser(); - public HttpImportListBase(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(importListStatusService, configService, parsingService, logger) + public HttpImportListBase(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, Logger logger) + : base(importListStatusService, configService, parsingService, localizationService, logger) { _httpClient = httpClient; } diff --git a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListImport.cs b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListImport.cs index bba85c407..67ee51e0a 100644 --- a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListImport.cs +++ b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListImport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.ThingiProvider; @@ -10,7 +11,7 @@ namespace NzbDrone.Core.ImportLists.Imdb { public class ImdbListImport : HttpImportListBase { - public override string Name => "IMDb Lists"; + public override string Name => _localizationService.GetLocalizedString("TypeOfList", new Dictionary { { "typeOfList", "IMDb" } }); public override ImportListType ListType => ImportListType.Other; public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(12); @@ -19,8 +20,9 @@ namespace NzbDrone.Core.ImportLists.Imdb IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { } diff --git a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs index 29df45aca..50a7934c0 100644 --- a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.ImportLists.Imdb public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "List ID", HelpText = "IMDb list ID (e.g ls12345678)")] + [FieldDefinition(1, Label = "ImportListsImdbSettingsListId", HelpText = "ImportListsImdbSettingsListIdHelpText")] public string ListId { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/ImportListBase.cs b/src/NzbDrone.Core/ImportLists/ImportListBase.cs index a4c2e81f9..5806e3095 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListBase.cs @@ -4,6 +4,7 @@ using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.ImportLists protected readonly IImportListStatusService _importListStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; + protected readonly ILocalizationService _localizationService; protected readonly Logger _logger; public abstract string Name { get; } @@ -24,11 +26,12 @@ namespace NzbDrone.Core.ImportLists public abstract TimeSpan MinRefreshInterval { get; } - public ImportListBase(IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + public ImportListBase(IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, Logger logger) { _importListStatusService = importListStatusService; _configService = configService; _parsingService = parsingService; + _localizationService = localizationService; _logger = logger; } @@ -86,7 +89,7 @@ namespace NzbDrone.Core.ImportLists catch (Exception ex) { _logger.Error(ex, "Test aborted due to exception"); - failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); + failures.Add(new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("ImportListsValidationTestFailed", new Dictionary { { "exceptionMessage", ex.Message } }))); } return new ValidationResult(failures); diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs index 5edb0fa91..393bdb692 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs @@ -5,6 +5,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Notifications.Plex.PlexTv; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -24,13 +25,14 @@ namespace NzbDrone.Core.ImportLists.Plex IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { _plexTvService = plexTvService; } - public override string Name => "Plex Watchlist"; + public override string Name => _localizationService.GetLocalizedString("ImportListsPlexSettingsWatchlistName"); public override int PageSize => 50; public override IList Fetch() diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs index 8d95285e8..0ffbdba2a 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs @@ -27,10 +27,10 @@ namespace NzbDrone.Core.ImportLists.Plex public string BaseUrl { get; set; } - [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } - [FieldDefinition(99, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)] + [FieldDefinition(99, Label = "ImportListsPlexSettingsAuthenticateWithPlex", Type = FieldType.OAuth)] public string SignIn { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImport.cs b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImport.cs index 91a379c54..20bec751f 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImport.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImport.cs @@ -2,13 +2,14 @@ using System; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Rss.Plex { public class PlexRssImport : RssImportBase { - public override string Name => "Plex Watchlist RSS"; + public override string Name => _localizationService.GetLocalizedString("ImportListsPlexSettingsWatchlistRSSName"); public override ImportListType ListType => ImportListType.Plex; public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6); @@ -16,8 +17,9 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { } diff --git a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs index 941b90a4a..d6d2e1709 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex { private PlexRssImportSettingsValidator Validator => new (); - [FieldDefinition(0, Label = "Url", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")] + [FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")] public override string Url { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs index de3846687..ef6b6a7ef 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -19,8 +20,9 @@ namespace NzbDrone.Core.ImportLists.Rss IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { } diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs index 32f631676..9df9d4dd0 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.ImportLists.Rss public string BaseUrl { get; set; } - [FieldDefinition(0, Label = "Url", Type = FieldType.Textbox)] + [FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox)] public virtual string Url { get; set; } public virtual NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs b/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs index 6b7ba596e..8592fb1cc 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -28,8 +29,9 @@ namespace NzbDrone.Core.ImportLists.Simkl IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { _importListRepository = netImportRepository; } diff --git a/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs index f77de5665..13deba893 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs @@ -37,19 +37,19 @@ namespace NzbDrone.Core.ImportLists.Simkl public string BaseUrl { get; set; } - [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } - [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string RefreshToken { get; set; } - [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public DateTime Expires { get; set; } - [FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAuthUser", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AuthUser { get; set; } - [FieldDefinition(99, Label = "Authenticate with Simkl", Type = FieldType.OAuth)] + [FieldDefinition(99, Label = "ImportListsSimklSettingsAuthenticatewithSimkl", Type = FieldType.OAuth)] public string SignIn { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserImport.cs b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserImport.cs index d7a9f7908..a425c385b 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserImport.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Simkl.User @@ -12,12 +13,13 @@ namespace NzbDrone.Core.ImportLists.Simkl.User IImportListStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, localizationService, logger) { } - public override string Name => "Simkl User Watchlist"; + public override string Name => _localizationService.GetLocalizedString("ImportListsSimklSettingsName"); public override IImportListRequestGenerator GetRequestGenerator() { diff --git a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserListType.cs b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserListType.cs index e00bc60ac..6da42ccfc 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserListType.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserListType.cs @@ -1,18 +1,18 @@ -using System.Runtime.Serialization; +using NzbDrone.Core.Annotations; namespace NzbDrone.Core.ImportLists.Simkl.User { public enum SimklUserListType { - [EnumMember(Value = "Watching")] + [FieldOption(Label = "ImportListsSimklSettingsUserListTypeWatching")] Watching = 0, - [EnumMember(Value = "Plan To Watch")] + [FieldOption(Label = "ImportListsSimklSettingsUserListTypePlanToWatch")] PlanToWatch = 1, - [EnumMember(Value = "Hold")] + [FieldOption(Label = "ImportListsSimklSettingsUserListTypeHold")] Hold = 2, - [EnumMember(Value = "Completed")] + [FieldOption(Label = "ImportListsSimklSettingsUserListTypeCompleted")] Completed = 3, - [EnumMember(Value = "Dropped")] + [FieldOption(Label = "ImportListsSimklSettingsUserListTypeDropped")] Dropped = 4 } } diff --git a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs index 65f19aa3f..d5342c578 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs @@ -22,10 +22,10 @@ namespace NzbDrone.Core.ImportLists.Simkl.User ShowType = (int)SimklUserShowType.Shows; } - [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(SimklUserListType), HelpText = "Type of list you're seeking to import from")] + [FieldDefinition(1, Label = "ImportListsSimklSettingsListType", Type = FieldType.Select, SelectOptions = typeof(SimklUserListType), HelpText = "ImportListsSimklSettingsListTypeHelpText")] public int ListType { get; set; } - [FieldDefinition(1, Label = "Show Type", Type = FieldType.Select, SelectOptions = typeof(SimklUserShowType), HelpText = "Type of show you're seeking to import from")] + [FieldDefinition(1, Label = "ImportListsSimklSettingsShowType", Type = FieldType.Select, SelectOptions = typeof(SimklUserShowType), HelpText = "ImportListsSimklSettingsShowTypeHelpText")] public int ShowType { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs index 33d701781..369b10e52 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs @@ -5,6 +5,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -23,8 +24,9 @@ namespace NzbDrone.Core.ImportLists.Sonarr IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(importListStatusService, configService, parsingService, logger) + : base(importListStatusService, configService, parsingService, localizationService, logger) { _sonarrV3Proxy = sonarrV3Proxy; } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs index ced896f8f..8037a4efa 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs @@ -29,22 +29,23 @@ namespace NzbDrone.Core.ImportLists.Sonarr RootFolderPaths = Array.Empty(); } - [FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Sonarr instance to import from")] + [FieldDefinition(0, Label = "ImportListsSonarrSettingsFullUrl", HelpText = "ImportListsSonarrSettingsFullUrlHelpText")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Key", HelpText = "Apikey of the Sonarr instance to import from")] + [FieldDefinition(1, Label = "ApiKey", HelpText = "ImportListsSonarrSettingsApiKeyHelpText")] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "Quality Profiles", HelpText = "Quality Profiles from the source instance to import from")] + [FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "QualityProfiles", HelpText = "ImportListsSonarrSettingsQualityProfilesHelpText")] public IEnumerable ProfileIds { get; set; } + // TODO: Remove this eventually, no translation added as deprecated [FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getLanguageProfiles", Label = "Language Profiles", HelpText = "Language Profiles from the source instance to import from")] public IEnumerable LanguageProfileIds { get; set; } - [FieldDefinition(4, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")] + [FieldDefinition(4, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "ImportListsSonarrSettingsTagsHelpText")] public IEnumerable TagIds { get; set; } - [FieldDefinition(5, Type = FieldType.Select, SelectOptionsProviderAction = "getRootFolders", Label = "Root Folders", HelpText = "Root Folders from the source instance to import from")] + [FieldDefinition(5, Type = FieldType.Select, SelectOptionsProviderAction = "getRootFolders", Label = "RootFolders", HelpText = "ImportListsSonarrSettingsRootFoldersHelpText")] public IEnumerable RootFolderPaths { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrV3Proxy.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrV3Proxy.cs index 2021304ed..2913dae9b 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrV3Proxy.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrV3Proxy.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Localization; namespace NzbDrone.Core.ImportLists.Sonarr { @@ -23,10 +24,12 @@ namespace NzbDrone.Core.ImportLists.Sonarr { private readonly IHttpClient _httpClient; private readonly Logger _logger; + private readonly ILocalizationService _localizationService; - public SonarrV3Proxy(IHttpClient httpClient, Logger logger) + public SonarrV3Proxy(IHttpClient httpClient, ILocalizationService localizationService, Logger logger) { _httpClient = httpClient; + _localizationService = localizationService; _logger = logger; } @@ -66,22 +69,22 @@ namespace NzbDrone.Core.ImportLists.Sonarr if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) { _logger.Error(ex, "API Key is invalid"); - return new ValidationFailure("ApiKey", "API Key is invalid"); + return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("ImportListsValidationInvalidApiKey")); } if (ex.Response.HasHttpRedirect) { _logger.Error(ex, "Sonarr returned redirect and is invalid"); - return new ValidationFailure("BaseUrl", "Sonarr URL is invalid, are you missing a URL base?"); + return new ValidationFailure("BaseUrl", _localizationService.GetLocalizedString("ImportListsSonarrValidationInvalidUrl")); } _logger.Error(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 new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("ImportListsValidationUnableToConnectException", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (Exception ex) { _logger.Error(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 new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("ImportListsValidationUnableToConnectException", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs index 3d67b7ead..a8c577d84 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Trakt.List @@ -12,12 +14,13 @@ namespace NzbDrone.Core.ImportLists.Trakt.List IImportListStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, localizationService, logger) { } - public override string Name => "Trakt List"; + public override string Name => _localizationService.GetLocalizedString("TypeOfList", new Dictionary { { "typeOfList", "Trakt" } }); public override IImportListRequestGenerator GetRequestGenerator() { diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs index 39b1bd6d7..d6fa02405 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs @@ -17,10 +17,10 @@ namespace NzbDrone.Core.ImportLists.Trakt.List { protected override AbstractValidator Validator => new TraktListSettingsValidator(); - [FieldDefinition(1, Label = "Username", HelpText = "Username for the List to import from")] + [FieldDefinition(1, Label = "Username", HelpText = "ImportListsTraktSettingsUsernameHelpText")] public string Username { get; set; } - [FieldDefinition(2, Label = "List Name", HelpText = "List name for import, list must be public or you must have access to the list")] + [FieldDefinition(2, Label = "ImportListsTraktSettingsListName", HelpText = "ImportListsTraktSettingsListNameHelpText")] public string Listname { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs index b31cc8322..960e0c6db 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Trakt.Popular @@ -12,12 +13,13 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular IImportListStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, localizationService, logger) { } - public override string Name => "Trakt Popular List"; + public override string Name => _localizationService.GetLocalizedString("ImportListsTraktSettingsPopularName"); public override IParseImportListResponse GetParser() { diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs index 461e986e6..3ce9cf275 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs @@ -1,31 +1,30 @@ -using System.Runtime.Serialization; +using NzbDrone.Core.Annotations; namespace NzbDrone.Core.ImportLists.Trakt.Popular { public enum TraktPopularListType { - [EnumMember(Value = "Trending Shows")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeTrendingShows")] Trending = 0, - [EnumMember(Value = "Popular Shows")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypePopularShows")] Popular = 1, - [EnumMember(Value = "Anticipated Shows")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeAnticipatedShows")] Anticipated = 2, - - [EnumMember(Value = "Top Watched Shows By Week")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeTopWeekShows")] TopWatchedByWeek = 3, - [EnumMember(Value = "Top Watched Shows By Month")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeTopMonthShows")] TopWatchedByMonth = 4, - [EnumMember(Value = "Top Watched Shows By Year")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeTopYearShows")] TopWatchedByYear = 5, - [EnumMember(Value = "Top Watched Shows Of All Time")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeTopAllTimeShows")] TopWatchedByAllTime = 6, - [EnumMember(Value = "Recommended Shows By Week")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows")] RecommendedByWeek = 7, - [EnumMember(Value = "Recommended Shows By Month")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows")] RecommendedByMonth = 8, - [EnumMember(Value = "Recommended Shows By Year")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeRecommendedYearShows")] RecommendedByYear = 9, - [EnumMember(Value = "Recommended Shows Of All Time")] + [FieldOption(Label = "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows")] RecommendedByAllTime = 10 } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs index 24dcc097d..6d2fef5fb 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs @@ -35,16 +35,16 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular TraktListType = (int)TraktPopularListType.Popular; } - [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "Type of list you're seeking to import from")] + [FieldDefinition(1, Label = "ImportListsTraktSettingsListType", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "ImportListsTraktSettingsListTypeHelpText")] public int TraktListType { get; set; } - [FieldDefinition(2, Label = "Rating", HelpText = "Filter series by rating range (0-100)")] + [FieldDefinition(2, Label = "ImportListsTraktSettingsRating", HelpText = "ImportListsTraktSettingsRatingHelpText")] public string Rating { get; set; } - [FieldDefinition(4, Label = "Genres", HelpText = "Filter series by Trakt Genre Slug (Comma Separated) Only for Popular Lists")] + [FieldDefinition(4, Label = "ImportListsTraktSettingsGenres", HelpText = "ImportListsTraktSettingsGenresHelpText")] public string Genres { get; set; } - [FieldDefinition(5, Label = "Years", HelpText = "Filter series by year or year range")] + [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsHelpText")] public string Years { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs index 79922f3f4..848fe9553 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -28,8 +29,9 @@ namespace NzbDrone.Core.ImportLists.Trakt IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) { _importListRepository = netImportRepository; } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs index 6b7ed65c7..91ebf19be 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs @@ -48,25 +48,25 @@ namespace NzbDrone.Core.ImportLists.Trakt public string BaseUrl { get; set; } - [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } - [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string RefreshToken { get; set; } - [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public DateTime Expires { get; set; } - [FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + [FieldDefinition(0, Label = "ImportListsSettingsAuthUser", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AuthUser { get; set; } - [FieldDefinition(5, Label = "Limit", HelpText = "Limit the number of series to get")] + [FieldDefinition(5, Label = "ImportListsTraktSettingsLimit", HelpText = "ImportListsTraktSettingsLimitHelpText")] public int Limit { get; set; } - [FieldDefinition(6, Label = "Additional Parameters", HelpText = "Additional Trakt API parameters", Advanced = true)] + [FieldDefinition(6, Label = "ImportListsTraktSettingsAdditionalParameters", HelpText = "ImportListsTraktSettingsAdditionalParametersHelpText", Advanced = true)] public string TraktAdditionalParameters { get; set; } - [FieldDefinition(99, Label = "Authenticate with Trakt", Type = FieldType.OAuth)] + [FieldDefinition(99, Label = "ImportListsTraktSettingsAuthenticateWithTrakt", Type = FieldType.OAuth)] public string SignIn { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs index 9a826112a..cea9b81b0 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Trakt.User @@ -12,12 +13,13 @@ namespace NzbDrone.Core.ImportLists.Trakt.User IImportListStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, + ILocalizationService localizationService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, localizationService, logger) { } - public override string Name => "Trakt User"; + public override string Name => _localizationService.GetLocalizedString("ImportListsTraktSettingsUserListName"); public override IParseImportListResponse GetParser() { diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs index 8a0720c23..a42eaf01a 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.ImportLists.Trakt.User { public enum TraktUserListType { - [FieldOption(Label = "User Watch List")] + [FieldOption(Label = "ImportListsTraktSettingsUserListTypeWatch")] UserWatchList = 0, - [FieldOption(Label = "User Watched List")] + [FieldOption(Label = "ImportListsTraktSettingsUserListTypeWatched")] UserWatchedList = 1, - [FieldOption(Label = "User Collection List")] + [FieldOption(Label = "ImportListsTraktSettingsUserListTypeCollection")] UserCollectionList = 2 } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs index f6ccb7a9c..87becf6f0 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -25,16 +25,16 @@ namespace NzbDrone.Core.ImportLists.Trakt.User TraktWatchSorting = (int)TraktUserWatchSorting.Rank; } - [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktUserListType), HelpText = "Type of list you're seeking to import from")] + [FieldDefinition(1, Label = "ImportListsTraktSettingsListType", Type = FieldType.Select, SelectOptions = typeof(TraktUserListType), HelpText = "ImportListsTraktSettingsListTypeHelpText")] public int TraktListType { get; set; } - [FieldDefinition(2, Label = "Watched List Filter", Type = FieldType.Select, SelectOptions = typeof(TraktUserWatchedListType), HelpText = "If List Type is Watched. Series do you want to import from")] + [FieldDefinition(2, Label = "ImportListsTraktSettingsWatchedListFilter", Type = FieldType.Select, SelectOptions = typeof(TraktUserWatchedListType), HelpText = "ImportListsTraktSettingsWatchedListFilterHelpText")] public int TraktWatchedListType { get; set; } - [FieldDefinition(3, Label = "Watch List Sorting", Type = FieldType.Select, SelectOptions = typeof(TraktUserWatchSorting), HelpText = "If List Type is Watch")] + [FieldDefinition(3, Label = "ImportListsTraktSettingsWatchedListSorting", Type = FieldType.Select, SelectOptions = typeof(TraktUserWatchSorting), HelpText = "ImportListsTraktSettingsWatchedListSortingHelpText")] public int TraktWatchSorting { get; set; } - [FieldDefinition(4, Label = "Username", HelpText = "Username for the List to import from (empty to use Auth User)")] + [FieldDefinition(4, Label = "Username", HelpText = "ImportListsTraktSettingsUserListUsernameHelpText")] public string Username { get; set; } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs index 396ce5987..84634d7d1 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.ImportLists.Trakt.User { public enum TraktUserWatchedListType { - [FieldOption(Label = "All")] + [FieldOption(Label = "ImportListsTraktSettingsWatchedListTypeAll")] All = 0, - [FieldOption(Label = "In Progress")] + [FieldOption(Label = "ImportListsTraktSettingsWatchedListTypeInProgress")] InProgress = 1, - [FieldOption(Label = "100% Watched")] + [FieldOption(Label = "ImportListsTraktSettingsWatchedListTypeCompleted")] CompletelyWatched = 2 } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 13dab7ab6..c437f4973 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -127,6 +127,14 @@ "AutoTaggingLoadError": "Unable to load auto tagging", "AutoTaggingNegateHelpText": "If checked, the auto tagging rule will not apply if this {implementationName} condition matches.", "AutoTaggingRequiredHelpText": "This {implementationName} condition must match for the auto tagging rule to apply. Otherwise a single {implementationName} match is sufficient.", + "AutoTaggingSpecificationGenre": "Genre(s)", + "AutoTaggingSpecificationMaximumYear": "Maximum Year", + "AutoTaggingSpecificationMinimumYear": "Minimum Year", + "AutoTaggingSpecificationOriginalLanguage": "Language", + "AutoTaggingSpecificationQualityProfile": "Quality Profile", + "AutoTaggingSpecificationRootFolder": "Root Folder", + "AutoTaggingSpecificationSeriesType": "Series Type", + "AutoTaggingSpecificationStatus": "Status", "Automatic": "Automatic", "AutomaticAdd": "Automatic Add", "AutomaticSearch": "Automatic Search", @@ -257,6 +265,16 @@ "CustomFormatsLoadError": "Unable to load Custom Formats", "CustomFormatsSettings": "Custom Formats Settings", "CustomFormatsSettingsSummary": "Custom Formats and Settings", + "CustomFormatsSpecificationLanguage": "Language", + "CustomFormatsSpecificationMaximumSize": "Maximum Size", + "CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size", + "CustomFormatsSpecificationMinimumSize": "Minimum Size", + "CustomFormatsSpecificationMinimumSizeHelpText": "Release must be greater than this size", + "CustomFormatsSpecificationRegularExpression": "Language", + "CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive", + "CustomFormatsSpecificationReleaseGroup": "Release Group", + "CustomFormatsSpecificationResolution": "Resolution", + "CustomFormatsSpecificationSource": "Source", "Cutoff": "Cutoff", "CutoffUnmet": "Cutoff Unmet", "CutoffUnmetLoadError": "Error loading cutoff unmet items", @@ -759,8 +777,108 @@ "ImportListStatusAllUnavailableHealthCheckMessage": "All lists are unavailable due to failures", "ImportListStatusUnavailableHealthCheckMessage": "Lists unavailable due to failures: {importListNames}", "ImportLists": "Import Lists", + "ImportListsAniListSettingsAuthenticateWithAniList": "Authenticate with AniList", + "ImportListsAniListSettingsImportCancelled": "Import Cancelled", + "ImportListsAniListSettingsImportCancelledHelpText": "Media: Series is cancelled", + "ImportListsAniListSettingsImportCompleted": "Import Completed", + "ImportListsAniListSettingsImportCompletedHelpText": "List: Completed Watching", + "ImportListsAniListSettingsImportDropped": "Import Dropped", + "ImportListsAniListSettingsImportDroppedHelpText": "List: Dropped", + "ImportListsAniListSettingsImportFinished": "Import Finished", + "ImportListsAniListSettingsImportFinishedHelpText": "Media: All episodes have aired", + "ImportListsAniListSettingsImportHiatus": "Import Hiatus", + "ImportListsAniListSettingsImportHiatusHelpText": "Media: Series on Hiatus", + "ImportListsAniListSettingsImportNotYetReleased": "Import Not Yet Released", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Media: Airing has not yet started", + "ImportListsAniListSettingsImportPaused": "Import Paused", + "ImportListsAniListSettingsImportPausedHelpText": "List: On Hold", + "ImportListsAniListSettingsImportPlanning": "Import Planning", + "ImportListsAniListSettingsImportPlanningHelpText": "List: Planning to Watch", + "ImportListsAniListSettingsImportReleasing": "Import Releasing", + "ImportListsAniListSettingsImportReleasingHelpText": "Media: Currently airing new episodes", + "ImportListsAniListSettingsImportRepeating": "Import Repeating", + "ImportListsAniListSettingsImportRepeatingHelpText": "List: Currently Re-watching", + "ImportListsAniListSettingsImportWatching": "Import Watching", + "ImportListsAniListSettingsImportWatchingHelpText": "List: Currently Watching", + "ImportListsAniListSettingsUsernameHelpText": "Username for the List to import from", + "ImportListsCustomListSettingsName": "Custom List", + "ImportListsCustomListSettingsUrl": "List URL", + "ImportListsCustomListSettingsUrlHelpText": "The URL for the series list", + "ImportListsCustomListValidationAuthenticationFailure": "Authentication Failure", + "ImportListsCustomListValidationConnectionError": "Unable to make the request to that URL. StatusCode: {exceptionStatusCode}", + "ImportListsImdbSettingsListId": "List ID", + "ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)", "ImportListsLoadError": "Unable to load Import Lists", + "ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", + "ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS", + "ImportListsSettingsAccessToken": "Access Token", + "ImportListsSettingsAuthUser": "Auth User", + "ImportListsSettingsExpires": "Expires", + "ImportListsSettingsRefreshToken": "Refresh Token", + "ImportListsSettingsRssUrl": "RSS URL", "ImportListsSettingsSummary": "Import from another {appName} instance or Trakt lists and manage list exclusions", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Authenticate with Simkl", + "ImportListsSimklSettingsListType": "List Type", + "ImportListsSimklSettingsListTypeHelpText": "Type of list you're seeking to import from", + "ImportListsSimklSettingsName": "Simkl User Watchlist", + "ImportListsSimklSettingsShowType": "Show Type", + "ImportListsSimklSettingsShowTypeHelpText": "Type of show you're seeking to import from", + "ImportListsSimklSettingsUserListTypeCompleted": "Completed", + "ImportListsSimklSettingsUserListTypeDropped": "Dropped", + "ImportListsSimklSettingsUserListTypeHold": "Hold", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Plan To Watch", + "ImportListsSimklSettingsUserListTypeWatching": "Watching", + "ImportListsSonarrSettingsApiKeyHelpText": "API Key of the {appName} instance to import from", + "ImportListsSonarrSettingsFullUrl": "Full URL", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, including port, of the {appName} instance to import from", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Quality Profiles from the source instance to import from", + "ImportListsSonarrSettingsRootFoldersHelpText": "Root Folders from the source instance to import from", + "ImportListsSonarrSettingsTagsHelpText": "Tags from the source instance to import from", + "ImportListsSonarrValidationInvalidUrl": "{appName} URL is invalid, are you missing a URL base?", + "ImportListsTraktSettingsAdditionalParameters": "Additional Parameters", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Additional Trakt API parameters", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Authenticate with Trakt", + "ImportListsTraktSettingsGenres": "Genres", + "ImportListsTraktSettingsGenresHelpText": "Filter series by Trakt Genre Slug (Comma Separated) Only for Popular Lists", + "ImportListsTraktSettingsLimit": "Limit", + "ImportListsTraktSettingsLimitHelpText": "Limit the number of series to get", + "ImportListsTraktSettingsListName": "List Name", + "ImportListsTraktSettingsListNameHelpText": "List name for import, list must be public or you must have access to the list", + "ImportListsTraktSettingsListType": "List Type", + "ImportListsTraktSettingsListTypeHelpText": "Type of list you're seeking to import from", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Anticipated Shows", + "ImportListsTraktSettingsPopularListTypePopularShows": "Popular Shows", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Recommended Shows Of All Time", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Recommended Shows By Month", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Recommended Shows By Week", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Recommended Shows By Year", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Top Watched Shows Of All Time", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Top Watched Shows By Month", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Top Watched Shows By Week", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Top Watched Shows By Year", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trending Shows", + "ImportListsTraktSettingsPopularName": "Trakt Popular List", + "ImportListsTraktSettingsRating": "Rating", + "ImportListsTraktSettingsRatingHelpText": "Filter series by rating range (0-100)", + "ImportListsTraktSettingsUserListName": "Trakt User", + "ImportListsTraktSettingsUserListTypeCollection": "User Collection List", + "ImportListsTraktSettingsUserListTypeWatch": "User Watch List", + "ImportListsTraktSettingsUserListTypeWatched": "User Watched List", + "ImportListsTraktSettingsUserListUsernameHelpText": "Username for the List to import from (leave empty to use Auth User)", + "ImportListsTraktSettingsUsernameHelpText": "Username for the List to import from", + "ImportListsTraktSettingsWatchedListFilter": "Watched List Filter", + "ImportListsTraktSettingsWatchedListFilterHelpText": "If List Type is Watched, select the series type you want to import", + "ImportListsTraktSettingsWatchedListSorting": "Watch List Sorting", + "ImportListsTraktSettingsWatchedListSortingHelpText": "If List Type is Watched, select the order to sort the list", + "ImportListsTraktSettingsWatchedListTypeAll": "All", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Watched", + "ImportListsTraktSettingsWatchedListTypeInProgress": "In Progress", + "ImportListsTraktSettingsYears": "Years", + "ImportListsTraktSettingsYearsHelpText": "Filter series by year or year range", + "ImportListsValidationInvalidApiKey": "API Key is invalid", + "ImportListsValidationTestFailed": "Test was aborted due to an error: {exceptionMessage}", + "ImportListsValidationUnableToConnectException": "Unable to connect to import list: {exceptionMessage}. Check the log surrounding this error for details.", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Enable Completed Download Handling if possible", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling", @@ -959,12 +1077,26 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Creates a .plexmatch file in the series folder", "MetadataProvidedBy": "Metadata is provided by {provider}", "MetadataSettings": "Metadata Settings", + "MetadataSettingsEpisodeImages": "Episode Images", + "MetadataSettingsEpisodeMetadata": "Episode Metadata", + "MetadataSettingsEpisodeMetadataImageThumbs": "Episode Metadata Image Thumbs", + "MetadataSettingsSeasonImages": "Season Images", + "MetadataSettingsSeriesImages": "Series Images", + "MetadataSettingsSeriesMetadata": "Series Metadata", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Series Metadata Episode Guide", + "MetadataSettingsSeriesMetadataUrl": "Series Metadata URL", "MetadataSettingsSeriesSummary": "Create metadata files when episodes are imported or series are refreshed", "MetadataSource": "Metadata Source", "MetadataSourceSettings": "Metadata Source Settings", "MetadataSourceSettingsSeriesSummary": "Information on where {appName} gets series and episode information", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Include image thumb tags in .nfo (Requires 'Episode Metadata')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Include JSON formatted episode guide element in tvshow.nfo (Requires 'Series Metadata')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo with full series metadata", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Include TheTVDB show URL in tvshow.nfo (can be combined with 'Series Metadata')", "MidseasonFinale": "Midseason Finale", "Min": "Min", "MinimumAge": "Minimum Age", diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index cd088677e..f6fc874f1 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -228,10 +228,15 @@ namespace Sonarr.Http.ClientSchema if (attrib != null) { + var label = attrib.Label.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(attrib.Label, + GetTokens(selectOptions, attrib.Label, TokenField.Label)) + : attrib.Label; + return new SelectOption { Value = value, - Name = attrib.Label ?? name, + Name = label ?? name, Order = attrib.Order, Hint = attrib.Hint ?? $"({value})" }; From 75bb34afaac575de48bcf069d617aa4bd1779054 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 19 Jan 2024 19:26:38 -0800 Subject: [PATCH 042/762] Bump version to 4.0.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f901eddc9..14f77ea55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ env: FRAMEWORK: net6.0 BRANCH: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.0 + VERSION: 4.0.1 jobs: backend: From ec40bc6eea1eb282cb804b8dd5461bf5ade332e9 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Fri, 19 Jan 2024 23:30:24 -0600 Subject: [PATCH 043/762] Improve Release Title Custom Format debugging Towards #5598 --- .../CustomFormats/CustomFormatCalculationService.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index e344c355f..1840d087c 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Blocklisting; using NzbDrone.Core.History; @@ -23,10 +24,12 @@ namespace NzbDrone.Core.CustomFormats public class CustomFormatCalculationService : ICustomFormatCalculationService { private readonly ICustomFormatService _formatService; + private readonly Logger _logger; - public CustomFormatCalculationService(ICustomFormatService formatService) + public CustomFormatCalculationService(ICustomFormatService formatService, Logger logger) { _formatService = formatService; + _logger = logger; } public List ParseCustomFormat(RemoteEpisode remoteEpisode, long size) @@ -153,20 +156,23 @@ namespace NzbDrone.Core.CustomFormats return matches.OrderBy(x => x.Name).ToList(); } - private static List ParseCustomFormat(EpisodeFile episodeFile, Series series, List allCustomFormats) + private List ParseCustomFormat(EpisodeFile episodeFile, Series series, List allCustomFormats) { var releaseTitle = string.Empty; if (episodeFile.SceneName.IsNotNullOrWhiteSpace()) { + _logger.Trace("Using scene name for release title: {0}", episodeFile.SceneName); releaseTitle = episodeFile.SceneName; } else if (episodeFile.OriginalFilePath.IsNotNullOrWhiteSpace()) { + _logger.Trace("Using original file path for release title: {0}", Path.GetFileName(episodeFile.OriginalFilePath)); releaseTitle = Path.GetFileName(episodeFile.OriginalFilePath); } else if (episodeFile.RelativePath.IsNotNullOrWhiteSpace()) { + _logger.Trace("Using relative path for release title: {0}", Path.GetFileName(episodeFile.RelativePath)); releaseTitle = Path.GetFileName(episodeFile.RelativePath); } From d336aaf3f04136471970155b5a7cc876770c64ff Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 19 Jan 2024 17:27:29 -0800 Subject: [PATCH 044/762] Fixed: Don't clone indexer API Key Closes #6265 --- frontend/src/Store/Actions/Settings/indexers.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index 10a2c2808..55e08a5c0 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -149,7 +149,13 @@ export default { delete selectedSchema.name; selectedSchema.fields = selectedSchema.fields.map((field) => { - return { ...field }; + const newField = { ...field }; + + if (newField.privacy === 'apiKey' || newField.privacy === 'password') { + newField.value = ''; + } + + return newField; }); newState.selectedSchema = selectedSchema; From ae96ebca5704bfb982c53cdbce5f87465f86c42a Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 20 Jan 2024 05:29:09 +0000 Subject: [PATCH 045/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Bastián Quezada Co-authored-by: Blair Noctis Co-authored-by: Dani Talens Co-authored-by: Deleted User Co-authored-by: Havok Dan Co-authored-by: Julian Baquero Co-authored-by: Koch Norbert Co-authored-by: MaddionMax Co-authored-by: Weblate Co-authored-by: brn Co-authored-by: resi23 Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 144 +++++++++++++++--- src/NzbDrone.Core/Localization/Core/de.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 53 ++++++- src/NzbDrone.Core/Localization/Core/hu.json | 41 +++-- src/NzbDrone.Core/Localization/Core/pl.json | 5 +- .../Localization/Core/pt_BR.json | 139 ++++++++++++++++- src/NzbDrone.Core/Localization/Core/tr.json | 8 +- .../Localization/Core/zh_CN.json | 4 +- 8 files changed, 352 insertions(+), 44 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 0c878ceb3..51bb50024 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -26,7 +26,7 @@ "AfterManualRefresh": "Després de l'actualització manual", "AnalyseVideoFiles": "Analitza els fitxers de vídeo", "Analytics": "Anàlisi", - "AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per prioritzar les funcions i les correccions d'errors.", + "AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per a prioritzar les funcions i les correccions d'errors.", "AuthenticationRequiredHelpText": "Canvia per a quines sol·licituds cal autenticar. No canvieu tret que entengueu els riscos.", "BypassDelayIfAboveCustomFormatScoreHelpText": "Habiliteu l'omissió quan la versió tingui una puntuació superior a la puntuació mínima per al format personalitzat", "FailedToUpdateSettings": "No s'ha pogut actualitzar la configuració", @@ -58,7 +58,7 @@ "EditConditionImplementation": "Edita la condició - {implementationName}", "EnableProfile": "Activa el perfil", "EnableInteractiveSearch": "Activa la cerca interactiva", - "FullColorEventsHelpText": "Estil alterat per pintar tot l'esdeveniment amb el color d'estat, en lloc de només la vora esquerra. No s'aplica a l'Agenda", + "FullColorEventsHelpText": "Estil alterat per a pintar tot l'esdeveniment amb el color d'estat, en lloc de només la vora esquerra. No s'aplica a l'Agenda", "HideAdvanced": "Amaga avançat", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Activa la gestió de baixades completades si és possible", "InteractiveImportNoQuality": "Cal triar la qualitat per a cada fitxer seleccionat", @@ -121,7 +121,7 @@ "DownloadIgnored": "Baixa ignorada", "PreviousAiring": "Emissió prèvia", "Priority": "Prioritat", - "HealthMessagesInfoBox": "Podeu trobar més informació sobre la causa d'aquests missatges de comprovació de salut fent clic a l'enllaç wiki (icona del llibre) al final de la fila o consultant els vostres [registres]({link}). Si teniu problemes per interpretar aquests missatges, podeu posar-vos en contacte amb el nostre suport als enllaços següents.", + "HealthMessagesInfoBox": "Podeu trobar més informació sobre la causa d'aquests missatges de comprovació de salut fent clic a l'enllaç wiki (icona del llibre) al final de la fila o consultant els vostres [registres]({link}). Si teniu problemes per a interpretar aquests missatges, podeu posar-vos en contacte amb el nostre suport als enllaços següents.", "AddListExclusion": "Afegeix una llista d'exclusió", "AddNewSeries": "Afegeix una sèrie nova", "AddNewSeriesError": "No s'han pogut carregar els resultats de la cerca, torneu-ho a provar.", @@ -170,11 +170,11 @@ "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.", "AppUpdated": "{appName} Actualitzada", "ApplyChanges": "Aplica els canvis", - "AppDataLocationHealthCheckMessage": "No es podrà actualitzar per evitar que s'eliminin AppData a l'actualització", + "AppDataLocationHealthCheckMessage": "No es podrà actualitzar per a evitar que s'eliminin AppData a l'actualització", "AnimeEpisodeFormat": "Format d'episodi d'Anime", "AuthenticationRequiredUsernameHelpTextWarning": "Introduïu un nom d'usuari nou", - "AuthenticationRequiredWarning": "Per evitar l'accés remot sense autenticació, ara {appName} requereix que l'autenticació estigui activada. Opcionalment, podeu desactivar l'autenticació des d'adreces locals.", - "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuació mínima de format personalitzada necessaria per evitar el retard del protocol preferit", + "AuthenticationRequiredWarning": "Per a evitar l'accés remot sense autenticació, ara {appName} requereix que l'autenticació estigui activada. Opcionalment, podeu desactivar l'autenticació des d'adreces locals.", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuació mínima de format personalitzada necessaria per a evitar el retard del protocol preferit", "ApplyTagsHelpTextHowToApplyImportLists": "Com aplicar etiquetes a les llistes d'importació seleccionades", "ApplyTagsHelpTextHowToApplyIndexers": "Com aplicar etiquetes als indexadors seleccionats", "CalendarOptions": "Opcions de calendari", @@ -184,8 +184,8 @@ "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.", - "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 evitar problemes d'importació.", - "HiddenClickToShow": "Amagat, feu clic per mostrar", + "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", "IndexerJackettAllHealthCheckMessage": "Indexadors que utilitzen l'extrem \"tot\" no compatible amb Jackett: {indexerNames}", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadors no disponibles a causa d'errors durant més de 6 hores: {indexerNames}", @@ -203,8 +203,8 @@ "FormatShortTimeSpanHours": "{hours} hora(es)", "FormatShortTimeSpanMinutes": "{minutes} minut(s)", "FormatShortTimeSpanSeconds": "{seconds} segon(s)", - "AutoRedownloadFailed": "No s'ha pogut tornar a baixar", - "AutoRedownloadFailedFromInteractiveSearch": "No s'ha pogut tornar a baixar des de la cerca interactiva", + "AutoRedownloadFailed": "Tornar a baixar les baixades fallades", + "AutoRedownloadFailedFromInteractiveSearch": "Tornar a baixar baixades fallades des de la cerca interactiva", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Cerqueu i intenteu baixar automàticament una versió diferent quan es trobi una versió fallida a la cerca interactiva", "DeleteCondition": "Suprimeix la condició", "EditSelectedDownloadClients": "Editeu els clients de descàrrega seleccionats", @@ -236,7 +236,7 @@ "BypassDelayIfHighestQuality": "Bypass si és de màxima qualitat", "BindAddressHelpText": "Adreça IP vàlida, localhost o '*' per a totes les interfícies", "BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData de {appName}", - "BranchUpdate": "Branca que s'utilitza per actualitzar {appName}", + "BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}", "CalendarLoadError": "No es pot carregar el calendari", "AudioInfo": "Informació d'àudio", "ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes", @@ -246,7 +246,7 @@ "Blocklist": "Llista de bloquejats", "Calendar": "Calendari", "ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent", - "AptUpdater": "Utilitzeu apt per instal·lar l'actualització", + "AptUpdater": "Utilitzeu apt per a instal·lar l'actualització", "BackupNow": "Fes ara la còpia de seguretat", "AppDataDirectory": "Directori AppData", "Authentication": "Autenticació", @@ -256,9 +256,9 @@ "ApplicationURL": "URL de l'aplicació", "ApplyTags": "Aplica etiquetes", "ApplyTagsHelpTextHowToApplySeries": "Com aplicar etiquetes a les sèries seleccionades", - "ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per esborrar totes les etiquetes)", + "ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per a esborrar totes les etiquetes)", "AuthForm": "Formularis (pàgina d'inici de sessió)", - "AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per accedir a {appName}", + "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.", @@ -307,8 +307,8 @@ "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.", - "ClickToChangeQuality": "Feu clic per canviar la qualitat", - "ClickToChangeSeries": "Feu clic per canviar la sèrie", + "ClickToChangeQuality": "Feu clic per a canviar la qualitat", + "ClickToChangeSeries": "Feu clic per a canviar la sèrie", "CollapseMultipleEpisodes": "Contreu múltiples episodis", "Condition": "Condició", "ColonReplacement": "Substitució de dos punts", @@ -363,14 +363,14 @@ "Info": "Informació", "InstanceName": "Nom de la instància", "TagDetails": "Detalls de l'etiqueta - {label}", - "ClickToChangeSeason": "Feu clic per canviar la temporada", + "ClickToChangeSeason": "Feu clic per a canviar la temporada", "Category": "Categoria", "CertificateValidation": "Validació del certificat", "CertificateValidationHelpText": "Canvieu la validació estricta de la certificació HTTPS. No canvieu tret que entengueu els riscos.", "Certification": "Certificació", - "CheckDownloadClientForDetails": "Consulteu el client de descàrrega per obtenir més detalls", + "CheckDownloadClientForDetails": "Consulteu el client de descàrrega per a obtenir més detalls", "ChmodFolderHelpText": "Octal, aplicat durant la importació/reanomenat de carpetes i fitxers multimèdia (sense bits d'execució)", - "ClickToChangeReleaseGroup": "Feu clic per canviar el grup de llançaments", + "ClickToChangeReleaseGroup": "Feu clic per a canviar el grup de llançaments", "CloneIndexer": "Clona l'indexador", "Close": "Tanca", "CollapseAll": "Contreure-ho tot", @@ -378,7 +378,7 @@ "Connect": "Connecta", "ConnectionLost": "Connexió perduda", "ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.", - "ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per restaurar la funcionalitat.", + "ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.", "Continuing": "Continua", "ContinuingSeriesDescription": "S'esperen més episodis o altra temporada", "CountSelectedFiles": "{selectedCount} fitxers seleccionats", @@ -386,14 +386,14 @@ "CreateEmptySeriesFoldersHelpText": "Crea carpetes de sèrie que falten durant l'exploració del disc", "CreateGroup": "Crea un grup", "DeleteIndexerMessageText": "Esteu segur que voleu suprimir l'indexador '{name}'?", - "CustomFormatHelpText": "{appName} puntua cada llançament utilitzant la suma de puntuacions per fer coincidir els formats personalitzats. Si un nou llançament millorés la puntuació, amb la mateixa o millor qualitat, llavors {appName} l'afegirà.", + "CustomFormatHelpText": "{appName} puntua cada llançament utilitzant la suma de puntuacions per a fer coincidir els formats personalitzats. Si un nou llançament millorés la puntuació, amb la mateixa o millor qualitat, llavors {appName} l'afegirà.", "CustomFormatScore": "Puntuació de format personalitzat", "CustomFormats": "Formats personalitzats", "CustomFormatsLoadError": "No es poden carregar els formats personalitzats", "CustomFormatsSettings": "Configuració de formats personalitzats", "CustomFormatsSettingsSummary": "Formats i configuracions personalitzades", "DeleteNotificationMessageText": "Esteu segur que voleu suprimir la notificació '{name}'?", - "DeletedReasonUpgrade": "S'ha suprimit el fitxer per importar una versió millorada", + "DeletedReasonUpgrade": "S'ha suprimit el fitxer per a importar una versió millorada", "InstallLatest": "Instal·la l'últim", "WouldYouLikeToRestoreBackup": "Voleu restaurar la còpia de seguretat '{name}'?", "FormatAgeMinutes": "minuts", @@ -415,8 +415,8 @@ "RemoveSelectedItemQueueMessageText": "Esteu segur que voleu eliminar 1 element de la cua?", "ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?", "TheLogLevelDefault": "El nivell de registre per defecte és \"Info\" i es pot canviar a [Configuració general](/configuració/general)", - "ClickToChangeLanguage": "Feu clic per canviar l'idioma", - "ClickToChangeEpisode": "Feu clic per canviar l'episodi", + "ClickToChangeLanguage": "Feu clic per a canviar l'idioma", + "ClickToChangeEpisode": "Feu clic per a canviar l'episodi", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Clau de configuració per a la solució d'emmagatzematge persistent. Deixeu-lo en blanc si s'utilitzen URL sense estat.", "FormatAgeMinute": "minut", "NotificationsAppriseSettingsStatelessUrlsHelpText": "Un o més URL separats per comes que identifiquen on s'ha d'enviar la notificació. Deixeu-lo en blanc si s'utilitza l'emmagatzematge persistent.", @@ -492,5 +492,99 @@ "Series": "Sèries", "Updates": "Actualitzacions", "No": "No", - "Result": "Resultat" + "Result": "Resultat", + "Fixed": "Corregit", + "Forums": "Fòrums", + "IndexerDownloadClientHelpText": "Especifiqueu quin client de baixada s'utilitza per a capturar des d'aquest indexador", + "IndexerPriorityHelpText": "Prioritat de l'indexador d'1 (la més alta) a 50 (la més baixa). Per defecte: 25. S'utilitza quan es capturen llançaments com a desempat per a versions iguals, {appName} encara utilitzarà tots els indexadors habilitats per a la sincronització i la cerca RSS", + "Progress": "Progrés", + "RetentionHelpText": "Només Usenet: establiu-lo a zero per a establir una retenció il·limitada", + "Started": "Començat", + "Wiki": "Wiki", + "Discord": "Discord", + "Season": "Temporada", + "TorrentDelayHelpText": "Retard en minuts per a esperar abans de capturar un torrent", + "Deleted": "S'ha suprimit", + "MaximumSizeHelpText": "Mida màxima per a una versió que es pot capturar en MB. Establiu a zero per a establir-lo en il·limitat", + "Interval": "Interval", + "Proper": "Proper", + "Donations": "Donacions", + "From": "Des de", + "Formats": "Formats", + "Location": "Ubicació", + "Logs": "Registres", + "MinimumAgeHelpText": "Només Usenet: edat mínima en minuts dels NZB abans de ser capturats. Utilitzeu-ho per a donar temps a les noves versions per propagar-se al vostre proveïdor d'usenet.", + "New": "Nou", + "Restore": "Restaura", + "WaitingToImport": "S’està esperant per a importar", + "Seasons": "Temporades", + "Rating": "Valoració", + "Save": "Desa", + "Folder": "Carpeta", + "Scheduled": "Programat", + "Status": "Estat", + "TaskUserAgentTooltip": "Agent d'usuari proporcionat per l'aplicació per a fer peticions a l'API", + "Uptime": "Temps de funcionament", + "Version": "Versió", + "WaitingToProcess": "S’està esperant per a processar", + "Genres": "Gèneres", + "Grabbed": "Capturat", + "Protocol": "Protocol", + "Repack": "Tornat a empaquetar", + "Download": "Baixa", + "DotNetVersion": ".NET", + "Duration": "Durada", + "Restart": "Reinicia", + "Size": "Mida", + "Time": "Temps", + "DownloadPropersAndRepacksHelpTextWarning": "Utilitzeu formats personalitzats per a actualitzacions automàtiques a Propers/Repacks", + "Reset": "Restableix", + "Exception": "Excepció", + "MultiSeason": "Multi-temporada", + "Manual": "Manual", + "Renamed": "Reanomenat", + "ShownClickToHide": "Es mostra, feu clic per a amagar", + "Unavailable": "No disponible", + "Real": "Real", + "Reload": "Torna a carregar", + "Seeders": "Llavors", + "Title": "Títol", + "Type": "Tipus", + "Twitter": "Twitter", + "Negate": "Negació", + "Options": "Opcions", + "Peers": "Parells", + "Release": "Llançament", + "Queued": "En cua", + "DeleteSelectedEpisodeFiles": "Suprimeix els fitxers d'episodis seleccionats", + "Docker": "Docker", + "Source": "Font", + "Year": "Any", + "Episodes": "Episodis", + "Failed": "S'ha produït un error", + "Health": "Salut", + "Duplicate": "Duplica", + "Episode": "Episodi", + "ExternalUpdater": "{appName} està configurat per a utilitzar un mecanisme d'actualització extern", + "Error": "Error", + "Filename": "Nom de fitxer", + "IRC": "IRC", + "Ignored": "Ignorat", + "Imported": "Importat", + "Indexer": "Indexador", + "Message": "Missatge", + "OrganizeNothingToRename": "Èxit! La feina està acabada, no hi ha fitxers per a canviar el nom.", + "Refresh": "Actualitza", + "Runtime": "Temps d'execució", + "Special": "Especial", + "Warn": "Avís", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El client de baixada {downloadClientName} està configurat per a eliminar les baixades completades. Això pot provocar que les baixades s'eliminin del vostre client abans que {appName} pugui importar-les.", + "Mode": "Mode", + "EnableColorImpairedModeHelpText": "Estil alterat per a permetre als usuaris amb problemes de color distingir millor la informació codificada per colors", + "EnableAutomaticAdd": "Activa la captura automàtica", + "EnableSsl": "Activa SSL", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desactiva l'ordenació per data", + "EnableRss": "Activa RSS", + "EnableColorImpairedMode": "Activa el mode amb alteracions del color", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "La cua no està habilitada" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index ef2624f64..b0383481a 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -248,7 +248,7 @@ "TheTvdb": "TheTVDB", "TvdbId": "TVDB-ID", "UpdateAll": "Alle aktualisieren", - "UpdateSelected": "Update ausgewählt", + "UpdateSelected": "Auswahl aktualisieren", "ChmodFolderHelpTextWarning": "Dies funktioniert nur, wenn der Benutzer, der {appName} ausführt, der Eigentümer der Datei ist. Es ist besser, sicherzustellen, dass der Download-Client die Berechtigungen richtig festlegt.", "ChownGroupHelpTextWarning": "Dies funktioniert nur, wenn der Benutzer, der {appName} ausführt, der Eigentümer der Datei ist. Es ist besser sicherzustellen, dass der Download-Client dieselbe Gruppe wie {appName} verwendet.", "ClearBlocklistMessageText": "Sind Sie sicher, dass Sie alle Elemente aus der Sperrliste löschen möchten?", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 2c5526ce1..fbbff560d 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -61,7 +61,7 @@ "Dates": "Fechas", "Debug": "Debug", "Date": "Fecha", - "DeleteTag": "EliminarEtiqueta", + "DeleteTag": "Eliminar Etiqueta", "Duplicate": "Duplicar", "Error": "Error", "Episodes": "Episodios", @@ -335,7 +335,7 @@ "ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento", "ClickToChangeSeries": "Click para cambiar la serie", "ColonReplacement": "Reemplazar dos puntos", - "CollapseMultipleEpisodes": "Cerrar episodios multiples", + "CollapseMultipleEpisodes": "Colapsar episodios multiples", "CompletedDownloadHandling": "Manipulación de descargas completas", "ColonReplacementFormatHelpText": "Cambia la forma en que {appName} reemplaza los dos puntos", "Connect": "Conectar", @@ -483,5 +483,52 @@ "DefaultDelayProfileSeries": "Este es el perfil por defecto. Aplica a todas las series que no tienen un perfil explícito.", "DelayProfileSeriesTagsHelpText": "Aplica a series con al menos una etiqueta coincidente", "DeleteCustomFormat": "Eliminar Formato Personalizado", - "BlackholeWatchFolder": "Monitorizar Carpeta" + "BlackholeWatchFolder": "Monitorizar Carpeta", + "DeleteEmptyFolders": "Eliminar directorios vacíos", + "DeleteNotification": "Borrar Notificacion", + "DeleteReleaseProfile": "Borrar perfil de estreno", + "Details": "Detalles", + "DeleteDownloadClient": "Borrar cliente de descarga", + "DeleteSelectedSeries": "Eliminar serie seleccionada", + "DestinationPath": "Ruta de destino", + "DeleteImportListExclusion": "Eliminar exclusión de listas de importación.", + "DeleteSeriesFolderConfirmation": "El directorio de series '{path}' y todos sus contenidos seran eliminados.", + "DeleteDelayProfileMessageText": "¿Está seguro de que desea borrar este perfil de retraso?", + "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", + "DeleteImportListExclusionMessageText": "¿Está seguro de que desea eliminar esta exclusión de la lista de importación?", + "DeleteQualityProfile": "Borrar perfil de calidad", + "DeleteReleaseProfileMessageText": "Esta seguro que quiere borrar este perfil de estreno? '{name}'?", + "DeleteRemotePathMapping": "Borrar mapeo de ruta remota", + "DeleteSelectedEpisodeFiles": "Borrar los archivos de episodios seleccionados", + "DeleteSelectedEpisodeFilesHelpText": "Esta seguro que desea borrar los archivos de episodios seleccionados?", + "DeleteSeriesFolder": "Eliminar directorio de series", + "DeleteSeriesFolderCountWithFilesConfirmation": "Esta seguro que desea eliminar '{count}' seleccionadas y sus contenidos?", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} archivos de episodios que suman {size}", + "DeleteSeriesFolderHelpText": "Eliminar el directorio de series y sus contenidos", + "DeleteSeriesFoldersHelpText": "Eliminar los directorios de series y sus contenidos", + "DeleteSeriesModalHeader": "Borrar - {title}", + "DeleteSpecification": "Borrar especificacion", + "Directory": "Directorio", + "Disabled": "Deshabilitado", + "Discord": "Discord", + "DiskSpace": "Espacio en Disco", + "DeleteSpecificationHelpText": "Esta seguro que desea borrar la especificacion '{name}'?", + "DeleteSelectedIndexers": "Borrar indexer(s)", + "DeleteIndexer": "Borrar Indexer", + "DeleteSelectedDownloadClients": "Borrar gestor de descarga(s)", + "DeleteSeriesFolderCountConfirmation": "Esta seguro que desea eliminar '{count}' series seleccionadas?", + "DeleteSeriesFolders": "Eliminar directorios de series", + "DeletedSeriesDescription": "Serie fue eliminada de TheTVDB", + "Destination": "Destino", + "DestinationRelativePath": "Ruta Relativa de Destino", + "DetailedProgressBar": "Barra de Progreso Detallada", + "DetailedProgressBarHelpText": "Mostrar tecto en la barra de progreso", + "DeletedReasonEpisodeMissingFromDisk": "{appName} no pudo encontrar el archivo en disco entonces el archivo fue desvinculado del episodio en la base de datos", + "DeleteEmptySeriesFoldersHelpText": "Eliminar directorios vacíos de series y temporadas durante el escaneo del disco y cuando se eliminen archivos correspondientes a episodios.", + "DeleteEpisodeFile": "Eliminar archivo de episodio", + "DeleteEpisodeFileMessage": "¿Está seguro de que desea borrar '{path}'?", + "DeleteEpisodeFromDisk": "Eliminar episodio del disco", + "DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series", + "DoNotPrefer": "No preferir", + "DoNotUpgradeAutomatically": "No actualizar automáticamente" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 9028cd1f6..077965714 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -19,14 +19,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 {length} 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 {hossz} 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.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nem lehet kommunikálni a {downloadClientName} -val", "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltési kliens elérhetetlen meghibásodások miatt", "EditSelectedDownloadClients": "Kiválasztott letöltési kliensek szerkesztése", "EditSelectedImportLists": "Kiválasztott import listák szerkesztése", @@ -96,7 +96,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 '{startupFolder}' az App Translocation mappában található.", + "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ó.", "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.", @@ -160,7 +160,7 @@ "AddCondition": "Feltétel hozzáadása", "AddConditionError": "Nem sikerült új feltételt hozzáadni, próbálkozzon újra.", "AddCustomFormat": "Egyéni formátum hozzáadása", - "AddCustomFormatError": "Nem sikerült új egyéni formátum hozzáadása, próbálkozzon újra", + "AddCustomFormatError": "Nem sikerült új egyéni formátum hozzáadása, próbálkozzon újra.", "AddConnection": "Csatlakozás hozzáadása", "AddDelayProfile": "Késleltetési profil hozzáadása", "AddDownloadClient": "Letöltési kliens hozzáadása", @@ -212,7 +212,7 @@ "AllTitles": "Minden címke", "Indexers": "Indexerek", "Mode": "Mód", - "Quality": "minőség", + "Quality": "Minőség", "AddingTag": "Címke hozzáadása", "Apply": "Alkamaz", "No": "Nem", @@ -232,10 +232,10 @@ "Interval": "Intervallum", "Proper": "Megfelelő", "Blocklist": "Feketelista", - "Backup": "biztonsági mentés", + "Backup": "Biztonsági mentés", "AddNew": "Új hozzáadása", "AddANewPath": "Új útvonal hozzáadása", - "AddConditionImplementation": "Feltétel hozzáadása –{implementationName}", + "AddConditionImplementation": "Feltétel hozzáadása –{megvalósítás név}", "AddConnectionImplementation": "Csatlakozás hozzáadása - {implementationName}", "AddCustomFilter": "Egyéni szűrő hozzáadása", "AddDownloadClientImplementation": "Letöltési kliens hozzáadása – {implementationName}", @@ -272,13 +272,13 @@ "Branch": "Ágazat", "Series": "Sorozat", "ClearBlocklist": "Letiltási lista törlése", - "Edit": "szerkeszt", + "Edit": "Szerkeszt", "Duration": "Időtartam", "History": "Előzmény", "Missing": "Hiányzó", "Message": "Üzenet", "AlreadyInYourLibrary": "Már a könyvtárban", - "AnEpisodeIsDownloading": "epizód letöltése folyamatban van", + "AnEpisodeIsDownloading": "Epizód letöltése folyamatban", "Options": "Lehetőségek", "Agenda": "Teendők", "Result": "Eredmény", @@ -393,5 +393,26 @@ "Import": "Importálás", "ImportErrors": "Importálási hiba", "DefaultNameCopiedProfile": "{name} - Másolat", - "DefaultNameCopiedSpecification": "{name} - Másolat" + "DefaultNameCopiedSpecification": "{name} - Másolat", + "File": "Fájl", + "AuthenticationRequiredWarning": "A hitelesítés nélküli távoli hozzáférés megakadályozása érdekében a(z) {appName} alkalmazásnak engedélyeznie kell a hitelesítést. Opcionálisan letilthatja a helyi címekről történő hitelesítést.", + "DeleteRemotePathMapping": "Távoli Elérési Útvonal Módosítása", + "NoChange": "Nincs változás", + "TestParsing": "Tesztelemzés", + "NoDownloadClientsFound": "Nem találhatók letöltő kliensek", + "RemoveCompleted": "Eltávolítás kész", + "RemoveFailed": "Eltávolítás nem sikerült", + "UpdateMechanismHelpText": "Használja a {appName} beépített frissítőjét vagy szkriptjét", + "BindAddress": "Kötési cím", + "ManageLists": "Listák kezelése", + "Database": "Adatbázis", + "ManageImportLists": "Importálási listák kezelése", + "ManageIndexers": "Indexelők kezelése", + "NoImportListsFound": "Nem található importálási lista", + "NoIndexersFound": "Nem található indexelő", + "VideoCodec": "Videó Kodek", + "AddRootFolderError": "Hozzáadás a gyökérmappához", + "YesCancel": "Igen, elvet", + "Wanted": "Keresett", + "Warn": "Figyelmeztetés" } diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index c6d28aff1..416bb34ac 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -39,5 +39,8 @@ "AddIndexer": "Dodaj indekser", "AddIndexerError": "Nie można dodać nowego indeksatora, spróbuj ponownie.", "Actions": "Akcje", - "AddNewSeriesError": "Nie udało się załadować wyników wyszukiwania, spróbuj ponownie." + "AddNewSeriesError": "Nie udało się załadować wyników wyszukiwania, spróbuj ponownie.", + "AddConditionImplementation": "Dodaj condition - {implementationName}", + "AddConnectionImplementation": "Dodaj Connection - {implementationName}", + "AddDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 474e6f0d3..99a311934 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -338,7 +338,7 @@ "Progress": "Progresso", "Rating": "Avaliação", "RelativePath": "Caminho relativo", - "ReleaseGroups": "Grupos de lançamentos", + "ReleaseGroups": "Grupos do Lançamento", "Renamed": "Renomeado", "RootFolderPath": "Caminho da Pasta Raiz", "Runtime": "Duração", @@ -1874,5 +1874,140 @@ "NotificationsSynologySettingsUpdateLibraryHelpText": "Chame synoindex no localhost para atualizar um arquivo de biblioteca", "NotificationsTelegramSettingsSendSilentlyHelpText": "Envia a mensagem silenciosamente. Os usuários receberão uma notificação sem som", "EpisodeFileMissingTooltip": "Arquivo do episódio ausente", - "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2" + "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2", + "DownloadClientPriorityHelpText": "Prioridade do Cliente de Download de 1 (mais alta) a 50 (mais baixa). Padrão: 1. Round-Robin é usado para clientes com a mesma prioridade.", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar Torrent com Hashes na Lista de Bloqueio Enquanto Capturando", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "se um torrent for bloqueado por hash, ele pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", + "ImportListsSimklSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Séries Mais Assistidas por Semana", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Séries Mais Assistidas por Ano", + "ImportListsTraktSettingsUserListTypeWatched": "Lista de Assistido pelo Usuário", + "ImportListsTraktSettingsWatchedListFilter": "Filtrar Lista de Assistido", + "ImportListsValidationUnableToConnectException": "Não foi possível conectar-se à lista de importação: {exceptionMessage}. Verifique o log em torno desse erro para obter detalhes.", + "AutoTaggingSpecificationGenre": "Gênero(s)", + "AutoTaggingSpecificationMaximumYear": "Ano Máximo", + "AutoTaggingSpecificationMinimumYear": "Ano Mínimo", + "AutoTaggingSpecificationOriginalLanguage": "Idioma", + "AutoTaggingSpecificationQualityProfile": "Perfil de Qualidade", + "AutoTaggingSpecificationRootFolder": "Pasta Raiz", + "AutoTaggingSpecificationSeriesType": "Tipo de Série", + "AutoTaggingSpecificationStatus": "Estado", + "CustomFormatsSpecificationLanguage": "Idioma", + "CustomFormatsSpecificationMaximumSize": "Tamanho Máximo", + "CustomFormatsSpecificationMaximumSizeHelpText": "O lançamento deve ser menor ou igual a este tamanho", + "CustomFormatsSpecificationMinimumSize": "Tamanho Mínimo", + "CustomFormatsSpecificationMinimumSizeHelpText": "O lançamento deve ser maior que esse tamanho", + "CustomFormatsSpecificationRegularExpression": "Idioma", + "CustomFormatsSpecificationRegularExpressionHelpText": "Regex dos Formatos Personalizados são Insensíveis à diferença de maiúscula e minúscula", + "CustomFormatsSpecificationReleaseGroup": "Grupo do Lançamento", + "CustomFormatsSpecificationResolution": "Resolução", + "CustomFormatsSpecificationSource": "Fonte", + "ImportListsAniListSettingsAuthenticateWithAniList": "Autenticar com AniList", + "ImportListsAniListSettingsImportCancelled": "Importação Cancelada", + "ImportListsAniListSettingsImportCancelledHelpText": "Mídia: Série foi cancelada", + "ImportListsAniListSettingsImportCompleted": "Importação Concluída", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: Assistindo Concluídas", + "ImportListsAniListSettingsImportDropped": "Importação Descartada", + "ImportListsAniListSettingsImportDroppedHelpText": "Lista: Descartada", + "ImportListsAniListSettingsImportFinished": "Importação Concluída", + "ImportListsAniListSettingsImportFinishedHelpText": "Mídia: todos os episódios foram ao ar", + "ImportListsAniListSettingsImportHiatus": "Importação em Hiato", + "ImportListsAniListSettingsImportHiatusHelpText": "Mídia: Série em Hiato", + "ImportListsAniListSettingsImportNotYetReleased": "Importação do Ainda Não Lançado", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Mídia: A exibição ainda não começou", + "ImportListsAniListSettingsImportPaused": "Importação Pausada", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: Em espera", + "ImportListsAniListSettingsImportPlanning": "Importação em Planejamento", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: Planejando Assistir", + "ImportListsAniListSettingsImportReleasing": "Importar em Lançamento", + "ImportListsAniListSettingsImportReleasingHelpText": "Mídia: atualmente exibindo novos episódios", + "ImportListsAniListSettingsImportRepeating": "Importar Repetição", + "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: Atualmente Assistindo Novamente", + "ImportListsAniListSettingsImportWatchingHelpText": "Lista: Atualmente Assistindo", + "ImportListsAniListSettingsUsernameHelpText": "Nome de usuário da lista a ser importada", + "ImportListsCustomListSettingsName": "Lista Personalizada", + "ImportListsAniListSettingsImportWatching": "Importar Assistindo", + "ImportListsCustomListSettingsUrl": "URL da Lista", + "ImportListsCustomListSettingsUrlHelpText": "O URL da lista de séries", + "ImportListsCustomListValidationAuthenticationFailure": "Falha de Autenticação", + "ImportListsCustomListValidationConnectionError": "Não foi possível fazer a solicitação para esse URL. Código de status: {exceptionStatusCode}", + "ImportListsImdbSettingsListId": "ID da Lista", + "ImportListsImdbSettingsListIdHelpText": "ID da lista IMDb (por exemplo, ls12345678)", + "ImportListsPlexSettingsAuthenticateWithPlex": "Autenticar com Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Plex Para Assistir", + "ImportListsPlexSettingsWatchlistRSSName": "Plex Para Assistir RSS", + "ImportListsSettingsAccessToken": "Token de Acesso", + "ImportListsSettingsAuthUser": "Usuário de Autenticação", + "ImportListsSettingsExpires": "Expirar", + "ImportListsSettingsRefreshToken": "Atualizar Token", + "ImportListsSettingsRssUrl": "URL do RSS", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Autenticar com Simkl", + "ImportListsSimklSettingsListType": "Tipo de Lista", + "ImportListsSimklSettingsName": "Usuário do Simkl Para Assistir", + "ImportListsSimklSettingsShowType": "Tipo de Série", + "ImportListsSimklSettingsShowTypeHelpText": "Tipo de série do qual você deseja importar", + "ImportListsSimklSettingsUserListTypeCompleted": "Concluída", + "ImportListsSimklSettingsUserListTypeDropped": "Descartada", + "ImportListsSimklSettingsUserListTypeHold": "Parada", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Planeja Assistir", + "ImportListsSimklSettingsUserListTypeWatching": "Assistindo", + "ImportListsSonarrSettingsApiKeyHelpText": "Chave de API da instância {appName} da qual importar", + "ImportListsSonarrSettingsFullUrl": "URL Completa", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluindo a porta, da instância {appName} da qual importar", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Perfis de Qualidade da instância de origem para importação", + "ImportListsSonarrSettingsRootFoldersHelpText": "Pastas Raiz da instância de origem para importação", + "ImportListsSonarrSettingsTagsHelpText": "Etiquetas da instância de origem para importação", + "ImportListsSonarrValidationInvalidUrl": "A URL de {appName} é inválida. Está faltando uma base de URL?", + "ImportListsTraktSettingsAdditionalParameters": "Parâmetros Adicionais", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Parâmetros adicionais da API Trakt", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Autenticar com Trakt", + "ImportListsTraktSettingsGenres": "Gêneros", + "ImportListsTraktSettingsGenresHelpText": "Filtrar séries por Trakt Genre Slug (separado por vírgula) apenas para listas populares", + "ImportListsTraktSettingsLimit": "Limite", + "ImportListsTraktSettingsLimitHelpText": "Limite o número de séries para baixar", + "ImportListsTraktSettingsListName": "Nome da Lista", + "ImportListsTraktSettingsListNameHelpText": "Nome da lista para importação, a lista deve ser pública ou você deve ter acesso à lista", + "ImportListsTraktSettingsListType": "Tipo de Lista", + "ImportListsTraktSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Séries Antecipadas", + "ImportListsTraktSettingsPopularListTypePopularShows": "Séries Populares", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Séries Recomendadas de Todos os Tempos", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Séries Recomendadas por Mês", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Séries Recomendadas por Semana", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Séries Recomendadas por Ano", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Séries Mais Assistidas de Todos os Tempos", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Séries Mais Assistidas por Mês", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Séries Em Alta", + "ImportListsTraktSettingsPopularName": "Lista Popularidade do Trakt", + "ImportListsTraktSettingsRating": "Avaliação", + "ImportListsTraktSettingsRatingHelpText": "Filtrar séries por faixa de avaliação (0-100)", + "ImportListsTraktSettingsUserListName": "Usuário Trakt", + "ImportListsTraktSettingsUserListTypeCollection": "Lista de Coleção de Usuário", + "ImportListsTraktSettingsUserListTypeWatch": "Lista Para Assistir do Usuário", + "ImportListsTraktSettingsUserListUsernameHelpText": "Nome de usuário da lista a ser importada (deixe em branco para usar usuário de autenticação)", + "ImportListsTraktSettingsUsernameHelpText": "Nome de usuário da lista a ser importada", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Se o tipo de lista for Assistido, selecione o tipo de série que deseja importar", + "ImportListsTraktSettingsWatchedListSorting": "Ordenar Lista de Para Assistir", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Se o tipo de lista for Assistido, selecione a ordem de classificação da lista", + "ImportListsTraktSettingsWatchedListTypeAll": "Todas", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Assistido", + "ImportListsTraktSettingsWatchedListTypeInProgress": "Em Andamento", + "ImportListsTraktSettingsYears": "Anos", + "ImportListsTraktSettingsYearsHelpText": "Filtrar séries por ano ou intervalo de anos", + "ImportListsValidationInvalidApiKey": "A chave de API é inválida", + "ImportListsValidationTestFailed": "O teste foi abortado devido a um erro: {exceptionMessage}", + "MetadataPlexSettingsSeriesPlexMatchFile": "Arquivo de Correspondência da Série Plex", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Cria um arquivo .plexmatch na pasta da série", + "MetadataSettingsEpisodeImages": "Imagens do Episódio", + "MetadataSettingsEpisodeMetadata": "Metadados do Episódio", + "MetadataSettingsEpisodeMetadataImageThumbs": "Miniaturas de Imagens de Metadados de Episódios", + "MetadataSettingsSeasonImages": "Imagens da Temporada", + "MetadataSettingsSeriesImages": "Imagens da Série", + "MetadataSettingsSeriesMetadata": "Metadados da Série", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Guia de Episódios de Metadados da Série", + "MetadataSettingsSeriesMetadataUrl": "URL de Metadados da Série", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Incluir etiquetas de miniatura de imagem no nome do arquivo .nfo (requer 'Metadados de Episódio')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Incluir elemento de guia de episódios formatado em JSON em tvshow.nfo (requer 'Metadados da Série')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo com metadados da série completa", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 49785a41a..ef5f17a5a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -8,5 +8,11 @@ "EditConnectionImplementation": "Koşul Ekle - {implementationName}", "AddConnectionImplementation": "Koşul Ekle - {implementationName}", "AddIndexerImplementation": "Koşul Ekle - {implementationName}", - "EditIndexerImplementation": "Koşul Ekle - {implementationName}" + "EditIndexerImplementation": "Koşul Ekle - {implementationName}", + "AddToDownloadQueue": "İndirme sırasına ekle", + "AddedToDownloadQueue": "İndirme sırasına eklendi", + "AllTitles": "Tüm Filmler", + "AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları", + "Actions": "Eylemler", + "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 344a284be..add862d21 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1789,5 +1789,7 @@ "NotificationsPlexSettingsAuthenticateWithPlexTv": "使用 Plex.tv 验证身份", "NotificationsPlexValidationNoTvLibraryFound": "需要至少一个电视资源库", "NotificationsPushBulletSettingSenderId": "发送 ID", - "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置" + "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", + "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", + "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值" } From 3cf4d2907e32e81050f35cda042dcc2b4641d40d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 20 Jan 2024 13:37:25 +0200 Subject: [PATCH 046/762] Transpile logical assignment operators with babel --- frontend/babel.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/babel.config.js b/frontend/babel.config.js index 5c0d5ecdc..ade9f24a2 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -2,6 +2,8 @@ const loose = true; module.exports = { plugins: [ + '@babel/plugin-transform-logical-assignment-operators', + // Stage 1 '@babel/plugin-proposal-export-default-from', ['@babel/plugin-transform-optional-chaining', { loose }], From c0b30a50281c38a103c2f800064d0ec964fae6e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Jan 2024 10:36:16 -0800 Subject: [PATCH 047/762] Fixed: Series poster view on mobile devices Closes #6387 --- .../src/Series/Index/Posters/SeriesIndexPosters.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index 48e9674c0..bf1915761 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -190,11 +190,15 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { if (isSmallScreen) { const padding = bodyPaddingSmallScreen - 5; + const width = window.innerWidth - padding * 2; + const height = window.innerHeight; - setSize({ - width: window.innerWidth - padding * 2, - height: window.innerHeight, - }); + if (width !== size.width || height !== size.height) { + setSize({ + width, + height, + }); + } return; } From e66ba84fc0b5b120dd4e87f6b8ae1b3c038ee72b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Jan 2024 13:15:29 -0800 Subject: [PATCH 048/762] New: Log warning if less than 1 GB free space during update Closes #6385 --- src/NzbDrone.Core/Update/InstallUpdateService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index bb19c4ffe..569f36e47 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -107,6 +107,11 @@ namespace NzbDrone.Core.Update var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder(); + if (_diskProvider.GetTotalSize(updateSandboxFolder) < 1.Gigabytes()) + { + _logger.Warn("Temporary location '{0}' has less than 1 GB free space, Sonarr may not be able to update itself.", updateSandboxFolder); + } + var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); if (_diskProvider.FolderExists(updateSandboxFolder)) From 7be5732a3a6679120b0f01bd1eb1207194f57f5e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Jan 2024 15:18:26 -0800 Subject: [PATCH 049/762] New: Option to disable Email encryption Closes #6380 --- .../Migration/201_email_encryptionFixture.cs | 142 ++++++++++++++++++ .../EmailSettingsValidatorFixture.cs | 2 +- .../Migration/201_email_encryption.cs | 50 ++++++ src/NzbDrone.Core/Localization/Core/en.json | 4 +- .../Notifications/Email/Email.cs | 19 +-- .../Notifications/Email/EmailSettings.cs | 11 +- 6 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs new file mode 100644 index 000000000..7a6cee1ca --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Notifications.Email; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class email_encryptionFixture : MigrationTest + { + [Test] + public void should_convert_do_not_require_encryption_to_auto() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Mail Sonarr", + Implementation = "Email", + Tags = "[]", + Settings = new EmailSettings200 + { + Server = "smtp.gmail.com", + Port = 563, + To = new List { "dont@email.me" }, + RequireEncryption = false + }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred); + } + + [Test] + public void should_convert_require_encryption_to_always() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Mail Sonarr", + Implementation = "Email", + Tags = "[]", + Settings = new EmailSettings200 + { + Server = "smtp.gmail.com", + Port = 563, + To = new List { "dont@email.me" }, + RequireEncryption = true + }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always); + } + } + + public class NotificationDefinition201 + { + public int Id { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public EmailSettings201 Settings { get; set; } + public string Name { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool OnSeriesDelete { get; set; } + public bool OnEpisodeFileDelete { get; set; } + public bool OnEpisodeFileDeleteForUpgrade { get; set; } + public bool OnHealthIssue { get; set; } + public bool OnApplicationUpdate { get; set; } + public bool OnManualInteractionRequired { get; set; } + public bool OnSeriesAdd { get; set; } + public bool OnHealthRestored { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public bool SupportsOnSeriesDelete { get; set; } + public bool SupportsOnEpisodeFileDelete { get; set; } + public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; } + public bool SupportsOnHealthIssue { get; set; } + public bool IncludeHealthWarnings { get; set; } + public List Tags { get; set; } + } + + public class EmailSettings200 + { + public string Server { get; set; } + public int Port { get; set; } + public bool RequireEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public IEnumerable To { get; set; } + public IEnumerable Cc { get; set; } + public IEnumerable Bcc { get; set; } + } + + public class EmailSettings201 + { + public string Server { get; set; } + public int Port { get; set; } + public int UseEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public IEnumerable To { get; set; } + public IEnumerable Cc { get; set; } + public IEnumerable Bcc { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs index 35aa38fdf..68d5d5e2f 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.NotificationTests.EmailTests _emailSettings = Builder.CreateNew() .With(s => s.Server = "someserver") .With(s => s.Port = 567) - .With(s => s.RequireEncryption = true) + .With(s => s.UseEncryption = (int)EmailEncryptionType.Always) .With(s => s.From = "dont@email.me") .With(s => s.To = new string[] { "dont@email.me" }) .Build(); diff --git a/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs b/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs new file mode 100644 index 000000000..93ee7a962 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(201)] + public class email_encryption : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeEncryption); + } + + private void ChangeEncryption(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + using (var getEmailCmd = conn.CreateCommand()) + { + getEmailCmd.Transaction = tran; + getEmailCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'"; + + using (var reader = getEmailCmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + settings["useEncryption"] = settings["requireEncryption"].ToObject() ? 1 : 0; + settings["requireEncryption"] = null; + + updated.Add(new + { + Settings = settings.ToJson(), + Id = id + }); + } + } + } + + var updateSql = $"UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index c437f4973..3a0b392e7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1258,10 +1258,10 @@ "NotificationsEmailSettingsName": "Email", "NotificationsEmailSettingsRecipientAddress": "Recipient Address(es)", "NotificationsEmailSettingsRecipientAddressHelpText": "Comma separated list of email recipients", - "NotificationsEmailSettingsRequireEncryption": "Require Encryption", - "NotificationsEmailSettingsRequireEncryptionHelpText": "Require SSL (Port 465 only) or StartTLS (any other port)", "NotificationsEmailSettingsServer": "Server", "NotificationsEmailSettingsServerHelpText": "Hostname or IP of Email server", + "NotificationsEmailSettingsUseEncryption": "Use Encryption", + "NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption", "NotificationsEmbySettingsSendNotifications": "Send Notifications", "NotificationsEmbySettingsSendNotificationsHelpText": "Have MediaBrowser send notifications to configured providers", "NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete?", diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index f8d3646ad..941754c34 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -134,19 +134,16 @@ namespace NzbDrone.Core.Notifications.Email using var client = new SmtpClient(); client.Timeout = 10000; - var serverOption = SecureSocketOptions.Auto; + var useEncyption = (EmailEncryptionType)settings.UseEncryption; - if (settings.RequireEncryption) + var serverOption = useEncyption switch { - if (settings.Port == 465) - { - serverOption = SecureSocketOptions.SslOnConnect; - } - else - { - serverOption = SecureSocketOptions.StartTls; - } - } + EmailEncryptionType.Always => settings.Port == 465 + ? SecureSocketOptions.SslOnConnect + : SecureSocketOptions.StartTls, + EmailEncryptionType.Never => SecureSocketOptions.None, + _ => SecureSocketOptions.Auto + }; client.ServerCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError; diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index 69ffaa60f..f6b23caad 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -45,8 +45,8 @@ namespace NzbDrone.Core.Notifications.Email [FieldDefinition(1, Label = "Port")] public int Port { get; set; } - [FieldDefinition(2, Label = "NotificationsEmailSettingsRequireEncryption", HelpText = "NotificationsEmailSettingsRequireEncryptionHelpText", Type = FieldType.Checkbox)] - public bool RequireEncryption { get; set; } + [FieldDefinition(2, Label = "NotificationsEmailSettingsUseEncryption", HelpText = "NotificationsEmailSettingsUseEncryptionHelpText", Type = FieldType.Select, SelectOptions = typeof(EmailEncryptionType))] + public int UseEncryption { get; set; } [FieldDefinition(3, Label = "Username", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } @@ -71,4 +71,11 @@ namespace NzbDrone.Core.Notifications.Email return new NzbDroneValidationResult(Validator.Validate(this)); } } + + public enum EmailEncryptionType + { + Preferred = 0, + Always = 1, + Never = 2 + } } From 69f99373e56a2fca49a2be645e6640624cf12339 Mon Sep 17 00:00:00 2001 From: Jendrik Weise Date: Sun, 21 Jan 2024 00:19:33 +0100 Subject: [PATCH 050/762] New: Parse subtitle titles Closes #5955 --- ...les_from_existing_subtitle_filesFixture.cs | 100 ++++++++++++++++++ .../AggregateSubtitleInfoFixture.cs | 48 +++++++++ .../ParserTests/LanguageParserFixture.cs | 33 ++++++ ...rse_titles_from_existing_subtitle_files.cs | 88 +++++++++++++++ .../Subtitles/ExistingSubtitleImporter.cs | 13 ++- .../Extras/Subtitles/SubtitleFile.cs | 30 +++++- .../Extras/Subtitles/SubtitleService.cs | 31 ++++-- .../Aggregation/AggregationService.cs | 3 +- .../Aggregators/AggregateEpisodes.cs | 2 + .../Aggregators/AggregateLanguage.cs | 2 + .../Aggregators/AggregateQuality.cs | 2 + .../Aggregators/AggregateReleaseGroup.cs | 2 + .../Aggregators/AggregateReleaseInfo.cs | 2 + .../Aggregators/AggregateSubtitleInfo.cs | 65 ++++++++++++ .../Aggregators/IAggregateLocalEpisode.cs | 1 + src/NzbDrone.Core/Parser/LanguageParser.cs | 73 ++++++++++++- .../Parser/Model/LocalEpisode.cs | 1 + .../Parser/Model/SubtitleTitleInfo.cs | 15 +++ 18 files changed, 494 insertions(+), 17 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs create mode 100644 src/NzbDrone.Core/Parser/Model/SubtitleTitleInfo.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs new file mode 100644 index 000000000..754c2308c --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dapper; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class parse_title_from_existing_subtitle_filesFixture : MigrationTest + { + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle - 3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle - 3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] + public void should_process_file_with_missing_title(string subtitlePath, string episodePath, string title, int copy) + { + var now = DateTime.UtcNow; + + var db = WithDapperMigrationTestDb(c => + { + c.Insert.IntoTable("SubtitleFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + EpisodeFileId = 1, + RelativePath = subtitlePath, + Added = now, + LastUpdated = now, + Extension = Path.GetExtension(subtitlePath), + Language = 10, + LanguageTags = new List { "sdh" }.ToJson() + }); + + c.Insert.IntoTable("EpisodeFiles").Row(new + { + Id = 1, + SeriesId = 1, + RelativePath = episodePath, + OriginalFilePath = string.Empty, + Quality = new { }.ToJson(), + Size = 0, + DateAdded = now, + SeasonNumber = 1, + Languages = new List { 1 }.ToJson() + }); + }); + + var files = db.Query("SELECT * FROM \"SubtitleFiles\"").ToList(); + + files.Should().HaveCount(1); + + files.First().Title.Should().Be(title); + files.First().Copy.Should().Be(copy); + files.First().LanguageTags.Should().NotContain("sdh"); + files.First().Language.Should().NotBe(10); + } + } + + public class SubtitleFile198 + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int? EpisodeFileId { get; set; } + public int? SeasonNumber { get; set; } + public string RelativePath { get; set; } + public DateTime Added { get; set; } + public DateTime LastUpdated { get; set; } + public string Extension { get; set; } + public int Language { get; set; } + public int Copy { get; set; } + public string Title { get; set; } + public List LanguageTags { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs new file mode 100644 index 000000000..d5e4a472a --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.Test.Framework; + +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) + { + var episodeFile = new EpisodeFile + { + RelativePath = relativePath, + OriginalFilePath = originalFilePath + }; + + var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path); + + subtitleTitleInfo.Title.Should().BeNull(); + subtitleTitleInfo.Copy.Should().Be(0); + } + + [TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")] + [TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].eng.default.ass")] + [TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass")] + [TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].testtitle.eng.default.ass")] + public void should_not_parse_default(string relativePath, string path) + { + var episodeFile = new EpisodeFile + { + RelativePath = relativePath + }; + + var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path); + + subtitleTitleInfo.LanguageTags.Should().NotContain("default"); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 42d06dc4c..0f3cf2749 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -428,5 +428,38 @@ namespace NzbDrone.Core.Test.ParserTests result.Languages.Should().Contain(Language.Original); result.Languages.Should().Contain(Language.English); } + + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", new[] { "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", new[] { "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", new[] { "forced" }, "testtitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", new[] { "default", "forced" }, "testtitle", "French")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")] + public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage) + { + var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle); + + subtitleTitleInfo.LanguageTags.Should().BeEquivalentTo(expectedTags); + subtitleTitleInfo.Title.Should().BeEquivalentTo(expectedTitle); + subtitleTitleInfo.Language.Should().BeEquivalentTo((Language)expectedLanguage); + } + + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].ass")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass")] + public void should_not_parse_false_title(string postTitle) + { + var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle); + subtitleTitleInfo.Language.Should().Be(Language.Unknown); + subtitleTitleInfo.LanguageTags.Should().BeEmpty(); + subtitleTitleInfo.RawTitle.Should().BeNull(); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs b/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs new file mode 100644 index 000000000..5b8d18199 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using Dapper; +using FluentMigrator; +using NLog; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(198)] + public class parse_title_from_existing_subtitle_files : NzbDroneMigrationBase + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(AggregateSubtitleInfo)); + + protected override void MainDbUpgrade() + { + Alter.Table("SubtitleFiles").AddColumn("Title").AsString().Nullable(); + Alter.Table("SubtitleFiles").AddColumn("Copy").AsInt32().WithDefaultValue(0); + Execute.WithConnection(UpdateTitles); + } + + private void UpdateTitles(IDbConnection conn, IDbTransaction tran) + { + var updates = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"SubtitleFiles\".\"Id\", \"SubtitleFiles\".\"RelativePath\", \"EpisodeFiles\".\"RelativePath\", \"EpisodeFiles\".\"OriginalFilePath\" FROM \"SubtitleFiles\" JOIN \"EpisodeFiles\" ON \"SubtitleFiles\".\"EpisodeFileId\" = \"EpisodeFiles\".\"Id\""; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var id = reader.GetInt32(0); + var relativePath = reader.GetString(1); + var episodeFileRelativePath = reader.GetString(2); + var episodeFileOriginalFilePath = reader.GetString(3); + + var subtitleTitleInfo = CleanSubtitleTitleInfo(episodeFileRelativePath, episodeFileOriginalFilePath, relativePath); + + updates.Add(new + { + Id = id, + Title = subtitleTitleInfo.Title, + Language = subtitleTitleInfo.Language, + LanguageTags = subtitleTitleInfo.LanguageTags, + Copy = subtitleTitleInfo.Copy + }); + } + } + + var updateSubtitleFilesSql = "UPDATE \"SubtitleFiles\" SET \"Title\" = @Title, \"Copy\" = @Copy, \"Language\" = @Language, \"LanguageTags\" = @LanguageTags, \"LastUpdated\" = CURRENT_TIMESTAMP WHERE \"Id\" = @Id"; + conn.Execute(updateSubtitleFilesSql, updates, transaction: tran); + } + + private static SubtitleTitleInfo CleanSubtitleTitleInfo(string relativePath, string originalFilePath, string path) + { + var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path); + + var episodeFileTitle = Path.GetFileNameWithoutExtension(relativePath); + var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(originalFilePath) ?? string.Empty; + + if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase))) + { + Logger.Debug("Subtitle title '{0}' is in episode file title '{1}'. Removing from subtitle title.", subtitleTitleInfo.RawTitle, episodeFileTitle); + + subtitleTitleInfo = LanguageParser.ParseBasicSubtitle(path); + } + + var cleanedTags = subtitleTitleInfo.LanguageTags.Where(t => !episodeFileTitle.Contains(t, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (cleanedTags.Count != subtitleTitleInfo.LanguageTags.Count) + { + Logger.Debug("Removed language tags '{0}' from subtitle title '{1}'.", string.Join(", ", subtitleTitleInfo.LanguageTags.Except(cleanedTags)), subtitleTitleInfo.RawTitle); + subtitleTitleInfo.LanguageTags = cleanedTags; + } + + return subtitleTitleInfo; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index ee1c63179..6c5a5481e 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -5,7 +5,6 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -71,15 +70,19 @@ namespace NzbDrone.Core.Extras.Subtitles continue; } + var firstEpisode = localEpisode.Episodes.First(); + var subtitleFile = new SubtitleFile { SeriesId = series.Id, SeasonNumber = localEpisode.SeasonNumber, - EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, + EpisodeFileId = firstEpisode.EpisodeFileId, RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), - Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), - LanguageTags = LanguageParser.ParseLanguageTags(possibleSubtitleFile), - Extension = extension + Language = localEpisode.SubtitleInfo.Language, + LanguageTags = localEpisode.SubtitleInfo.LanguageTags, + Title = localEpisode.SubtitleInfo.Title, + Extension = extension, + Copy = localEpisode.SubtitleInfo.Copy }; subtitleFiles.Add(subtitleFile); diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs index 41a28ef4a..c3a3b7db1 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Languages; @@ -13,15 +14,40 @@ namespace NzbDrone.Core.Extras.Subtitles public Language Language { get; set; } - public string AggregateString => Language + LanguageTagsAsString + Extension; + public string AggregateString => Language + Title + LanguageTagsAsString + Extension; + + public int Copy { get; set; } public List LanguageTags { get; set; } + public string Title { get; set; } + private string LanguageTagsAsString => string.Join(".", LanguageTags); public override string ToString() { - return $"[{Id}] {RelativePath} ({Language}{(LanguageTags.Count > 0 ? "." : "")}{LanguageTagsAsString}{Extension})"; + var stringBuilder = new StringBuilder(); + stringBuilder.AppendFormat("[{0}] ", Id); + stringBuilder.Append(RelativePath); + + stringBuilder.Append(" ("); + stringBuilder.Append(Language); + if (Title is not null) + { + stringBuilder.Append('.'); + stringBuilder.Append(Title); + } + + if (LanguageTags.Count > 0) + { + stringBuilder.Append('.'); + stringBuilder.Append(LanguageTagsAsString); + } + + stringBuilder.Append(Extension); + stringBuilder.Append(')'); + + return stringBuilder.ToString(); } } } diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs index 0b3a4e557..465783f16 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -76,16 +76,20 @@ namespace NzbDrone.Core.Extras.Subtitles foreach (var group in groupedExtraFilesForEpisodeFile) { - var groupCount = group.Count(); - var copy = 1; + var multipleCopies = group.Count() > 1; + var orderedGroup = group.OrderBy(s => -s.Copy).ToList(); + var copy = group.First().Copy; - foreach (var subtitleFile in group) + foreach (var subtitleFile in orderedGroup) { - var suffix = GetSuffix(subtitleFile.Language, copy, subtitleFile.LanguageTags, groupCount > 1); + if (multipleCopies && subtitleFile.Copy == 0) + { + subtitleFile.Copy = ++copy; + } + + var suffix = GetSuffix(subtitleFile.Language, subtitleFile.Copy, subtitleFile.LanguageTags, multipleCopies, subtitleFile.Title); movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix)); - - copy++; } } } @@ -229,11 +233,22 @@ namespace NzbDrone.Core.Extras.Subtitles return importedFiles; } - private string GetSuffix(Language language, int copy, List languageTags, bool multipleCopies = false) + private string GetSuffix(Language language, int copy, List languageTags, bool multipleCopies = false, string title = null) { var suffixBuilder = new StringBuilder(); - if (multipleCopies) + if (title is not null) + { + suffixBuilder.Append('.'); + suffixBuilder.Append(title); + + if (multipleCopies) + { + suffixBuilder.Append(" - "); + suffixBuilder.Append(copy); + } + } + else if (multipleCopies) { suffixBuilder.Append('.'); suffixBuilder.Append(copy); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs index 69dd300cc..49cdda995 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; @@ -30,7 +31,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation IConfigService configService, Logger logger) { - _augmenters = augmenters; + _augmenters = augmenters.OrderBy(a => a.Order).ToList(); _diskProvider = diskProvider; _videoFileInfoReader = videoFileInfoReader; _configService = configService; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs index f243537e5..3c978576e 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs @@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public class AggregateEpisodes : IAggregateLocalEpisode { + public int Order => 1; + private readonly IParsingService _parsingService; public AggregateEpisodes(IParsingService parsingService) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateLanguage.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateLanguage.cs index 07c0dd34e..fade77302 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateLanguage.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateLanguage.cs @@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public class AggregateLanguage : IAggregateLocalEpisode { + public int Order => 1; + private readonly List _augmentLanguages; private readonly Logger _logger; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs index 434663f5e..234568a92 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs @@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public class AggregateQuality : IAggregateLocalEpisode { + public int Order => 1; + private readonly List _augmentQualities; private readonly Logger _logger; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs index a8b1d1b8e..db909a4c7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs @@ -6,6 +6,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public class AggregateReleaseGroup : IAggregateLocalEpisode { + public int Order => 1; + public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { // Prefer ReleaseGroup from DownloadClient/Folder if they're not a season pack diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseInfo.cs index 111b465f6..c68464049 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseInfo.cs @@ -8,6 +8,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public class AggregateReleaseInfo : IAggregateLocalEpisode { + public int Order => 1; + private readonly IHistoryService _historyService; public AggregateReleaseInfo(IHistoryService historyService) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs new file mode 100644 index 000000000..5beabf7d5 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Core.Download; +using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateSubtitleInfo : IAggregateLocalEpisode + { + public int Order => 2; + + private readonly Logger _logger; + + public AggregateSubtitleInfo(Logger logger) + { + _logger = logger; + } + + public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + var path = localEpisode.Path; + var isSubtitleFile = SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)); + + if (!isSubtitleFile) + { + return localEpisode; + } + + var firstEpisode = localEpisode.Episodes.First(); + var episodeFile = firstEpisode.EpisodeFile.Value; + localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path); + + return localEpisode; + } + + public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path) + { + var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path); + + var episodeFileTitle = Path.GetFileNameWithoutExtension(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))) + { + _logger.Debug("Subtitle title '{0}' is in episode file title '{1}'. Removing from subtitle title.", subtitleTitleInfo.RawTitle, episodeFileTitle); + + subtitleTitleInfo = LanguageParser.ParseBasicSubtitle(path); + } + + var cleanedTags = subtitleTitleInfo.LanguageTags.Where(t => !episodeFileTitle.Contains(t, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (cleanedTags.Count != subtitleTitleInfo.LanguageTags.Count) + { + _logger.Debug("Removed language tags '{0}' from subtitle title '{1}'.", string.Join(", ", subtitleTitleInfo.LanguageTags.Except(cleanedTags)), subtitleTitleInfo.RawTitle); + subtitleTitleInfo.LanguageTags = cleanedTags; + } + + return subtitleTitleInfo; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs index e32cf7a02..7cc4f58be 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators { public interface IAggregateLocalEpisode { + int Order { get; } LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); } } diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 149af2329..7344bab08 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Parser { @@ -28,7 +29,11 @@ namespace NzbDrone.Core.Parser private static readonly Regex GermanDualLanguageRegex = new (@"(?[a-z]{2,3})([-_. ](?full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SubtitleLanguageRegex = new Regex(".+?([-_. ](?full|forced|foreign|default|cc|psdh|sdh))*[-_. ](?[a-z]{2,3})([-_. ](?full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SubtitleLanguageTitleRegex = new Regex(".+?(\\.((?full|forced|foreign|default|cc|psdh|sdh)|(?[a-z]{2,3})))*\\.(?[^.]*)(\\.((?<tags2>full|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+)$", RegexOptions.Compiled); public static List<Language> ParseLanguages(string title) { @@ -249,6 +254,72 @@ namespace NzbDrone.Core.Parser return Language.Unknown; } + public static SubtitleTitleInfo ParseBasicSubtitle(string fileName) + { + return new SubtitleTitleInfo + { + TitleFirst = false, + LanguageTags = ParseLanguageTags(fileName), + Language = ParseSubtitleLanguage(fileName) + }; + } + + public static SubtitleTitleInfo ParseSubtitleLanguageInformation(string fileName) + { + var simpleFilename = Path.GetFileNameWithoutExtension(fileName); + var matchTitle = SubtitleLanguageTitleRegex.Match(simpleFilename); + + if (!matchTitle.Groups["title"].Success || (matchTitle.Groups["iso_code"].Captures.Count is var languageCodeNumber && languageCodeNumber != 1)) + { + Logger.Debug("Could not parse a title from subtitle file: {0}. Falling back to parsing without title.", fileName); + + return ParseBasicSubtitle(fileName); + } + + var isoCode = matchTitle.Groups["iso_code"].Value; + var isoLanguage = IsoLanguages.Find(isoCode.ToLower()); + + var language = isoLanguage?.Language ?? Language.Unknown; + + var languageTags = matchTitle.Groups["tags1"].Captures + .Union(matchTitle.Groups["tags2"].Captures) + .Cast<Capture>() + .Where(tag => !tag.Value.Empty()) + .Select(tag => tag.Value.ToLower()); + var rawTitle = matchTitle.Groups["title"].Value; + + var subtitleTitleInfo = new SubtitleTitleInfo + { + TitleFirst = matchTitle.Groups["tags1"].Captures.Empty(), + LanguageTags = languageTags.ToList(), + RawTitle = rawTitle, + Language = language + }; + + UpdateTitleAndCopyFromTitle(subtitleTitleInfo); + + return subtitleTitleInfo; + } + + public static void UpdateTitleAndCopyFromTitle(SubtitleTitleInfo subtitleTitleInfo) + { + if (subtitleTitleInfo.RawTitle is null) + { + subtitleTitleInfo.Title = null; + subtitleTitleInfo.Copy = 0; + } + else if (SubtitleTitleRegex.Match(subtitleTitleInfo.RawTitle) is var match && match.Success) + { + subtitleTitleInfo.Title = match.Groups["title"].Success ? match.Groups["title"].ToString() : null; + subtitleTitleInfo.Copy = int.Parse(match.Groups["copy"].ToString()); + } + else + { + subtitleTitleInfo.Title = subtitleTitleInfo.RawTitle; + subtitleTitleInfo.Copy = 0; + } + } + public static List<string> ParseLanguageTags(string fileName) { try diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 470310b7c..060a14e6e 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Parser.Model public bool FileRenamedAfterScriptImport { get; set; } public bool ShouldImportExtras { get; set; } public List<string> PossibleExtraFiles { get; set; } + public SubtitleTitleInfo SubtitleInfo { get; set; } public int SeasonNumber { diff --git a/src/NzbDrone.Core/Parser/Model/SubtitleTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SubtitleTitleInfo.cs new file mode 100644 index 000000000..29ea84377 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/SubtitleTitleInfo.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Parser.Model +{ + public class SubtitleTitleInfo + { + public List<string> LanguageTags { get; set; } + public Language Language { get; set; } + public string RawTitle { get; set; } + public string Title { get; set; } + public int Copy { get; set; } + public bool TitleFirst { get; set; } + } +} From dbbf1a7f58779d54cdcd926e14a0262a57b71b00 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 20 Jan 2024 23:19:41 +0000 Subject: [PATCH 051/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 72 +++++++++- src/NzbDrone.Core/Localization/Core/fr.json | 2 - src/NzbDrone.Core/Localization/Core/hu.json | 125 +++++++++++++++++- .../Localization/Core/pt_BR.json | 6 +- .../Localization/Core/zh_CN.json | 2 - 5 files changed, 196 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index fbbff560d..d0ef8f0ff 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -457,7 +457,7 @@ "LogFilesLocation": "Los archivos de registro se encuentran en: {location}", "DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido", "InfoUrl": "Información de la URL", - "HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia, en los enlaces que aparecen a continuación.", + "HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación.", "ManualGrab": "Captura manual", "FullColorEvents": "Eventos a todo color", "FullColorEventsHelpText": "Estilo alterado para colorear todo el evento con el color de estado, en lugar de sólo el borde izquierdo. No se aplica a la Agenda", @@ -530,5 +530,73 @@ "DeleteEpisodeFromDisk": "Eliminar episodio del disco", "DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series", "DoNotPrefer": "No preferir", - "DoNotUpgradeAutomatically": "No actualizar automáticamente" + "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", + "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", + "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", + "DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb", + "Docker": "Docker", + "DockerUpdater": "Actualiza el contenedor docker para recibir la actualización", + "Donations": "Donaciones", + "DownloadClient": "Cliente de descarga", + "AutoTaggingSpecificationGenre": "Género(s)", + "AutoTaggingSpecificationMaximumYear": "Año máximo", + "AutoTaggingSpecificationMinimumYear": "Año mínimo", + "AutoTaggingSpecificationOriginalLanguage": "Idioma", + "AutoTaggingSpecificationQualityProfile": "Perfil de calidad", + "AutoTaggingSpecificationRootFolder": "Carpeta raíz", + "AutoTaggingSpecificationSeriesType": "Tipo de series", + "AutoTaggingSpecificationStatus": "Estado", + "CustomFormatsSpecificationLanguage": "Idioma", + "CustomFormatsSpecificationMaximumSize": "Tamaño máximo", + "CustomFormatsSpecificationMaximumSizeHelpText": "La versión debe ser menor o igual a este tamaño", + "CustomFormatsSpecificationMinimumSize": "Tamaño mínimo", + "CustomFormatsSpecificationMinimumSizeHelpText": "La versión debe ser mayor que este tamaño", + "CustomFormatsSpecificationRegularExpression": "Idioma", + "CustomFormatsSpecificationRegularExpressionHelpText": "El formato RegEx personalizado no distingue mayúsculas de minúsculas", + "CustomFormatsSpecificationReleaseGroup": "Grupo de publicación", + "CustomFormatsSpecificationResolution": "Resolución", + "CustomFormatsSpecificationSource": "Fuente", + "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}", + "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}', predeterminado con '{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 a '{port}'", + "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto 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.", + "UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", + "DownloadClientDownloadStationProviderMessage": "{appName} no puede conectarse a la Estación de descarga si la Autenticación de 2 factores está habilitada en tu cuenta de DSM", + "DownloadClientDownloadStationSettingsDirectory": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", + "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Predeterminada: 1. Se usa round-robin para clientes con la misma prioridad.", + "DownloadClientDelugeValidationLabelPluginInactive": "Extensión de etiqueta no activada", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de Ratio de Compartición", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Una vez alcanzada esta puntuación de formato personalizada {appName} no capturará más lanzamientos de episodios", + "IndexerValidationRequestLimitReached": "Límite de petición alcanzado: {exceptionMessage}", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Debes tener la Extensión de etiqueta habilitada en {clientName} para usar categorías.", + "DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada", + "DownloadClientNzbgetValidationKeepHistoryZero": "La opción KeepHistory de NzbGet debería ser mayor que 0", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.", + "DownloadClientDownloadStationValidationSharedFolderMissing": "No existe la carpeta compartida" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c5b4d17fb..19e611ddb 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1735,8 +1735,6 @@ "NotificationsEmbySettingsSendNotifications": "Envoyer des notifications", "NotificationsEmailSettingsServerHelpText": "Nom d'hôte ou adresse IP du serveur de courriel", "NotificationsEmailSettingsServer": "Serveur", - "NotificationsEmailSettingsRequireEncryptionHelpText": "Requiert SSL (port 465 uniquement) ou StartTLS (tout autre port)", - "NotificationsEmailSettingsRequireEncryption": "Requiert le chiffrement", "NotificationsEmailSettingsRecipientAddress": "Adresse(s) du/des destinataire(s)", "NotificationsEmailSettingsName": "Courriel", "NotificationsEmailSettingsFromAddress": "Depuis l'adresse", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 077965714..fa8d1eb9c 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -414,5 +414,128 @@ "AddRootFolderError": "Hozzáadás a gyökérmappához", "YesCancel": "Igen, elvet", "Wanted": "Keresett", - "Warn": "Figyelmeztetés" + "Warn": "Figyelmeztetés", + "Downloading": "Letöltés", + "Refresh": "Frissítés", + "Renamed": "Átnevezve", + "CheckDownloadClientForDetails": "További részletekért ellenőrizze a letöltési klienst", + "Downloaded": "Letöltve", + "EpisodeFileRenamed": "Epizódfájl átnevezve", + "Error": "Hiba", + "Password": "Jelszó", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Engedélyezze az áthidalást, ha a kiadás pontszáma magasabb, mint a konfigurált minimális egyéni formátum pontszám", + "ChmodFolder": "chmod Mappa", + "ClientPriority": "Kliens prioritás", + "Connections": "Kapcsolatok", + "DeleteDelayProfileMessageText": "Biztosan törli ezt a késleltetési profilt?", + "DeleteImportList": "Importálási lista törlése", + "DoNotUpgradeAutomatically": "Ne frissítse automatikusan", + "DoneEditingGroups": "A csoportok szerkesztése kész", + "Failed": "Nem sikerült", + "Host": "Hoszt", + "Info": "Infó", + "Never": "Soha", + "Period": "Időszak", + "Protocol": "Protokoll", + "Queued": "Sorban", + "Rating": "Értékelés", + "Runtime": "Futási Idő", + "Season": "Évad", + "Folder": "Mappa", + "Hostname": "Hosztnév", + "Imported": "Importált", + "Airs": "Adásban", + "AuthenticationMethodHelpText": "Felhasználónév és jelszó szükséges a(z) {appName} eléréséhez", + "AuthenticationRequiredHelpText": "Módosítsa, hogy mely kérésekhez van szükség hitelesítésre. Ne változtasson, hacsak nem érti a kockázatokat.", + "BranchUpdateMechanism": "Külső frissítési mechanizmus által használt ág", + "Duplicate": "Duplikált", + "Reload": "Újratöltés", + "Qualities": "Minőségek", + "Reset": "Visszaállítás", + "CollapseAll": "Mindet összecsuk", + "Continuing": "Folytatás", + "Enable": "Aktiválás", + "Grabbed": "Megragadta", + "Group": "Csoport", + "AutoTaggingSpecificationGenre": "Műfaj(ok)", + "AutoTaggingSpecificationMaximumYear": "Maximum Év", + "AutoTaggingSpecificationMinimumYear": "Minimum Év", + "AutoTaggingSpecificationOriginalLanguage": "Nyelv", + "AutoTaggingSpecificationQualityProfile": "Minőségi Profil", + "AutoTaggingSpecificationRootFolder": "Gyökérmappa", + "AutoTaggingSpecificationSeriesType": "Sorozat típus", + "AutoTaggingSpecificationStatus": "Státusz", + "CalendarLegendEpisodeMissingTooltip": "Az epizódot leadták, és hiányzik a lemezről", + "ChangeFileDateHelpText": "Módosítsa a fájl dátumát az importáláskor", + "ClearBlocklistMessageText": "Biztosan törli az összes elemet a tiltólistáról?", + "CollapseMultipleEpisodes": "Több epizód összecsukása", + "Component": "Összetevő", + "CopyToClipboard": "Másolja a vágólapra", + "ConnectSettings": "Csatlakozási beállítások", + "ContinuingOnly": "Csak folytatás", + "CreateGroup": "Csoport létrehozása", + "CustomFormatsLoadError": "Nem sikerült betölteni az egyéni formátumokat", + "CustomFormatsSettings": "Egyéni formátumok beállításai", + "CustomFormatsSettingsSummary": "Egyéni formátumok és beállítások", + "CustomFormatsSpecificationLanguage": "Nyelv", + "CustomFormatsSpecificationMaximumSize": "Maximum méret", + "CustomFormatsSpecificationMaximumSizeHelpText": "A kioldásnak kisebbnek vagy egyenlőnek kell lennie ennél a méretnél", + "CustomFormatsSpecificationMinimumSize": "Minimum méret", + "CustomFormatsSpecificationMinimumSizeHelpText": "A kibocsátásnak nagyobbnak kell lennie ennél a méretnél", + "CustomFormatsSpecificationRegularExpression": "Nyelv", + "CustomFormatsSpecificationSource": "Forrás", + "CustomFormatsSpecificationReleaseGroup": "Release Csoport", + "CustomFormatsSpecificationResolution": "Felbontás", + "DiskSpace": "Lemez terület", + "Donate": "Adományoz", + "DownloadClient": "Letöltési kliens", + "EpisodeDownloaded": "Epizód letöltve", + "EpisodeFileDeleted": "Epizódfájl törölve", + "EpisodeFileMissingTooltip": "Az epizódfájl hiányzik", + "Episodes": "Epizodok", + "Folders": "Mappák", + "HttpHttps": "HTTP(S)", + "Indexer": "Indexelő", + "Monday": "Hétfő", + "Restart": "Újrakezd", + "Scheduled": "Ütemezve", + "Save": "Mentés", + "Original": "Eredeti", + "Importing": "Importálás", + "Directory": "Könyvtár", + "BypassDelayIfHighestQuality": "Bypass, ha a legjobb minőség", + "CertificateValidationHelpText": "Módosítsa a HTTPS-tanúsítvány-ellenőrzés szigorúságát. Ne változtasson, hacsak nem érti a kockázatokat.", + "CloneAutoTag": "Auto Tag klónozása", + "CloneIndexer": "Indexelő klónozása", + "CloneProfile": "Profil klónozása", + "CustomFormat": "Egyéni formátum", + "EditConditionImplementation": "Feltétel szerkesztése – {implementationName}", + "AnimeEpisodeTypeFormat": "Abszolút epizódszám ({format})", + "CalendarLegendEpisodeOnAirTooltip": "Az epizód jelenleg adásban van", + "CalendarLegendEpisodeUnairedTooltip": "Az epizódot még nem adták le", + "Wiki": "Wiki", + "ConnectionLost": "A kapcsolat megszakadt", + "CreateEmptySeriesFoldersHelpText": "Hozzon létre hiányzó sorozatú mappákat a lemezellenőrzés során", + "Custom": "Egyedi", + "CreateEmptySeriesFolders": "Hozzon létre üres sorozat mappákat", + "Cutoff": "Levág", + "DailyEpisodeFormat": "Napi epizód formátum", + "Date": "Dátum", + "DeleteEmptyFolders": "Üres mappák törlése", + "Deleted": "Törölve", + "Episode": "Epizód", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Automatikusan keressen és kíséreljen meg egy másik kiadást letölteni, ha az interaktív keresésből sikertelen kiadást ragadtak meg", + "EpisodeAirDate": "Epizód adási dátuma", + "EpisodeCount": "Epizódszám", + "Formats": "Formátum", + "Health": "Egészség", + "Peers": "Peerek", + "Proxy": "Proxy", + "Restore": "Visszaállít", + "Seeders": "Seederek", + "Started": "Elindult", + "Here": "itt", + "Uptime": "Üzemidő", + "AnalyticsEnabledHelpText": "Névtelen használati és hibainformáció küldése {appName} szervereinek. Ez magában foglalja a böngészővel kapcsolatos információkat, a használt {appName} WebUI oldalakat, a hibajelentéseket, valamint az operációs rendszert és a futásidejű verziót. Ezeket az információkat a funkciók és a hibajavítások fontossági sorrendjének meghatározására fogjuk használni.", + "ApplicationUrlHelpText": "Ennek az alkalmazásnak a külső URL-címe, beleértve a http-eket" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 99a311934..9c1b2357b 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -366,7 +366,7 @@ "Protocol": "Protocolo", "RejectionCount": "Número de rejeição", "SubtitleLanguages": "Idiomas das Legendas", - "UnmonitoredOnly": "Somente não monitorado", + "UnmonitoredOnly": "Somente Não Monitorados", "AddAutoTag": "Adicionar Tag Automática", "AddCondition": "Adicionar Condição", "Conditions": "Condições", @@ -1081,7 +1081,7 @@ "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", "NoHistoryBlocklist": "Não há lista de bloqueio no histórico", - "QualityCutoffNotMet": "O corte de qualidade não foi atingido", + "QualityCutoffNotMet": "Corte da Qualidade ainda não foi alcançado", "QueueLoadError": "Falha ao carregar a fila", "RemoveQueueItem": "Remover - {sourceTitle}", "RemoveQueueItemConfirmation": "Tem certeza de que deseja remover '{sourceTitle}' da fila?", @@ -1735,7 +1735,6 @@ "NotificationsEmailSettingsName": "Email", "NotificationsEmailSettingsRecipientAddress": "Endereço(s) do Destinatário", "NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por vírgulas de destinatários de e-mail", - "NotificationsEmailSettingsRequireEncryption": "Exigir Criptografia", "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Altere os campos passados para esta notificação 'ao capturar'", "NotificationsEmailSettingsServer": "Servidor", "NotificationsEmailSettingsServerHelpText": "Nome do host ou IP do servidor de e-mail", @@ -1848,7 +1847,6 @@ "NotificationsValidationUnableToSendTestMessage": "Não foi possível enviar a mensagem de teste: {exceptionMessage}", "NotificationsValidationUnableToSendTestMessageApiResponse": "Não foi possível enviar mensagem de teste. Resposta da API: {error}", "NotificationsAppriseSettingsStatelessUrlsHelpText": "Uma ou mais URLs separadas por vírgulas identificando para onde a notificação deve ser enviada. Deixe em branco se o armazenamento persistente for usado.", - "NotificationsEmailSettingsRequireEncryptionHelpText": "Exigir SSL (somente porta 465) ou StartTLS (qualquer outra porta)", "NotificationsEmbySettingsSendNotificationsHelpText": "Faça com que o MediaBrowser envie notificações para provedores configurados", "NotificationsJoinSettingsDeviceIdsHelpText": "Obsoleto, use nomes de dispositivos. Lista separada por vírgulas de IDs de dispositivos para os quais você gostaria de enviar notificações. Se não for definido, todos os dispositivos receberão notificações.", "NotificationsTwitterSettingsConnectToTwitter": "Conecte-se ao Twitter / X", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index add862d21..c24b4be3b 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1727,8 +1727,6 @@ "NotificationsEmailSettingsFromAddress": "发件地址", "NotificationsEmailSettingsRecipientAddress": "收件地址", "NotificationsEmailSettingsRecipientAddressHelpText": "逗号分隔的收件地址", - "NotificationsEmailSettingsRequireEncryption": "要求加密", - "NotificationsEmailSettingsRequireEncryptionHelpText": "要求 SSL(仅 465 端口)或 STARTTLS(其他任意端口)", "NotificationsEmailSettingsServer": "服务器", "NotificationsEmailSettingsServerHelpText": "邮件服务器的主机名或 IP", "NotificationsEmbySettingsSendNotifications": "发送通知", From 8921c5d7a079c58b0c74713355bf82846abc43ab Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 20 Jan 2024 16:16:29 -0800 Subject: [PATCH 052/762] Fixed: Subtitle title migration when original title is null --- .../198_parse_titles_from_existing_subtitle_filesFixture.cs | 1 - .../Migration/198_parse_titles_from_existing_subtitle_files.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs index 754c2308c..332a2f165 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs @@ -62,7 +62,6 @@ namespace NzbDrone.Core.Test.Datastore.Migration Id = 1, SeriesId = 1, RelativePath = episodePath, - OriginalFilePath = string.Empty, Quality = new { }.ToJson(), Size = 0, DateAdded = now, diff --git a/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs b/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs index 5b8d18199..121d11d8d 100644 --- a/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs +++ b/src/NzbDrone.Core/Datastore/Migration/198_parse_titles_from_existing_subtitle_files.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Datastore.Migration var id = reader.GetInt32(0); var relativePath = reader.GetString(1); var episodeFileRelativePath = reader.GetString(2); - var episodeFileOriginalFilePath = reader.GetString(3); + var episodeFileOriginalFilePath = reader[3] as string; var subtitleTitleInfo = CleanSubtitleTitleInfo(episodeFileRelativePath, episodeFileOriginalFilePath, relativePath); From cab93249ec2457cd1ee40720922b1ccfa5768a8a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 20 Jan 2024 15:43:51 -0800 Subject: [PATCH 053/762] Fixed: Number only hashes getting substituted incorrectly --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 5dccae231..b966a4988 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -128,6 +128,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Different show 1. Bölüm (23.10.2023) 720p WebDL AAC H.264 - TURG", "Different show", 1, 0, 0)] [TestCase("Dubbed show 79.BLM Sezon Finali(25.06.2023) 720p WEB-DL AAC2.0 H.264-TURG", "Dubbed show", 79, 0, 0)] [TestCase("Exclusive BLM Documentary with no false positives EP03.1080p.AAC.x264", "Exclusive BLM Documentary with no false positives", 3, 0, 0)] + [TestCase("[SubsPlease] Title de Series S2 - 03 (540p) [63501322]", "Title de Series S2", 3, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 2b4d4b3ee..1ff338e2f 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@"^\[(?<subgroup>[^\]]+)\]\[(?<chinesetitle>(?<![^a-zA-Z0-9])[^a-zA-Z0-9]+)(?<title>[^\]]+?)\](?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), // Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]{1,4}(?:-[0-9]{1,4})?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), From 271266b10ac51ee6dd7a7024d346b631bd5397c2 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:11:28 +0200 Subject: [PATCH 054/762] Fix possible NullRef in Email Encryption migration --- .../Migration/201_email_encryptionFixture.cs | 29 +++++++++++++++++++ .../Migration/201_email_encryption.cs | 4 +-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs index 7a6cee1ca..3a0f28ee2 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/201_email_encryptionFixture.cs @@ -81,6 +81,35 @@ namespace NzbDrone.Core.Test.Datastore.Migration items.First().ConfigContract.Should().Be("EmailSettings"); items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always); } + + [Test] + public void should_use_defaults_when_settings_are_empty() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Mail Sonarr", + Implementation = "Email", + Tags = "[]", + Settings = new { }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred); + } } public class NotificationDefinition201 diff --git a/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs b/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs index 93ee7a962..5f5f0c737 100644 --- a/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs +++ b/src/NzbDrone.Core/Datastore/Migration/201_email_encryption.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Datastore.Migration var id = reader.GetInt32(0); var settings = Json.Deserialize<JObject>(reader.GetString(1)); - settings["useEncryption"] = settings["requireEncryption"].ToObject<bool>() ? 1 : 0; + settings["useEncryption"] = settings.Value<bool>("requireEncryption") ? 1 : 0; settings["requireEncryption"] = null; updated.Add(new @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Datastore.Migration } } - var updateSql = $"UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; conn.Execute(updateSql, updated, transaction: tran); } } From f95dd00b51e61a96a0e6c094ec922c8f5cbc5334 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 22 Jan 2024 17:29:15 -0800 Subject: [PATCH 055/762] Fixed: Migrating subtitle files with unexpectedly large number at end Closes #6409 --- .../198_parse_titles_from_existing_subtitle_filesFixture.cs | 1 + src/NzbDrone.Core/Parser/LanguageParser.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs index 332a2f165..279d8a584 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/198_parse_titles_from_existing_subtitle_filesFixture.cs @@ -38,6 +38,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration [TestCase("Name (2020) - S01E20 - [AAC 2.0].3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)] + [TestCase("Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.en.forced.srt", "Name (2020)/Season 1/Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.mkv", null, 0)] public void should_process_file_with_missing_title(string subtitlePath, string episodePath, string title, int copy) { var now = DateTime.UtcNow; diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 7344bab08..77b51e828 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -31,9 +31,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex SubtitleLanguageRegex = new Regex(".+?([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SubtitleLanguageTitleRegex = new Regex(".+?(\\.((?<tags1>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*\\.(?<title>[^.]*)(\\.((?<tags2>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?<tags1>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*\.(?<title>[^.]*)(\.((?<tags2>full|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+)$", 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) { From 0d064181941fc6d149fc2f891661e059758d5428 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 20 Jan 2024 16:26:19 -0800 Subject: [PATCH 056/762] New: Add size to more history events Closes #6234 --- src/NzbDrone.Core/History/HistoryService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 813329908..b893e0959 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -219,6 +219,7 @@ namespace NzbDrone.Core.History history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name); history.Data.Add("ReleaseGroup", message.EpisodeInfo.ReleaseGroup); history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString()); + history.Data.Add("Size", message.EpisodeInfo.Size.ToString()); _historyRepository.Insert(history); } @@ -244,6 +245,7 @@ namespace NzbDrone.Core.History history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name); history.Data.Add("Message", message.Message); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup); + history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString()); _historyRepository.Insert(history); } @@ -277,6 +279,7 @@ namespace NzbDrone.Core.History history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); + history.Data.Add("Size", message.EpisodeFile.Size.ToString()); _historyRepository.Insert(history); } @@ -307,6 +310,7 @@ namespace NzbDrone.Core.History history.Data.Add("Path", path); history.Data.Add("RelativePath", relativePath); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); + history.Data.Add("Size", message.EpisodeFile.Size.ToString()); _historyRepository.Insert(history); } @@ -334,6 +338,7 @@ namespace NzbDrone.Core.History history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name); history.Data.Add("Message", message.Message); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup); + history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString()); historyToAdd.Add(history); } From 9ba5850fcaf0a5fb73dec7d7f8f1d8d3de0b3fb9 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 20 Jan 2024 22:27:52 -0800 Subject: [PATCH 057/762] Fixed: Parsing Hungarian anime releases Closes #6275 --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 3 +++ src/NzbDrone.Core/Parser/Parser.cs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index b966a4988..37c54215c 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -129,6 +129,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Dubbed show 79.BLM Sezon Finali(25.06.2023) 720p WEB-DL AAC2.0 H.264-TURG", "Dubbed show", 79, 0, 0)] [TestCase("Exclusive BLM Documentary with no false positives EP03.1080p.AAC.x264", "Exclusive BLM Documentary with no false positives", 3, 0, 0)] [TestCase("[SubsPlease] Title de Series S2 - 03 (540p) [63501322]", "Title de Series S2", 3, 0, 0)] + [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("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 1ff338e2f..99b26355b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -254,7 +254,6 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[-_. ]S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[-_. ]?[ex]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // TODO: Before this // Single or multi episode releases with multiple titles, each followed by season and episode numbers in brackets new Regex(@"^(?<title>.*?)[ ._]\(S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?\)(?:[ ._]\/[ ._])(?<title>.*?)[ ._]\(", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -487,7 +486,7 @@ namespace NzbDrone.Core.Parser // Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt - private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*\]|[ -]{2,})[ -]*", + private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*\]|[ -]{2,})[ -]*", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); From a71d40edba1388d67e4deefd8bfc354a7a83c6b1 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 20 Jan 2024 19:22:07 -0800 Subject: [PATCH 058/762] New: Add recycle bin path for deleted episodes to webhook/custom script Closes #6114 --- .../Checks/RemotePathMappingCheckFixture.cs | 2 +- .../HistoryTests/HistoryServiceFixture.cs | 2 +- .../NotificationTests/SynologyIndexerFixture.cs | 12 ++++++------ .../NotificationTests/Xbmc/OnDownloadFixture.cs | 13 ++++++++----- src/NzbDrone.Core/MediaFiles/DeletedEpisodeFile.cs | 14 ++++++++++++++ .../MediaFiles/EpisodeFileMoveResult.cs | 6 +++--- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../MediaFiles/Events/EpisodeImportedEvent.cs | 4 ++-- src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs | 8 ++++++-- .../MediaFiles/ScriptImportDecider.cs | 7 ++++--- .../MediaFiles/UpgradeMediaFileService.cs | 5 +++-- .../Notifications/CustomScript/CustomScript.cs | 7 ++++--- src/NzbDrone.Core/Notifications/DownloadMessage.cs | 2 +- .../Notifications/Synology/SynologyIndexer.cs | 2 +- .../Notifications/Webhook/WebhookBase.cs | 5 +++-- .../Notifications/Webhook/WebhookEpisodeFile.cs | 1 + src/NzbDrone.Core/Parser/Model/LocalEpisode.cs | 2 +- 17 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/DeletedEpisodeFile.cs diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs index 65ad530ae..fd4d721c1 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs @@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_ok_on_episode_imported_event() { GivenFolderExists(_downloadRootPath); - var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<EpisodeFile>(), true, new DownloadClientItem()); + var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<DeletedEpisodeFile>(), true, new DownloadClientItem()); Subject.Check(importEvent).ShouldBeOk(); } diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 66595f760..96ab16a0d 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.HistoryTests DownloadId = "abcd" }; - Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<EpisodeFile>(), true, downloadClientItem)); + Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<DeletedEpisodeFile>(), true, downloadClientItem)); Mocker.GetMock<IHistoryRepository>() .Verify(v => v.Insert(It.Is<EpisodeHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path)))); diff --git a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs index b2bb853e1..fbdd02ffc 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Core.MediaFiles; @@ -33,16 +33,16 @@ namespace NzbDrone.Core.Test.NotificationTests RelativePath = "file1.S01E01E02.mkv" }, - OldFiles = new List<EpisodeFile> + OldFiles = new List<DeletedEpisodeFile> { - new EpisodeFile + new DeletedEpisodeFile(new EpisodeFile { RelativePath = "file1.S01E01.mkv" - }, - new EpisodeFile + }, null), + new DeletedEpisodeFile(new EpisodeFile { RelativePath = "file1.S01E02.mkv" - } + }, null) } }; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs index 53052ea88..09063bc4d 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc _downloadMessage = Builder<DownloadMessage>.CreateNew() .With(d => d.Series = series) .With(d => d.EpisodeFile = episodeFile) - .With(d => d.OldFiles = new List<EpisodeFile>()) + .With(d => d.OldFiles = new List<DeletedEpisodeFile>()) .Build(); Subject.Definition = new NotificationDefinition(); @@ -40,9 +40,12 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc private void GivenOldFiles() { - _downloadMessage.OldFiles = Builder<EpisodeFile>.CreateListOfSize(1) - .Build() - .ToList(); + _downloadMessage.OldFiles = Builder<DeletedEpisodeFile> + .CreateListOfSize(1) + .All() + .WithFactory(() => new DeletedEpisodeFile(Builder<EpisodeFile>.CreateNew().Build(), null)) + .Build() + .ToList(); Subject.Definition.Settings = new XbmcSettings { diff --git a/src/NzbDrone.Core/MediaFiles/DeletedEpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/DeletedEpisodeFile.cs new file mode 100644 index 000000000..1383128c2 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DeletedEpisodeFile.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Core.MediaFiles +{ + public class DeletedEpisodeFile + { + public string RecycleBinPath { get; set; } + public EpisodeFile EpisodeFile { get; set; } + + public DeletedEpisodeFile(EpisodeFile episodeFile, string recycleBinPath) + { + EpisodeFile = episodeFile; + RecycleBinPath = recycleBinPath; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMoveResult.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMoveResult.cs index e88a10d29..428bf1762 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMoveResult.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMoveResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.MediaFiles { @@ -6,10 +6,10 @@ namespace NzbDrone.Core.MediaFiles { public EpisodeFileMoveResult() { - OldFiles = new List<EpisodeFile>(); + OldFiles = new List<DeletedEpisodeFile>(); } public EpisodeFile EpisodeFile { get; set; } - public List<EpisodeFile> OldFiles { get; set; } + public List<DeletedEpisodeFile> OldFiles { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 5d4bb77f6..308a210df 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -67,7 +67,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport .ThenByDescending(e => e.LocalEpisode.Size)) { var localEpisode = importDecision.LocalEpisode; - var oldFiles = new List<EpisodeFile>(); + var oldFiles = new List<DeletedEpisodeFile>(); try { diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index 35a1fc800..e8a2400f7 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -9,12 +9,12 @@ namespace NzbDrone.Core.MediaFiles.Events { public LocalEpisode EpisodeInfo { get; private set; } public EpisodeFile ImportedEpisode { get; private set; } - public List<EpisodeFile> OldFiles { get; private set; } + public List<DeletedEpisodeFile> OldFiles { get; private set; } public bool NewDownload { get; private set; } public DownloadClientItemClientInfo DownloadClientInfo { get; set; } public string DownloadId { get; private set; } - public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, List<EpisodeFile> oldFiles, bool newDownload, DownloadClientItem downloadClientItem) + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, List<DeletedEpisodeFile> oldFiles, bool newDownload, DownloadClientItem downloadClientItem) { EpisodeInfo = episodeInfo; ImportedEpisode = importedEpisode; diff --git a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs index faf11e2a3..89d9c2340 100644 --- a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs +++ b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles public interface IRecycleBinProvider { void DeleteFolder(string path); - void DeleteFile(string path, string subfolder = ""); + string DeleteFile(string path, string subfolder = ""); void Empty(); void Cleanup(); } @@ -66,7 +66,7 @@ namespace NzbDrone.Core.MediaFiles } } - public void DeleteFile(string path, string subfolder = "") + public string DeleteFile(string path, string subfolder = "") { _logger.Debug("Attempting to send '{0}' to recycling bin", path); var recyclingBin = _configService.RecycleBin; @@ -82,6 +82,8 @@ namespace NzbDrone.Core.MediaFiles _diskProvider.DeleteFile(path); _logger.Debug("File has been permanently deleted: {0}", path); + + return null; } else { @@ -128,6 +130,8 @@ namespace NzbDrone.Core.MediaFiles SetLastWriteTime(destination, DateTime.UtcNow); _logger.Debug("File has been moved to the recycling bin: {0}", destination); + + return destination; } } diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs index 45ab383d7..6e4b77048 100644 --- a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs @@ -174,9 +174,10 @@ namespace NzbDrone.Core.MediaFiles if (oldFiles.Any()) { - environmentVariables.Add("Sonarr_DeletedRelativePaths", string.Join("|", oldFiles.Select(e => e.RelativePath))); - environmentVariables.Add("Sonarr_DeletedPaths", string.Join("|", oldFiles.Select(e => Path.Combine(series.Path, e.RelativePath)))); - environmentVariables.Add("Sonarr_DeletedDateAdded", string.Join("|", oldFiles.Select(e => e.DateAdded))); + environmentVariables.Add("Sonarr_DeletedRelativePaths", string.Join("|", oldFiles.Select(e => e.EpisodeFile.RelativePath))); + environmentVariables.Add("Sonarr_DeletedPaths", string.Join("|", oldFiles.Select(e => Path.Combine(series.Path, e.EpisodeFile.RelativePath)))); + environmentVariables.Add("Sonarr_DeletedDateAdded", string.Join("|", oldFiles.Select(e => e.EpisodeFile.DateAdded))); + environmentVariables.Add("Sonarr_DeletedRecycleBinPaths", string.Join("|", oldFiles.Select(e => e.RecycleBinPath ?? string.Empty))); } _logger.Debug("Executing external script: {0}", _configService.ScriptImportPath); diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 908271c6d..4910a1e57 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -57,14 +57,15 @@ namespace NzbDrone.Core.MediaFiles var file = existingFile.First(); var episodeFilePath = Path.Combine(localEpisode.Series.Path, file.RelativePath); var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(episodeFilePath)); + string recycleBinPath = null; if (_diskProvider.FileExists(episodeFilePath)) { _logger.Debug("Removing existing episode file: {0}", file); - _recycleBinProvider.DeleteFile(episodeFilePath, subfolder); + recycleBinPath = _recycleBinProvider.DeleteFile(episodeFilePath, subfolder); } - moveFileResult.OldFiles.Add(file); + moveFileResult.OldFiles.Add(new DeletedEpisodeFile(file, recycleBinPath)); _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 2a6483e18..d72f668e3 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -158,9 +158,10 @@ namespace NzbDrone.Core.Notifications.CustomScript if (message.OldFiles.Any()) { - environmentVariables.Add("Sonarr_DeletedRelativePaths", string.Join("|", message.OldFiles.Select(e => e.RelativePath))); - environmentVariables.Add("Sonarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => Path.Combine(series.Path, e.RelativePath)))); - environmentVariables.Add("Sonarr_DeletedDateAdded", string.Join("|", message.OldFiles.Select(e => e.DateAdded))); + environmentVariables.Add("Sonarr_DeletedRelativePaths", string.Join("|", message.OldFiles.Select(e => e.EpisodeFile.RelativePath))); + environmentVariables.Add("Sonarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => Path.Combine(series.Path, e.EpisodeFile.RelativePath)))); + environmentVariables.Add("Sonarr_DeletedDateAdded", string.Join("|", message.OldFiles.Select(e => e.EpisodeFile.DateAdded))); + environmentVariables.Add("Sonarr_DeletedRecycleBinPaths", string.Join("|", message.OldFiles.Select(e => e.RecycleBinPath ?? string.Empty))); } ExecuteScript(environmentVariables); diff --git a/src/NzbDrone.Core/Notifications/DownloadMessage.cs b/src/NzbDrone.Core/Notifications/DownloadMessage.cs index 62f8143d0..bd3e1188f 100644 --- a/src/NzbDrone.Core/Notifications/DownloadMessage.cs +++ b/src/NzbDrone.Core/Notifications/DownloadMessage.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Notifications public Series Series { get; set; } public LocalEpisode EpisodeInfo { get; set; } public EpisodeFile EpisodeFile { get; set; } - public List<EpisodeFile> OldFiles { get; set; } + public List<DeletedEpisodeFile> OldFiles { get; set; } public string SourcePath { get; set; } public DownloadClientItemClientInfo DownloadClientInfo { get; set; } public string DownloadId { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index 23e576b47..733eaa5e4 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Notifications.Synology { foreach (var oldFile in message.OldFiles) { - var fullPath = Path.Combine(message.Series.Path, oldFile.RelativePath); + var fullPath = Path.Combine(message.Series.Path, oldFile.EpisodeFile.RelativePath); _indexerProxy.DeleteFile(fullPath); } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index 22375baa7..2214a1f03 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -65,9 +65,10 @@ namespace NzbDrone.Core.Notifications.Webhook if (message.OldFiles.Any()) { - payload.DeletedFiles = message.OldFiles.ConvertAll(x => new WebhookEpisodeFile(x) + payload.DeletedFiles = message.OldFiles.ConvertAll(x => new WebhookEpisodeFile(x.EpisodeFile) { - Path = Path.Combine(message.Series.Path, x.RelativePath) + Path = Path.Combine(message.Series.Path, x.EpisodeFile.RelativePath), + RecycleBinPath = x.RecycleBinPath }); } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs index 9f392a33b..deeb9db95 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs @@ -37,5 +37,6 @@ namespace NzbDrone.Core.Notifications.Webhook public long Size { get; set; } public DateTime DateAdded { get; set; } public WebhookEpisodeFileMediaInfo MediaInfo { get; set; } + public string RecycleBinPath { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 060a14e6e..0c4212d61 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Parser.Model public ParsedEpisodeInfo FolderEpisodeInfo { get; set; } public Series Series { get; set; } public List<Episode> Episodes { get; set; } - public List<EpisodeFile> OldFiles { get; set; } + public List<DeletedEpisodeFile> OldFiles { get; set; } public QualityModel Quality { get; set; } public List<Language> Languages { get; set; } public MediaInfoModel MediaInfo { get; set; } From fc3a2e9ab2a7ae88320336d5288e94744091a93e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 21 Jan 2024 10:20:44 -0800 Subject: [PATCH 059/762] New: Added some extra pixels to grouped calendar events Closes #6395 --- frontend/src/Calendar/Events/CalendarEventGroup.css | 1 + frontend/src/Calendar/Events/CalendarEventGroup.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.css b/frontend/src/Calendar/Events/CalendarEventGroup.css index c52e0192d..990d994ec 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroup.css +++ b/frontend/src/Calendar/Events/CalendarEventGroup.css @@ -43,6 +43,7 @@ .expandContainer, .collapseContainer { display: flex; + align-items: center; justify-content: center; } diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.js b/frontend/src/Calendar/Events/CalendarEventGroup.js index 2130c11f9..2bec49df2 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroup.js +++ b/frontend/src/Calendar/Events/CalendarEventGroup.js @@ -224,16 +224,19 @@ class CalendarEventGroup extends Component { </div> { - showEpisodeInformation && + showEpisodeInformation ? <Link className={styles.expandContainer} component="div" onPress={this.onExpandPress} > +   <Icon name={icons.EXPAND} /> - </Link> +   + </Link> : + null } </div> ); From 3cd4c67ba12cd5e8cc00d3df8929555fc0ad5918 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 21 Jan 2024 10:25:28 -0800 Subject: [PATCH 060/762] New: Add download client name to pending items waiting for a specific client Closes #6274 --- .../Download/Pending/PendingReleaseService.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 6f16dfe3a..624dbdf46 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -46,6 +46,8 @@ namespace NzbDrone.Core.Download.Pending private readonly IConfigService _configService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly IRemoteEpisodeAggregationService _aggregationService; + private readonly IDownloadClientFactory _downloadClientFactory; + private readonly IIndexerFactory _indexerFactory; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -58,6 +60,8 @@ namespace NzbDrone.Core.Download.Pending IConfigService configService, ICustomFormatCalculationService formatCalculator, IRemoteEpisodeAggregationService aggregationService, + IDownloadClientFactory downloadClientFactory, + IIndexerFactory indexerFactory, IEventAggregator eventAggregator, Logger logger) { @@ -70,6 +74,8 @@ namespace NzbDrone.Core.Download.Pending _configService = configService; _formatCalculator = formatCalculator; _aggregationService = aggregationService; + _downloadClientFactory = downloadClientFactory; + _indexerFactory = indexerFactory; _eventAggregator = eventAggregator; _logger = logger; } @@ -107,9 +113,16 @@ namespace NzbDrone.Core.Download.Pending if (matchingReport.Reason != reason) { - _logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteEpisode, matchingReport.Reason, reason); - matchingReport.Reason = reason; - _repository.Update(matchingReport); + if (matchingReport.Reason == PendingReleaseReason.DownloadClientUnavailable) + { + _logger.Debug("The release {0} is already pending with reason {1}, not changing reason", decision.RemoteEpisode, matchingReport.Reason); + } + else + { + _logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteEpisode, matchingReport.Reason, reason); + matchingReport.Reason = reason; + _repository.Update(matchingReport); + } } else { @@ -355,6 +368,16 @@ namespace NzbDrone.Core.Download.Pending timeleft = TimeSpan.Zero; } + string downloadClientName = null; + var indexer = _indexerFactory.Find(pendingRelease.Release.IndexerId); + + if (indexer is { DownloadClientId: > 0 }) + { + var downloadClient = _downloadClientFactory.Find(indexer.DownloadClientId); + + downloadClientName = downloadClient?.Name; + } + var queue = new Queue.Queue { Id = GetQueueId(pendingRelease, episode), @@ -371,7 +394,8 @@ namespace NzbDrone.Core.Download.Pending Added = pendingRelease.Added, Status = pendingRelease.Reason.ToString(), Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol, - Indexer = pendingRelease.RemoteEpisode.Release.Indexer + Indexer = pendingRelease.RemoteEpisode.Release.Indexer, + DownloadClient = downloadClientName }; return queue; From 3c1ca6ea4e351c4171eb478b5c8b4f69d5bbda59 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 21 Jan 2024 11:09:54 +0200 Subject: [PATCH 061/762] New: Expand seasons with all episodes having missing air dates --- frontend/src/Series/Details/SeriesDetailsSeason.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.js b/frontend/src/Series/Details/SeriesDetailsSeason.js index 5605ad2d0..4268dddff 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.js +++ b/frontend/src/Series/Details/SeriesDetailsSeason.js @@ -129,10 +129,8 @@ class SeriesDetailsSeason extends Component { items } = this.props; - const expand = _.some(items, (item) => { - return isAfter(item.airDateUtc) || - isAfter(item.airDateUtc, { days: -30 }); - }); + const expand = _.some(items, (item) => isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })) || + items.every((item) => !item.airDateUtc); onExpandPress(seasonNumber, expand && seasonNumber > 0); } From 9f50166fa62a71d0a23e2c2d331651792285dc0e Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Tue, 23 Jan 2024 05:55:33 +0100 Subject: [PATCH 062/762] Fixed: Regular Expression Custom Format translation --- .../Specifications/EditSpecificationModalContent.js | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js index 90e2bd142..855832620 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js @@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) { {...otherProps} > { - fields && fields.some((x) => x.label === translate('RegularExpression')) && + fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) && <Alert kind={kinds.INFO}> <div> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 3a0b392e7..9c4e616e4 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -270,7 +270,7 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size", "CustomFormatsSpecificationMinimumSize": "Minimum Size", "CustomFormatsSpecificationMinimumSizeHelpText": "Release must be greater than this size", - "CustomFormatsSpecificationRegularExpression": "Language", + "CustomFormatsSpecificationRegularExpression": "Regular Expression", "CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive", "CustomFormatsSpecificationReleaseGroup": "Release Group", "CustomFormatsSpecificationResolution": "Resolution", From 7d0d503a5e132cda3c03d6f7cd7b51c9c80740de Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 21 Jan 2024 05:25:21 +0200 Subject: [PATCH 063/762] New: Display database migration version in Status --- frontend/src/System/Status/About/About.js | 7 +++++++ src/NzbDrone.Core/Localization/Core/en.json | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 7cd66edf2..84114b0dc 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -24,6 +24,7 @@ class About extends Component { runtimeVersion, databaseVersion, databaseType, + migrationVersion, appData, startupPath, mode, @@ -76,6 +77,11 @@ class About extends Component { data={`${titleCase(databaseType)} ${databaseVersion}`} /> + <DescriptionListItem + title={translate('DatabaseMigration')} + data={migrationVersion} + /> + <DescriptionListItem title={translate('AppDataDirectory')} data={appData} @@ -117,6 +123,7 @@ About.propTypes = { isDocker: PropTypes.bool.isRequired, databaseType: PropTypes.string.isRequired, databaseVersion: PropTypes.string.isRequired, + migrationVersion: PropTypes.number.isRequired, appData: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired, mode: PropTypes.string.isRequired, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 9c4e616e4..ad06b5924 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -87,7 +87,7 @@ "Any": "Any", "ApiKey": "API Key", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file", - "AppDataDirectory": "AppData directory", + "AppDataDirectory": "AppData Directory", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppUpdated": "{appName} Updated", "AppUpdatedVersion": "{appName} has been updated to version `{version}`, in order to get the latest changes you'll need to reload {appName} ", @@ -285,6 +285,7 @@ "DailyEpisodeTypeFormat": "Date ({format})", "Dash": "Dash", "Database": "Database", + "DatabaseMigration": "Database Migration", "Date": "Date", "Dates": "Dates", "Day": "Day", @@ -389,7 +390,6 @@ "DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}", - "DownloadClientPriorityHelpText": "Download Client Priority from 1 (Highest) to 50 (Lowest). Default: 1. Round-Robin is used for clients with the same priority.", "DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}", "DownloadClientDelugeTorrentStateError": "Deluge is reporting an error", "DownloadClientDelugeValidationLabelPluginFailure": "Configuration of label failed", @@ -437,6 +437,7 @@ "DownloadClientPneumaticSettingsNzbFolderHelpText": "This folder will need to be reachable from XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Strm Folder", "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone", + "DownloadClientPriorityHelpText": "Download Client Priority from 1 (Highest) to 50 (Lowest). Default: 1. Round-Robin is used for clients with the same priority.", "DownloadClientQbittorrentSettingsContentLayout": "Content Layout", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", @@ -1833,7 +1834,7 @@ "StartImport": "Start Import", "StartProcessing": "Start Processing", "Started": "Started", - "StartupDirectory": "Startup directory", + "StartupDirectory": "Startup Directory", "Status": "Status", "StopSelecting": "Stop Selecting", "Style": "Style", From 31baed4b2c2406e48b8defa51352a13adb6d470f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:10:24 +0200 Subject: [PATCH 064/762] Fixed: Sorting by name in Manage Indexer and Download Client modals --- frontend/src/Store/Actions/Settings/downloadClients.js | 7 ++++++- frontend/src/Store/Actions/Settings/indexers.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 1f4765121..aee945ef5 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -94,7 +94,12 @@ export default { items: [], pendingChanges: {}, sortKey: 'name', - sortDirection: sortDirections.DESCENDING + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + name: function(item) { + return item.name.toLowerCase(); + } + } }, // diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index 55e08a5c0..28aa9039d 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -99,7 +99,12 @@ export default { items: [], pendingChanges: {}, sortKey: 'name', - sortDirection: sortDirections.DESCENDING + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + name: function(item) { + return item.name.toLowerCase(); + } + } }, // From 345854d0fe9b65a561fdab12aac688782a420aa5 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 22 Jan 2024 20:56:35 -0800 Subject: [PATCH 065/762] New: Optionally remove from queue by changing category to 'Post-Import Category' when configured Closes #6023 --- frontend/src/Activity/Queue/Queue.js | 13 +- frontend/src/Activity/Queue/QueueRow.js | 3 + ...temsModal.css => RemoveQueueItemModal.css} | 0 ...css.d.ts => RemoveQueueItemModal.css.d.ts} | 0 .../Activity/Queue/RemoveQueueItemModal.js | 171 ------------- .../Activity/Queue/RemoveQueueItemModal.tsx | 230 ++++++++++++++++++ .../Activity/Queue/RemoveQueueItemsModal.js | 174 ------------- .../src/Components/Form/FormInputGroup.js | 1 + frontend/src/Store/Actions/queueActions.js | 10 +- .../Download/Clients/Aria2/Aria2.cs | 2 +- .../Clients/Blackhole/TorrentBlackhole.cs | 2 +- .../Clients/Blackhole/UsenetBlackhole.cs | 2 +- .../Download/Clients/Deluge/Deluge.cs | 2 +- .../DownloadStation/TorrentDownloadStation.cs | 2 +- .../DownloadStation/UsenetDownloadStation.cs | 2 +- .../Download/Clients/Flood/Flood.cs | 2 +- .../FreeboxDownload/TorrentFreeboxDownload.cs | 2 +- .../Download/Clients/Hadouken/Hadouken.cs | 2 +- .../Download/Clients/NzbVortex/NzbVortex.cs | 2 +- .../Download/Clients/Nzbget/Nzbget.cs | 4 +- .../Download/Clients/Pneumatic/Pneumatic.cs | 2 +- .../Clients/QBittorrent/QBittorrent.cs | 2 +- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 4 +- .../Clients/Transmission/TransmissionBase.cs | 2 +- .../Download/Clients/rTorrent/RTorrent.cs | 2 +- .../Download/Clients/uTorrent/UTorrent.cs | 2 +- .../Download/DownloadClientItem.cs | 6 +- src/NzbDrone.Core/Localization/Core/en.json | 25 +- src/NzbDrone.Core/Queue/Queue.cs | 1 + src/NzbDrone.Core/Queue/QueueService.cs | 3 +- src/Sonarr.Api.V3/Queue/QueueController.cs | 23 +- src/Sonarr.Api.V3/Queue/QueueResource.cs | 2 + 32 files changed, 317 insertions(+), 383 deletions(-) rename frontend/src/Activity/Queue/{RemoveQueueItemsModal.css => RemoveQueueItemModal.css} (100%) rename frontend/src/Activity/Queue/{RemoveQueueItemsModal.css.d.ts => RemoveQueueItemModal.css.d.ts} (100%) delete mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.js create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.tsx delete mode 100644 frontend/src/Activity/Queue/RemoveQueueItemsModal.js diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 633357b7e..30f5260cb 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected'; import QueueFilterModal from './QueueFilterModal'; import QueueOptionsConnector from './QueueOptionsConnector'; import QueueRowConnector from './QueueRowConnector'; -import RemoveQueueItemsModal from './RemoveQueueItemsModal'; +import RemoveQueueItemModal from './RemoveQueueItemModal'; class Queue extends Component { @@ -305,9 +305,16 @@ class Queue extends Component { } </PageContentBody> - <RemoveQueueItemsModal + <RemoveQueueItemModal isOpen={isConfirmRemoveModalOpen} selectedCount={selectedCount} + canChangeCategory={isConfirmRemoveModalOpen && ( + selectedIds.every((id) => { + const item = items.find((i) => i.id === id); + + return !!(item && item.downloadClientHasPostImportCategory); + }) + )} canIgnore={isConfirmRemoveModalOpen && ( selectedIds.every((id) => { const item = items.find((i) => i.id === id); @@ -315,7 +322,7 @@ class Queue extends Component { return !!(item && item.seriesId && item.episodeId); }) )} - allPending={isConfirmRemoveModalOpen && ( + pending={isConfirmRemoveModalOpen && ( selectedIds.every((id) => { const item = items.find((i) => i.id === id); diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 95ff2527e..f143ace3f 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -99,6 +99,7 @@ class QueueRow extends Component { indexer, outputPath, downloadClient, + downloadClientHasPostImportCategory, estimatedCompletionTime, added, timeleft, @@ -420,6 +421,7 @@ class QueueRow extends Component { <RemoveQueueItemModal isOpen={isRemoveQueueItemModalOpen} sourceTitle={title} + canChangeCategory={!!downloadClientHasPostImportCategory} canIgnore={!!series} isPending={isPending} onRemovePress={this.onRemoveQueueItemModalConfirmed} @@ -450,6 +452,7 @@ QueueRow.propTypes = { indexer: PropTypes.string, outputPath: PropTypes.string, downloadClient: PropTypes.string, + downloadClientHasPostImportCategory: PropTypes.bool, estimatedCompletionTime: PropTypes.string, added: PropTypes.string, timeleft: PropTypes.string, diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.css b/frontend/src/Activity/Queue/RemoveQueueItemModal.css similarity index 100% rename from frontend/src/Activity/Queue/RemoveQueueItemsModal.css rename to frontend/src/Activity/Queue/RemoveQueueItemModal.css diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.css.d.ts b/frontend/src/Activity/Queue/RemoveQueueItemModal.css.d.ts similarity index 100% rename from frontend/src/Activity/Queue/RemoveQueueItemsModal.css.d.ts rename to frontend/src/Activity/Queue/RemoveQueueItemModal.css.d.ts diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js deleted file mode 100644 index 0cf7af855..000000000 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.js +++ /dev/null @@ -1,171 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -class RemoveQueueItemModal extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - remove: true, - blocklist: false, - skipRedownload: false - }; - } - - // - // Control - - resetState = function() { - this.setState({ - remove: true, - blocklist: false, - skipRedownload: false - }); - }; - - // - // Listeners - - onRemoveChange = ({ value }) => { - this.setState({ remove: value }); - }; - - onBlocklistChange = ({ value }) => { - this.setState({ blocklist: value }); - }; - - onSkipRedownloadChange = ({ value }) => { - this.setState({ skipRedownload: value }); - }; - - onRemoveConfirmed = () => { - const state = this.state; - - this.resetState(); - this.props.onRemovePress(state); - }; - - onModalClose = () => { - this.resetState(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isOpen, - sourceTitle, - canIgnore, - isPending - } = this.props; - - const { remove, blocklist, skipRedownload } = this.state; - - return ( - <Modal - isOpen={isOpen} - size={sizes.MEDIUM} - onModalClose={this.onModalClose} - > - <ModalContent - onModalClose={this.onModalClose} - > - <ModalHeader> - {translate('RemoveQueueItem', { sourceTitle })} - </ModalHeader> - - <ModalBody> - <div> - {translate('RemoveQueueItemConfirmation', { sourceTitle })} - </div> - - { - isPending ? - null : - <FormGroup> - <FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="remove" - value={remove} - helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')} - isDisabled={!canIgnore} - onChange={this.onRemoveChange} - /> - </FormGroup> - } - - <FormGroup> - <FormLabel>{translate('BlocklistRelease')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="blocklist" - value={blocklist} - helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')} - onChange={this.onBlocklistChange} - /> - </FormGroup> - - { - blocklist ? - <FormGroup> - <FormLabel>{translate('SkipRedownload')}</FormLabel> - <FormInputGroup - type={inputTypes.CHECK} - name="skipRedownload" - value={skipRedownload} - helpText={translate('SkipRedownloadHelpText')} - onChange={this.onSkipRedownloadChange} - /> - </FormGroup> : - null - } - </ModalBody> - - <ModalFooter> - <Button onPress={this.onModalClose}> - {translate('Close')} - </Button> - - <Button - kind={kinds.DANGER} - onPress={this.onRemoveConfirmed} - > - {translate('Remove')} - </Button> - </ModalFooter> - </ModalContent> - </Modal> - ); - } -} - -RemoveQueueItemModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - sourceTitle: PropTypes.string.isRequired, - canIgnore: PropTypes.bool.isRequired, - isPending: PropTypes.bool.isRequired, - onRemovePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx new file mode 100644 index 000000000..4348f818c --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx @@ -0,0 +1,230 @@ +import React, { useCallback, useMemo, useState } from 'react'; +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 Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './RemoveQueueItemModal.css'; + +interface RemovePressProps { + remove: boolean; + changeCategory: boolean; + blocklist: boolean; + skipRedownload: boolean; +} + +interface RemoveQueueItemModalProps { + isOpen: boolean; + sourceTitle: string; + canChangeCategory: boolean; + canIgnore: boolean; + isPending: boolean; + selectedCount?: number; + onRemovePress(props: RemovePressProps): void; + onModalClose: () => void; +} + +type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore'; +type BlocklistMethod = + | 'doNotBlocklist' + | 'blocklistAndSearch' + | 'blocklistOnly'; + +function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { + const { + isOpen, + sourceTitle, + canIgnore, + canChangeCategory, + isPending, + selectedCount, + onRemovePress, + onModalClose, + } = props; + + const multipleSelected = selectedCount && selectedCount > 1; + + const [removalMethod, setRemovalMethod] = + useState<RemovalMethod>('removeFromClient'); + const [blocklistMethod, setBlocklistMethod] = + useState<BlocklistMethod>('doNotBlocklist'); + + const { title, message } = useMemo(() => { + if (!selectedCount) { + return { + title: translate('RemoveQueueItem', { sourceTitle }), + message: translate('RemoveQueueItemConfirmation', { sourceTitle }), + }; + } + + if (selectedCount === 1) { + return { + title: translate('RemoveSelectedItem'), + message: translate('RemoveSelectedItemQueueMessageText'), + }; + } + + return { + title: translate('RemoveSelectedItems'), + message: translate('RemoveSelectedItemsQueueMessageText', { + selectedCount, + }), + }; + }, [sourceTitle, selectedCount]); + + const removalMethodOptions = useMemo(() => { + return [ + { + key: 'removeFromClient', + value: translate('RemoveFromDownloadClient'), + hint: multipleSelected + ? translate('RemoveMultipleFromDownloadClientHint') + : translate('RemoveFromDownloadClientHint'), + }, + { + key: 'changeCategory', + value: translate('ChangeCategory'), + isDisabled: !canChangeCategory, + hint: multipleSelected + ? translate('ChangeCategoryMultipleHint') + : translate('ChangeCategoryHint'), + }, + { + key: 'ignore', + value: multipleSelected + ? translate('IgnoreDownloads') + : translate('IgnoreDownload'), + isDisabled: !canIgnore, + hint: multipleSelected + ? translate('IgnoreDownloadsHint') + : translate('IgnoreDownloadHint'), + }, + ]; + }, [canChangeCategory, canIgnore, multipleSelected]); + + const blocklistMethodOptions = useMemo(() => { + return [ + { + key: 'doNotBlocklist', + value: translate('DoNotBlocklist'), + hint: translate('DoNotBlocklistHint'), + }, + { + key: 'blocklistAndSearch', + value: translate('BlocklistAndSearch'), + hint: multipleSelected + ? translate('BlocklistAndSearchMultipleHint') + : translate('BlocklistAndSearchHint'), + }, + { + key: 'blocklistOnly', + value: translate('BlocklistOnly'), + hint: multipleSelected + ? translate('BlocklistMultipleOnlyHint') + : translate('BlocklistOnlyHint'), + }, + ]; + }, [multipleSelected]); + + const handleRemovalMethodChange = useCallback( + ({ value }: { value: RemovalMethod }) => { + setRemovalMethod(value); + }, + [setRemovalMethod] + ); + + const handleBlocklistMethodChange = useCallback( + ({ value }: { value: BlocklistMethod }) => { + setBlocklistMethod(value); + }, + [setBlocklistMethod] + ); + + const handleConfirmRemove = useCallback(() => { + onRemovePress({ + remove: removalMethod === 'removeFromClient', + changeCategory: removalMethod === 'changeCategory', + blocklist: blocklistMethod !== 'doNotBlocklist', + skipRedownload: blocklistMethod === 'blocklistOnly', + }); + + setRemovalMethod('removeFromClient'); + setBlocklistMethod('doNotBlocklist'); + }, [ + removalMethod, + blocklistMethod, + setRemovalMethod, + setBlocklistMethod, + onRemovePress, + ]); + + const handleModalClose = useCallback(() => { + setRemovalMethod('removeFromClient'); + setBlocklistMethod('doNotBlocklist'); + + onModalClose(); + }, [setRemovalMethod, setBlocklistMethod, onModalClose]); + + return ( + <Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}> + <ModalContent onModalClose={handleModalClose}> + <ModalHeader>{title}</ModalHeader> + + <ModalBody> + <div className={styles.message}>{message}</div> + + {isPending ? null : ( + <FormGroup> + <FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="removalMethod" + value={removalMethod} + values={removalMethodOptions} + isDisabled={!canChangeCategory && !canIgnore} + helpTextWarning={translate( + 'RemoveQueueItemRemovalMethodHelpTextWarning' + )} + onChange={handleRemovalMethodChange} + /> + </FormGroup> + )} + + <FormGroup> + <FormLabel> + {multipleSelected + ? translate('BlocklistReleases') + : translate('BlocklistRelease')} + </FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="blocklistMethod" + value={blocklistMethod} + values={blocklistMethodOptions} + helpText={translate('BlocklistReleaseHelpText')} + onChange={handleBlocklistMethodChange} + /> + </FormGroup> + </ModalBody> + + <ModalFooter> + <Button onPress={handleModalClose}>{translate('Close')}</Button> + + <Button kind={kinds.DANGER} onPress={handleConfirmRemove}> + {translate('Remove')} + </Button> + </ModalFooter> + </ModalContent> + </Modal> + ); +} + +export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js deleted file mode 100644 index 18ea39aea..000000000 --- a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js +++ /dev/null @@ -1,174 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './RemoveQueueItemsModal.css'; - -class RemoveQueueItemsModal extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - remove: true, - blocklist: false, - skipRedownload: false - }; - } - - // - // Control - - resetState = function() { - this.setState({ - remove: true, - blocklist: false, - skipRedownload: false - }); - }; - - // - // Listeners - - onRemoveChange = ({ value }) => { - this.setState({ remove: value }); - }; - - onBlocklistChange = ({ value }) => { - this.setState({ blocklist: value }); - }; - - onSkipRedownloadChange = ({ value }) => { - this.setState({ skipRedownload: value }); - }; - - onRemoveConfirmed = () => { - const state = this.state; - - this.resetState(); - this.props.onRemovePress(state); - }; - - onModalClose = () => { - this.resetState(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isOpen, - selectedCount, - canIgnore, - allPending - } = this.props; - - const { remove, blocklist, skipRedownload } = this.state; - - return ( - <Modal - isOpen={isOpen} - size={sizes.MEDIUM} - onModalClose={this.onModalClose} - > - <ModalContent - onModalClose={this.onModalClose} - > - <ModalHeader> - {selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')} - </ModalHeader> - - <ModalBody> - <div className={styles.message}> - {selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')} - </div> - - { - allPending ? - null : - <FormGroup> - <FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="remove" - value={remove} - helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')} - isDisabled={!canIgnore} - onChange={this.onRemoveChange} - /> - </FormGroup> - } - - <FormGroup> - <FormLabel> - {selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')} - </FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="blocklist" - value={blocklist} - helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')} - onChange={this.onBlocklistChange} - /> - </FormGroup> - - { - blocklist ? - <FormGroup> - <FormLabel>{translate('SkipRedownload')}</FormLabel> - <FormInputGroup - type={inputTypes.CHECK} - name="skipRedownload" - value={skipRedownload} - helpText={translate('SkipRedownloadHelpText')} - onChange={this.onSkipRedownloadChange} - /> - </FormGroup> : - null - } - </ModalBody> - - <ModalFooter> - <Button onPress={this.onModalClose}> - {translate('Close')} - </Button> - - <Button - kind={kinds.DANGER} - onPress={this.onRemoveConfirmed} - > - {translate('Remove')} - </Button> - </ModalFooter> - </ModalContent> - </Modal> - ); - } -} - -RemoveQueueItemsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - selectedCount: PropTypes.number.isRequired, - canIgnore: PropTypes.bool.isRequired, - allPending: PropTypes.bool.isRequired, - onRemovePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default RemoveQueueItemsModal; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 49f08c90b..d3b3eb206 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -264,6 +264,7 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), + isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), min: PropTypes.number, diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index fa4c3c473..dff490d12 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -430,13 +430,14 @@ export const actionHandlers = handleThunks({ id, remove, blocklist, - skipRedownload + skipRedownload, + changeCategory } = payload; dispatch(updateItem({ section: paged, id, isRemoving: true })); const promise = createAjaxRequest({ - url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, + url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, method: 'DELETE' }).request; @@ -454,7 +455,8 @@ export const actionHandlers = handleThunks({ ids, remove, blocklist, - skipRedownload + skipRedownload, + changeCategory } = payload; dispatch(batchActions([ @@ -470,7 +472,7 @@ export const actionHandlers = handleThunks({ ])); const promise = createAjaxRequest({ - url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, + url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, method: 'DELETE', dataType: 'json', contentType: 'application/json', diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 621b8937e..970d09d35 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -134,7 +134,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 CanMoveFiles = false, CanBeRemoved = torrent.Status == "complete", Category = null, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.InfoHash?.ToUpper(), IsEncrypted = false, Message = torrent.ErrorMessage, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index 282ededa1..8364a1fb2 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { yield return new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, Category = "sonarr", Title = item.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 3a7105ba9..e1eb75905 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { yield return new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, Category = "sonarr", Title = item.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 10716c699..3856e7a70 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge item.Title = torrent.Name; item.Category = Settings.TvCategory; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath)); item.OutputPath = outputPath + torrent.Name; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 612be692d..8ecda831e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var item = new DownloadClientItem() { Category = Settings.TvCategory, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = CreateDownloadId(torrent.Id, serialNumber), Title = torrent.Title, TotalSize = torrent.Size, diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 6f89845a9..0571847e2 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var item = new DownloadClientItem() { Category = Settings.TvCategory, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = CreateDownloadId(nzb.Id, serialNumber), Title = nzb.Title, TotalSize = nzb.Size, diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index b770792e1..60b153441 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.Flood var item = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.Key, Title = properties.Name, OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(properties.Directory)), diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs index 07f435a34..88248e4b5 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload Category = Settings.Category, Title = torrent.Name, TotalSize = torrent.Size, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), RemainingSize = (long)(torrent.Size * (double)(1 - ((double)torrent.ReceivedPrct / 10000))), RemainingTime = torrent.Eta <= 0 ? null : TimeSpan.FromSeconds(torrent.Eta), SeedRatio = torrent.StopRatio <= 0 ? 0 : torrent.StopRatio / 100, diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index a29be7f4c..59f28e34d 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken var item = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.InfoHash.ToUpper(), OutputPath = outputPath + torrent.Name, RemainingSize = torrent.TotalSize - torrent.DownloadedBytes, diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index dbdfdb7c4..2a12fc364 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { var queueItem = new DownloadClientItem(); - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString(); queueItem.Category = vortexQueueItem.GroupName; queueItem.Title = vortexQueueItem.UiTitle; diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index d7956318e..fc14c1496 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget queueItem.Title = item.NzbName; queueItem.TotalSize = totalSize; queueItem.Category = item.Category; - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.CanMoveFiles = true; queueItem.CanBeRemoved = true; @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var historyItem = new DownloadClientItem(); var itemDir = item.FinalDir.IsNullOrWhiteSpace() ? item.DestDir : item.FinalDir; - historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 920279263..6797c0b0e 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic var historyItem = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = GetDownloadClientId(file), Title = title, diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index e523992ad..e8917a0c9 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label, Title = torrent.Name, TotalSize = torrent.Size, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()), RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)), RemainingTime = GetRemainingTime(torrent), SeedRatio = torrent.Ratio diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 5d9003849..a1c856cfb 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } var queueItem = new DownloadClientItem(); - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.DownloadId = sabQueueItem.Id; queueItem.Category = sabQueueItem.Category; queueItem.Title = sabQueueItem.Title; @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var historyItem = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = sabHistoryItem.Id, Category = sabHistoryItem.Category, Title = sabHistoryItem.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 48a268275..59113cbab 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Category = Settings.TvCategory; item.Title = torrent.Name; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); item.OutputPath = GetOutputPath(outputPath, torrent); item.TotalSize = torrent.TotalSize; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index d1e129949..5705e33a3 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } var item = new DownloadClientItem(); - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); item.Title = torrent.Name; item.DownloadId = torrent.Hash; item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path)); diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index cecc76dd7..5b93a1d5d 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent item.Title = torrent.Name; item.TotalSize = torrent.Size; item.Category = torrent.Label; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); item.RemainingSize = torrent.Remaining; item.SeedRatio = torrent.Ratio; diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 671dae4ed..6dd1b6173 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -37,9 +37,10 @@ namespace NzbDrone.Core.Download public string Type { get; set; } public int Id { get; set; } public string Name { get; set; } + public bool HasPostImportCategory { get; set; } public static DownloadClientItemClientInfo FromDownloadClient<TSettings>( - DownloadClientBase<TSettings> downloadClient) + DownloadClientBase<TSettings> downloadClient, bool hasPostImportCategory) where TSettings : IProviderConfig, new() { return new DownloadClientItemClientInfo @@ -47,7 +48,8 @@ namespace NzbDrone.Core.Download Protocol = downloadClient.Protocol, Type = downloadClient.Name, Id = downloadClient.Definition.Id, - Name = downloadClient.Definition.Name + Name = downloadClient.Definition.Name, + HasPostImportCategory = hasPostImportCategory }; } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ad06b5924..f5e78bbd4 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -153,9 +153,15 @@ "BlackholeWatchFolder": "Watch Folder", "BlackholeWatchFolderHelpText": "Folder from which {appName} should import completed downloads", "Blocklist": "Blocklist", + "BlocklistAndSearch": "Blocklist and Search", + "BlocklistAndSearchHint": "Start a search for a replacement after blocklisting", + "BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting", "BlocklistLoadError": "Unable to load blocklist", + "BlocklistMultipleOnlyHint": "Blocklist without searching for replacements", + "BlocklistOnly": "Blocklist Only", + "BlocklistOnlyHint": "Blocklist without searching for a replacement", "BlocklistRelease": "Blocklist Release", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Starts a search for this episode again and prevents this release from being grabbed again", + "BlocklistReleaseHelpText": "Blocks this release from being redownloaded by {appName} via RSS or Automatic Search", "BlocklistReleases": "Blocklist Releases", "Branch": "Branch", "BranchUpdate": "Branch to use to update {appName}", @@ -188,6 +194,9 @@ "CertificateValidation": "Certificate Validation", "CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.", "Certification": "Certification", + "ChangeCategory": "Change Category", + "ChangeCategoryHint": "Changes download to the 'Post-Import Category' from Download Client", + "ChangeCategoryMultipleHint": "Changes downloads to the 'Post-Import Category' from Download Client", "ChangeFileDate": "Change File Date", "ChangeFileDateHelpText": "Change file date on import/rescan", "CheckDownloadClientForDetails": "check download client for more details", @@ -377,6 +386,8 @@ "DisabledForLocalAddresses": "Disabled for Local Addresses", "Discord": "Discord", "DiskSpace": "Disk Space", + "DoNotBlocklist": "Do not Blocklist", + "DoNotBlocklistHint": "Remove without blocklisting", "DoNotPrefer": "Do not Prefer", "DoNotUpgradeAutomatically": "Do not Upgrade Automatically", "Docker": "Docker", @@ -754,6 +765,10 @@ "IconForFinalesHelpText": "Show icon for series/season finales based on available episode information", "IconForSpecials": "Icon for Specials", "IconForSpecialsHelpText": "Show icon for special episodes (season 0)", + "IgnoreDownload": "Ignore Download", + "IgnoreDownloads": "Ignore Downloads", + "IgnoreDownloadHint": "Stops {appName} from processing this download further", + "IgnoreDownloadsHint": "Stops {appName} from processing these downloads further", "Ignored": "Ignored", "IgnoredAddresses": "Ignored Addresses", "Images": "Images", @@ -1596,11 +1611,15 @@ "RemoveFailedDownloadsHelpText": "Remove failed downloads from download client history", "RemoveFilter": "Remove filter", "RemoveFromBlocklist": "Remove from Blocklist", - "RemoveFromDownloadClient": "Remove From Download Client", - "RemoveFromDownloadClientHelpTextWarning": "Removing will remove the download and the file(s) from the download client.", + "RemoveFromDownloadClient": "Remove from Download Client", + "RemoveFromDownloadClientHint": "Removes download and file(s) from download client", "RemoveFromQueue": "Remove from queue", + "RemoveMultipleFromDownloadClientHint": "Removes downloads and files from download client", "RemoveQueueItem": "Remove - {sourceTitle}", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.", "RemoveQueueItemConfirmation": "Are you sure you want to remove '{sourceTitle}' from the queue?", + "RemoveQueueItemRemovalMethod": "Removal Method", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.", "RemoveRootFolder": "Remove root folder", "RemoveSelected": "Remove Selected", "RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?", diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 15ff7948a..c5d2a123a 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Queue public RemoteEpisode RemoteEpisode { get; set; } public DownloadProtocol Protocol { get; set; } public string DownloadClient { get; set; } + public bool DownloadClientHasPostImportCategory { get; set; } public string Indexer { get; set; } public string OutputPath { get; set; } public string ErrorMessage { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 6b4aadb4c..3d7078223 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -80,7 +80,8 @@ namespace NzbDrone.Core.Queue DownloadClient = trackedDownload.DownloadItem.DownloadClientInfo.Name, Indexer = trackedDownload.Indexer, OutputPath = trackedDownload.DownloadItem.OutputPath.ToString(), - Added = trackedDownload.Added + Added = trackedDownload.Added, + DownloadClientHasPostImportCategory = trackedDownload.DownloadItem.DownloadClientInfo.HasPostImportCategory }; queue.Id = HashConverter.GetHashInt31($"trackedDownload-{trackedDownload.DownloadClient}-{trackedDownload.DownloadItem.DownloadId}-ep{episode?.Id ?? 0}"); diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 744fedda3..8884ef4a6 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -71,7 +71,7 @@ namespace Sonarr.Api.V3.Queue } [RestDeleteById] - public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false) + public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -89,12 +89,12 @@ namespace Sonarr.Api.V3.Queue throw new NotFoundException(); } - Remove(trackedDownload, removeFromClient, blocklist, skipRedownload); + Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); } [HttpDelete("bulk")] - public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false) + public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false) { var trackedDownloadIds = new List<string>(); var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>(); @@ -125,7 +125,7 @@ namespace Sonarr.Api.V3.Queue foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId)) { - Remove(trackedDownload, removeFromClient, blocklist, skipRedownload); + Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); } @@ -292,7 +292,7 @@ namespace Sonarr.Api.V3.Queue _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); } - private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload) + private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory) { if (removeFromClient) { @@ -305,13 +305,24 @@ namespace Sonarr.Api.V3.Queue downloadClient.RemoveItem(trackedDownload.DownloadItem, true); } + else if (changeCategory) + { + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.MarkItemAsImported(trackedDownload.DownloadItem); + } if (blocklist) { _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload); } - if (!removeFromClient && !blocklist) + if (!removeFromClient && !blocklist && !changeCategory) { if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) { diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index 6aaf3b1ed..e5152f1d7 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -38,6 +38,7 @@ namespace Sonarr.Api.V3.Queue public string DownloadId { get; set; } public DownloadProtocol Protocol { get; set; } public string DownloadClient { get; set; } + public bool DownloadClientHasPostImportCategory { get; set; } public string Indexer { get; set; } public string OutputPath { get; set; } public bool EpisodeHasFile { get; set; } @@ -81,6 +82,7 @@ namespace Sonarr.Api.V3.Queue DownloadId = model.DownloadId, Protocol = model.Protocol, DownloadClient = model.DownloadClient, + DownloadClientHasPostImportCategory = model.DownloadClientHasPostImportCategory, Indexer = model.Indexer, OutputPath = model.OutputPath, EpisodeHasFile = model.Episode?.HasFile ?? false From b64c52a846529233d796014e1537b96e8f48ff7f Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Tue, 23 Jan 2024 04:58:49 +0000 Subject: [PATCH 066/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 730b3b9d5..51fbfb3b2 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -5386,6 +5386,14 @@ "type": "boolean", "default": false } + }, + { + "name": "changeCategory", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -5424,6 +5432,14 @@ "type": "boolean", "default": false } + }, + { + "name": "changeCategory", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "requestBody": { @@ -10364,6 +10380,9 @@ "type": "string", "nullable": true }, + "downloadClientHasPostImportCategory": { + "type": "boolean" + }, "indexer": { "type": "string", "nullable": true From 46367d202393fc229a1574ffbb7a8e5f4c108b2a Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 23 Jan 2024 04:57:39 +0000 Subject: [PATCH 067/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dani Talens <databio@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 85 +++++- src/NzbDrone.Core/Localization/Core/cs.json | 1 - src/NzbDrone.Core/Localization/Core/de.json | 2 - src/NzbDrone.Core/Localization/Core/es.json | 156 +++++++++- src/NzbDrone.Core/Localization/Core/fi.json | 2 - src/NzbDrone.Core/Localization/Core/fr.json | 37 ++- src/NzbDrone.Core/Localization/Core/hu.json | 279 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/id.json | 1 - src/NzbDrone.Core/Localization/Core/nl.json | 1 - .../Localization/Core/pt_BR.json | 6 +- src/NzbDrone.Core/Localization/Core/ru.json | 1 - .../Localization/Core/zh_CN.json | 2 - 12 files changed, 540 insertions(+), 33 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 51bb50024..6ee9776a1 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -269,7 +269,6 @@ "AnimeEpisodeTypeDescription": "Episodis publicats amb un número d'episodi absolut", "CalendarLegendEpisodeDownloadedTooltip": "L'episodi s'ha baixat i ordenat", "AnimeEpisodeTypeFormat": "Número absolut d'episodi ({format}}", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Torna a cercar d'aquest episodi i evita que es torni a capturar aquesta versió", "CalendarLegendEpisodeDownloadingTooltip": "L'episodi s'està baixant", "CalendarLegendEpisodeMissingTooltip": "L'episodi s'ha emès i falta al disc", "CalendarLegendEpisodeOnAirTooltip": "L'episodi s'està emetent en aquest moment", @@ -287,7 +286,6 @@ "Clear": "Esborra", "Component": "Component", "CopyToClipboard": "Copia al porta-papers", - "RemoveFromDownloadClientHelpTextWarning": "L'eliminació esborrarà la baixada i els fitxers del client de baixada.", "ChmodFolder": "Carpeta chmod", "ClientPriority": "Prioritat del client", "CollectionsLoadError": "No es poden carregar les col·leccions", @@ -586,5 +584,86 @@ "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desactiva l'ordenació per data", "EnableRss": "Activa RSS", "EnableColorImpairedMode": "Activa el mode amb alteracions del color", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "La cua no està habilitada" + "DownloadClientQbittorrentValidationQueueingNotEnabled": "La cua no està habilitada", + "HourShorthand": "h", + "Repeat": "Repetiu", + "Score": "Puntuació", + "Existing": "Existents", + "Extend": "Amplia", + "HttpHttps": "HTTP(S)", + "NotificationsPushoverSettingsSound": "So", + "NotificationsSimplepushSettingsKey": "Clau", + "NotificationsSlackSettingsIcon": "Icona", + "Original": "Original", + "Reorder": "Reordena", + "Script": "Script", + "StopSelecting": "Deixa de seleccionar", + "Group": "Grup", + "Images": "Imatges", + "Mechanism": "Mecanisme", + "True": "Vertader", + "Ungroup": "Desagrupa", + "Ui": "Interfície", + "Never": "Mai", + "None": "Cap", + "Enable": "Activa", + "Min": "Min", + "Permissions": "Permisos", + "Retention": "Retenció", + "Socks4": "Socks4", + "Style": "Estil", + "Upcoming": "Properament", + "Uppercase": "Majúscula", + "AutoTaggingSpecificationGenre": "Gènere(s)", + "AutoTaggingSpecificationMaximumYear": "Any màxim", + "AutoTaggingSpecificationMinimumYear": "Any mínim", + "Host": "Amfitrió", + "Import": "Importa", + "Importing": "S'està important", + "NotificationsEmailSettingsServer": "Servidor", + "NotificationsSimplepushSettingsEvent": "Esdeveniment", + "NotificationsTraktSettingsExpires": "Venciment", + "Rejections": "Rebutjats", + "Scene": "Escena", + "Space": "Espai", + "Sunday": "Diumenge", + "Trace": "Rastreig", + "Underscore": "Guió baix", + "Unknown": "Desconegut", + "Username": "Nom d'usuari", + "External": "Extern", + "Max": "Màx", + "Standard": "Estàndard", + "Monday": "Dilluns", + "Lowercase": "Minúscules", + "Logging": "Registre", + "Rss": "RSS", + "Security": "Seguretat", + "TheTvdb": "TheTVDB", + "Usenet": "Usenet", + "Theme": "Tema", + "Downloaded": "S'ha baixat", + "Folders": "Carpetes", + "NotificationsEmailSettingsName": "Correu electrònic", + "NotificationsSlackSettingsChannel": "Canal", + "Password": "Contrasenya", + "Period": "Període", + "Preferred": "Preferit", + "Presets": "Predefinits", + "Parse": "Analitza", + "Torrents": "Torrents", + "Unlimited": "Il·limitat", + "Downloading": "S'està baixant", + "Example": "Exemple", + "Here": "aquí", + "Hostname": "Nom d'amfitrió", + "Or": "o", + "NotificationsTwitterSettingsMention": "Menció", + "NotificationsNtfySettingsTopics": "Temes", + "NotificationsPushoverSettingsExpire": "Venciment", + "NotificationsPushoverSettingsDevices": "Dispositius", + "NotificationsPushoverSettingsRetry": "Torna-ho a provar", + "NotificationsSettingsWebhookMethod": "Mètode", + "Other": "Altres", + "Monitor": "Monitora" } diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 3793855d8..1a09bf4e1 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -18,7 +18,6 @@ "BackupIntervalHelpText": "Interval mezi automatickými zálohami", "BackupRetentionHelpText": "Automatické zálohy starší než doba uchovávání budou automaticky vyčištěny", "BackupsLoadError": "Nelze načíst zálohy", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Znovu spustí vyhledávání této epizody a zabrání tomu, aby bylo toto vydání získáno znovu", "BranchUpdate": "Větev, která se použije k aktualizaci {appName}u", "BranchUpdateMechanism": "Větev používaná externím aktualizačním mechanismem", "BrowserReloadRequired": "Vyžaduje se opětovné načtení prohlížeče", diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index b0383481a..8d1de1a3f 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -428,7 +428,6 @@ "RemoveFailedDownloadsHelpText": "Entfernen Sie fehlgeschlagene Downloads aus dem Download-Client-Verlauf", "RemoveFromDownloadClient": "Vom Download-Client entfernen", "RemoveFromBlocklist": "Aus der Sperrliste entfernen", - "RemoveFromDownloadClientHelpTextWarning": "Durch das Entfernen werden der Download und die Datei(en) vom Download-Client entfernt.", "Age": "Alter", "All": "Alle", "RemovingTag": "Tag entfernen", @@ -679,7 +678,6 @@ "Tomorrow": "Morgen", "AnimeEpisodeTypeDescription": "Veröffentlichte Episoden mit absoluter Episodennummer", "AnimeEpisodeTypeFormat": "Absolute Episodennummer ({format})", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Startet erneut eine Suche nach dieser Episode und verhindert, dass diese Veröffentlichung erneut abgerufen wird", "CalendarLegendEpisodeDownloadedTooltip": "Die Episode wurde heruntergeladen und sortiert", "CalendarLegendEpisodeDownloadingTooltip": "Die Folge wird gerade heruntergeladen", "CalendarLegendEpisodeMissingTooltip": "Die Folge wurde ausgestrahlt und fehlt auf der Festplatte", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index d0ef8f0ff..36a65a99c 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -341,7 +341,7 @@ "Connect": "Conectar", "ConnectionLost": "Conexión perdida", "CopyToClipboard": "Copiar al portapapeles", - "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, los archivos bloqueados impiden renombrar los archivos que siguen seedeando. Puedes desactivar el seedeo temporalmete y usar la función de renombrado de Radarr como alternativa.", + "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, los archivos bloqueados impiden renombrar los archivos que están siendo sembrados. Puedes desactivar temporalmente la siembra y usar la función de renombrado de {appName} como alternativa.", "CurrentlyInstalled": "Actualmente instalado", "CustomFilters": "Filtros Personalizados", "CustomFormat": "Formatos Personalizados", @@ -393,7 +393,6 @@ "WhatsNew": "Que es lo nuevo?", "BlocklistReleases": "Lista de lanzamientos bloqueados", "BypassDelayIfHighestQuality": "Pasar sí es la calidad más alta", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Empezar a buscar este episodio de vuelta y evitar este lanzamiento de ser agregado nuevamente", "ChownGroupHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga use el mismo grupo que {appName}.", "ClearBlocklistMessageText": "¿Estás seguro de que quieres borrar todos los elementos de la lista de bloqueo?", "FormatAgeDay": "día", @@ -491,7 +490,7 @@ "DeleteDownloadClient": "Borrar cliente de descarga", "DeleteSelectedSeries": "Eliminar serie seleccionada", "DestinationPath": "Ruta de destino", - "DeleteImportListExclusion": "Eliminar exclusión de listas de importación.", + "DeleteImportListExclusion": "Eliminar exclusión de listas de importación", "DeleteSeriesFolderConfirmation": "El directorio de series '{path}' y todos sus contenidos seran eliminados.", "DeleteDelayProfileMessageText": "¿Está seguro de que desea borrar este perfil de retraso?", "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", @@ -524,7 +523,7 @@ "DetailedProgressBar": "Barra de Progreso Detallada", "DetailedProgressBarHelpText": "Mostrar tecto en la barra de progreso", "DeletedReasonEpisodeMissingFromDisk": "{appName} no pudo encontrar el archivo en disco entonces el archivo fue desvinculado del episodio en la base de datos", - "DeleteEmptySeriesFoldersHelpText": "Eliminar directorios vacíos de series y temporadas durante el escaneo del disco y cuando se eliminen archivos correspondientes a episodios.", + "DeleteEmptySeriesFoldersHelpText": "Eliminar carpetas vacías de series y temporadas durante el escaneo del disco y cuando se eliminen archivos correspondientes a episodios", "DeleteEpisodeFile": "Eliminar archivo de episodio", "DeleteEpisodeFileMessage": "¿Está seguro de que desea borrar '{path}'?", "DeleteEpisodeFromDisk": "Eliminar episodio del disco", @@ -577,11 +576,11 @@ "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}', predeterminado con '{defaultApiUrl}'", + "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 a '{port}'", - "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto a '{url}' (solo funcionará en la misma red)", + "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)", "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.", @@ -598,5 +597,146 @@ "DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada", "DownloadClientNzbgetValidationKeepHistoryZero": "La opción KeepHistory de NzbGet debería ser mayor que 0", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.", - "DownloadClientDownloadStationValidationSharedFolderMissing": "No existe la carpeta compartida" + "DownloadClientDownloadStationValidationSharedFolderMissing": "No existe la carpeta compartida", + "DownloadPropersAndRepacksHelpText": "Decidir si automáticamente actualizar a Propers/Repacks", + "EditListExclusion": "Editar exclusión de lista", + "EnableAutomaticAdd": "Habilitar añadido automático", + "EditQualityProfile": "Editar perfil de calidad", + "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'", + "DownloadIgnoredEpisodeTooltip": "Descarga de episodio ignorada", + "EditDelayProfile": "Editar perfil de retraso", + "DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}", + "EnableAutomaticAddSeriesHelpText": "Añade series de esta lista a {appName} cuando las sincronizaciones se llevan a cabo vía interfaz de usuario o por {appName}", + "EditReleaseProfile": "Editar perfil de lanzamiento", + "DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría", + "DownloadClientRTorrentSettingsUrlPath": "Ruta de la 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.", + "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}", + "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", + "DownloadClientsSettingsSummary": "Clientes de descarga, manejo de descarga y mapeo de rutas remotas", + "EditSelectedSeries": "Editar series seleccionadas", + "EnableAutomaticSearchHelpTextWarning": "Será usado cuando se use la búsqueda interactiva", + "EnableColorImpairedMode": "Habilitar Modo de dificultad con los colores", + "EnableMetadataHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas hasta qBittorrent versión 3.3.0. Por favor, actualice o inténtelo de nuevo con una categoría vacía.", + "DownloadClientValidationCategoryMissing": "La categoría no existe", + "DownloadClientValidationGroupMissingDetail": "El grupo que introdujo no existe en {clientName}. Créelo primero en {clientName}", + "DownloadClientValidationTestNzbs": "Fallo al obtener la lista de NZBs: {exceptionMessage}", + "DownloadClientValidationUnableToConnectDetail": "Por favor, verifique el nombre de host y el puerto.", + "DownloadClientValidationUnableToConnect": "No es posible conectarse a {clientName}", + "DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para actualizaciones automáticas a propers/repacks", + "DownloadStationStatusExtracting": "Extrayendo: {progress}%", + "EditConnectionImplementation": "Editar conexión - {implementationName}", + "EnableInteractiveSearch": "Habilitar Búsqueda Interactiva", + "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.", + "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", + "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciales de una descarga. Para ser reconocida, una descarga debe tener todas las etiquetas iniciales. Esto evita conflictos con descargas no relacionadas.", + "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", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error", + "DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado", + "DownloadClientQbittorrentTorrentStateStalled": "La descarga está parada sin conexiones", + "DownloadClientQbittorrentValidationCategoryRecommended": "Se recomienda una categoría", + "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 Manejo de descargas completadas como 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", + "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", + "DownloadClientSettingsInitialState": "Estado inicial", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial para los 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}", + "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}'", + "DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error", + "DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta", + "DownloadClientValidationApiKeyRequired": "Clave API requerida", + "DownloadClientValidationAuthenticationFailure": "Fallo de autenticación", + "DownloadClientValidationCategoryMissingDetail": "La categoría que ha introducido no existe en {clientName}. Créela primero en {clientName}", + "DownloadClientValidationUnknownException": "Excepción desconocida: {exception}", + "DownloadClientVuzeValidationErrorVersion": "Versión de protocolo no soportada, use Vuze 5.0.0.0 o superior con la extensión Vuze Web remote.", + "Downloaded": "Descargado", + "Downloading": "Descargando", + "Duration": "Duración", + "EditCustomFormat": "Editar formato personalizado", + "EditGroups": "Editar grupos", + "EditImportListExclusion": "Editar exclusión de lista de importación", + "EditMetadata": "Editar metadatos {metadataType}", + "EditSeries": "Editar series", + "EditSeriesModalHeader": "Editar - {title}", + "DownloadClientQbittorrentTorrentStateUnknown": "Estado de descarga desconocido: {state}", + "DownloadClientSettingsOlderPriority": "Priorizar más antiguos", + "DownloadClientSettingsRecentPriority": "Priorizar más recientes", + "EditRemotePathMapping": "Editar mapeo de ruta remota", + "EditRestriction": "Editar restricción", + "EnableAutomaticSearch": "Habilitar Búsqueda Automática", + "DownloadClientSettings": "Opciones del cliente de descarga", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Usar 'No preferir' para ordenar por puntuación de formato personalizada en lugar de propers/repacks", + "Edit": "Editar", + "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.", + "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.", + "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", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Deshabilita la opción 'Verificar antes de descargar' en Sabnbzd", + "DownloadClientValidationTestTorrents": "Fallo al obtener la lista de torrents: {exceptionMessage}", + "DownloadClientValidationVerifySsl": "Verificar las opciones SSL", + "DownloadClientValidationVerifySslDetail": "Por favor, verifique su configuración SSL en {clientName} y {appName}", + "DownloadClients": "Clientes de descarga", + "DownloadFailed": "La descarga falló", + "DownloadPropersAndRepacks": "Propers y repacks", + "DownloadClientValidationErrorVersion": "La versión de {clientName} debería ser al menos {requiredVersion}. La versión devuelta es {reportedVersion}", + "EnableAutomaticSearchHelpText": "Será usado cuando las búsquedas automáticas sean realizadas por la interfaz de usuario o por {appName}", + "EnableColorImpairedModeHelpText": "Estilo modificado para permitir que usuarios con problemas de color distingan mejor la información codificada por colores", + "NotificationsDiscordSettingsUsernameHelpText": "El nombre de usuario para publicar, por defecto es el webhook predeterminado de Discord", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Debe deshabilitar la ordenación de TV para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.", + "DownloadClientSettingsCategoryHelpText": "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.", + "DownloadClientRTorrentProviderMessage": "rTorrent no pausará los torrents cuando satisfagan los criterios de siembra. {appName} manejará la eliminación automática de torrents basada en el actual criterio de siembra en Opciones ->Indexadores solo cuando Eliminar completados esté habilitado. Después de importarla también establecerá {importedView} como una vista de rTorrent, la cuál puede ser usada en los scripts de rTorrent para personalizar el comportamiento.", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefiere que cada descarga tenga una carpeta separada. Con * añadida a la carpeta/ruta, Sabnzbd no creará esas carpetas de trabajo. Vaya a Sabnzbd para arreglarlo.", + "DownloadClientValidationAuthenticationFailureDetail": "Por favor, verifique su nombre de usuario y contraseña. También verifique si no está bloqueado el acceso del host en ejecución {appName} a {clientName} por limitaciones en la lista blanca en la configuración de {clientName}.", + "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", + "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", + "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index b974bba3f..01d426e89 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1,5 +1,4 @@ { - "BlocklistReleaseSearchEpisodeAgainHelpText": "Etsii jaksoa uudelleen ja estää tämän julkaisun automaattisen uudelleenkaappauksen.", "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}\", muttei voi käyttää sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "Added": "Lisäysaika", @@ -512,7 +511,6 @@ "AddCustomFilter": "Lisää oma suodatin", "AddConnectionImplementation": "Lisätään kytköstä - {implementationName}", "RemoveCompletedDownloadsHelpText": "Poista tuodut lataukset lataustyökalun historiasta", - "RemoveFromDownloadClientHelpTextWarning": "Poistaminen poistaa latauksen ja sen sisältämät tiedostot lataustyökalusta.", "RemoveFilter": "Poista suodatin", "RemoveRootFolder": "Poista juurikansio", "RenameEpisodes": "Nimeä jaksot uudelleen", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 19e611ddb..1ecb4ddcf 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -164,7 +164,6 @@ "BlocklistReleases": "Publications de la liste de blocage", "BindAddress": "Adresse de liaison", "BackupsLoadError": "Impossible de charger les sauvegardes", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Lance une nouvelle recherche pour cet épisode et empêche que cette version soit à nouveau récupérée", "BuiltIn": "Intégré", "BrowserReloadRequired": "Rechargement du navigateur requis", "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score est supérieur au format personnalisé", @@ -869,7 +868,7 @@ "OptionalName": "Nom facultatif", "Paused": "En pause", "Pending": "En attente", - "Permissions": "Permissions", + "Permissions": "Autorisations", "PreviouslyInstalled": "Installé précédemment", "Priority": "Priorité", "PublishedDate": "Date de publication", @@ -1149,7 +1148,6 @@ "ReleaseSceneIndicatorSourceMessage": "Les versions {message} existent avec une numérotation ambiguë, incapable d'identifier l'épisode de manière fiable.", "ReleaseSceneIndicatorUnknownMessage": "La numérotation varie pour cet épisode et la version ne correspond à aucun mappage connu.", "ReleaseSceneIndicatorUnknownSeries": "Épisode ou série inconnu.", - "RemoveFromDownloadClientHelpTextWarning": "La suppression supprimera le téléchargement et le(s) fichier(s) du client de téléchargement.", "RemoveTagsAutomaticallyHelpText": "Supprimez automatiquement les étiquettes si les conditions ne sont pas remplies", "RemovedFromTaskQueue": "Supprimé de la file d'attente des tâches", "RemovedSeriesSingleRemovedHealthCheckMessage": "La série {series} a été supprimée de TheTVDB", @@ -1868,5 +1866,36 @@ "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Utiliser la disposition du contenu configurée par qBittorrent, la disposition originale du torrent ou toujours créer un sous-dossier (qBittorrent 4.3.2+)", "DownloadClientQbittorrentSettingsContentLayout": "Disposition du contenu", "NotificationsGotifySettingIncludeSeriesPoster": "Inclure l'affiche de la série", - "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Inclure l'affiche de la série dans le message" + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Inclure l'affiche de la série dans le message", + "EpisodeFileMissingTooltip": "Fichier de l'épisode manquant", + "AutoTaggingSpecificationGenre": "Genre(s)", + "AutoTaggingSpecificationMaximumYear": "Année maximum", + "AutoTaggingSpecificationMinimumYear": "Année maximum", + "AutoTaggingSpecificationOriginalLanguage": "Langue", + "AutoTaggingSpecificationQualityProfile": "Profil de Qualité", + "AutoTaggingSpecificationRootFolder": "Dossier Racine", + "AutoTaggingSpecificationSeriesType": "Type de série", + "AutoTaggingSpecificationStatus": "État", + "CustomFormatsSpecificationLanguage": "Langue", + "CustomFormatsSpecificationMaximumSize": "Taille maximum", + "CustomFormatsSpecificationMinimumSize": "Taille maximum", + "CustomFormatsSpecificationRegularExpression": "Langue", + "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é", + "ImportListsAniListSettingsImportFinished": "Importation terminée", + "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur de la liste à importer", + "ImportListsCustomListSettingsName": "Liste personnalisé", + "ImportListsCustomListValidationAuthenticationFailure": "Échec de l'authentification", + "ImportListsPlexSettingsAuthenticateWithPlex": "Se connecter avec Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", + "ImportListsSettingsAccessToken": "Jeton d'accès", + "ImportListsSettingsRefreshToken": "Jeton d'actualisation", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Se connecter 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é." } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index fa8d1eb9c..92bfab9e6 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1,5 +1,4 @@ { - "BlocklistReleaseSearchEpisodeAgainHelpText": "Megakadályozza, hogy a {appName} automatikusan újra letöltse ezt a kiadást", "CloneCondition": "Feltétel klónozása", "CloneCustomFormat": "Egyéni formátum klónozása", "Close": "Bezárás", @@ -12,7 +11,6 @@ "IndexerJackettAllHealthCheckMessage": "A nem támogatott Jackett 'all' végpontot használó indexelők: {indexerNames}", "Remove": "Eltávolítás", "RemoveFromDownloadClient": "Eltávolítás a letöltési kliensből", - "RemoveFromDownloadClientHelpTextWarning": "A törlés eltávolítja a letöltést és a fájl(okat) a letöltési kliensből.", "RemoveSelectedItem": "Kijelölt elem eltávolítása", "RemoveSelectedItemQueueMessageText": "Biztosan el akar távolítani 1 elemet a várólistáról?", "RemoveSelectedItems": "Kijelölt elemek eltávolítása", @@ -146,7 +144,7 @@ "AgeWhenGrabbed": "Életkor (amikor megragadták)", "BlocklistReleases": "Feketelista kiadása", "ChooseAnotherFolder": "Válasszon másik mappát", - "Metadata": "metaadat", + "Metadata": "Metaadat", "Queue": "Várakozási sor", "Profiles": "Profilok", "Implementation": "Végrehajtás", @@ -224,7 +222,7 @@ "ApplyTagsHelpTextHowToApplyIndexers": "Címkék alkalmazása a kiválasztott indexelőkre", "AutoTagging": "Automatikus címkézés", "CancelPendingTask": "Biztosan törölni szeretné ezt a függőben lévő feladatot?", - "MultiSeason": "Évad", + "MultiSeason": "Több évad", "AllResultsAreHiddenByTheAppliedFilter": "Az összes eredményt elrejti az alkalmazott szűrő", "IRC": "IRC", "ApplyTagsHelpTextAdd": "Hozzáadás: Adja hozzá a címkéket a meglévő címkék listájához", @@ -537,5 +535,276 @@ "Here": "itt", "Uptime": "Üzemidő", "AnalyticsEnabledHelpText": "Névtelen használati és hibainformáció küldése {appName} szervereinek. Ez magában foglalja a böngészővel kapcsolatos információkat, a használt {appName} WebUI oldalakat, a hibajelentéseket, valamint az operációs rendszert és a futásidejű verziót. Ezeket az információkat a funkciók és a hibajavítások fontossági sorrendjének meghatározására fogjuk használni.", - "ApplicationUrlHelpText": "Ennek az alkalmazásnak a külső URL-címe, beleértve a http-eket" + "ApplicationUrlHelpText": "Ennek az alkalmazásnak a külső URL-címe, beleértve a http-eket", + "DownloadFailed": "Sikertelen letöltés", + "EditDownloadClientImplementation": "Letöltési kliens szerkesztése – {implementationName}", + "FailedToLoadCustomFiltersFromApi": "Nem sikerült betölteni az egyéni szűrőket az API-ból", + "FailedToLoadSeriesFromApi": "Nem sikerült betölteni a sorozatokat az API-ból", + "False": "Hamis", + "FailedToLoadUiSettingsFromApi": "Nem sikerült betölteni a felhasználói felület beállításait az API-ból", + "Filter": "Szűrő", + "FilterInLast": "az utolsóban", + "FilterLessThan": "kevesebb, mint", + "KeyboardShortcutsCloseModal": "Az aktuális mód bezárása", + "KeyboardShortcuts": "Gyorsbillentyűket", + "LibraryImportTipsDontUseDownloadsFolder": "Ne használja letöltések importálására a letöltési kliensből, ez csak a meglévő szervezett könyvtárakra vonatkozik, a rendezetlen fájlokra nem.", + "More": "Több", + "Month": "Hónap", + "MyComputer": "A számítógépem", + "NoEventsFound": "Nem található események", + "Negated": "Negatív", + "FailedToLoadQualityProfilesFromApi": "Nem sikerült betölteni a minőségi profilokat az API-ból", + "FailedToLoadSonarr": "Nem sikerült betölteni a(z) {appName} alkalmazást", + "FileBrowser": "Fájl Böngésző", + "FilterContains": "tartalmaz", + "FilterInNext": "a következőben", + "FilterEqual": "egyenlő", + "InvalidUILanguage": "A felhasználói felület érvénytelen nyelvre van állítva, javítsa ki, és mentse el a beállításait", + "KeyboardShortcutsConfirmModal": "A megerősítési mód elfogadása", + "LibraryImportTips": "Néhány tipp az import zökkenőmentes lebonyolításához:", + "LibraryImportSeriesHeader": "A már meglévő sorozatok importálása", + "Local": "Helyi", + "ListsLoadError": "Nem sikerült betölteni a listákat", + "ManageDownloadClients": "Letöltési kliensek kezelése", + "ManualImportItemsLoadError": "Nem sikerült betölteni a kézi importálási elemeket", + "MaximumSizeHelpText": "A megragadható kiadás maximális mérete MB-ban. Állítsa nullára, hogy korlátlanra állítsa", + "MediaInfo": "Média információ", + "MidseasonFinale": "Középszezon finálé", + "MoveSeriesFoldersDontMoveFiles": "Nem, én magam mozgatom át a fájlokat", + "MoveSeriesFoldersMoveFiles": "Igen, mozgassa át a fájlokat", + "MoveSeriesFoldersToNewPath": "Szeretné áthelyezni a sorozatfájlokat a(z) „{originalPath}” helyről a „{destinationPath}” címre?", + "NoHistoryBlocklist": "Nincs előzmény a tiltólistán", + "NoHistoryFound": "Nem található előzmény", + "NotificationsDiscordSettingsAvatarHelpText": "Módosítsa az integrációból származó üzenetekhez használt avatart", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Módosítsa a „kézi interakcióról” értesítéshez átadott mezőket", + "NotificationsDiscordSettingsOnManualInteractionFields": "A kézi interakciós mezőkön", + "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord csatorna webhook url", + "NotificationsEmailSettingsFromAddress": "Címből", + "NotificationsEmailSettingsName": "Email", + "DownloadIgnored": "Letöltés Figyelmen kívül hagyva", + "DownloadWarning": "Letöltési figyelmeztetés: {warningMessage}", + "EditQualityProfile": "Minőségi profil szerkesztése", + "FileManagement": "Fájlkezelés", + "MetadataLoadError": "Nem sikerült betölteni a metaadatokat", + "MonitorExistingEpisodes": "Meglévő epizódok", + "MonitorFutureEpisodesDescription": "Figyelje meg a még nem sugárzott epizódokat", + "MultiEpisodeInvalidFormat": "Több epizód: Érvénytelen formátum", + "NoMatchFound": "Nem található egyezés!", + "None": "Egyik sem", + "DownloadClientsLoadError": "Nem sikerült betölteni a letöltő klienseket", + "EditImportListExclusion": "Importálási lista kizárásának szerkesztése", + "ExtraFileExtensionsHelpTextsExamples": "Példák: \".sub, .nfo\" vagy \"sub,nfo\"", + "FileNames": "Fájlnevek", + "Min": "Min", + "ListWillRefreshEveryInterval": "A lista minden {refreshInterval} frissítésre kerül", + "LocalPath": "Helyi útvonal", + "MatchedToEpisodes": "Epizódokkal párosítva", + "Mechanism": "Gépezet", + "MetadataProvidedBy": "A metaadatokat a(z) {provider} biztosítja", + "MetadataSourceSettings": "Metaadatforrás beállításai", + "MinimumCustomFormatScoreHelpText": "A letölthető minimális egyéni formátum pontszám", + "MinimumFreeSpace": "Minimális szabad hely", + "MinimumFreeSpaceHelpText": "Az importálás megakadályozása, ha ennél kevesebb lemezterület maradna elérhető", + "NoDelay": "Nincs késleltetés", + "NoIssuesWithYourConfiguration": "Nincs probléma a konfigurációval", + "NoLeaveIt": "Nem, Hagyd", + "NoLinks": "Nincsenek linkek", + "NoTagsHaveBeenAddedYet": "Még nem adtak hozzá címkéket", + "ManageClients": "Ügyfelek kezelése", + "ListExclusionsLoadError": "Nem sikerült betölteni a listakizárásokat", + "MonitorFirstSeason": "Első évad", + "NoEpisodesFoundForSelectedSeason": "Nem található epizód a kiválasztott évadhoz", + "MonitorSelected": "Monitor kiválasztva", + "EditMetadata": "Szerkessze a {metadataType} metaadatokat", + "FailedToFetchUpdates": "Nem sikerült lekérni a frissítéseket", + "FileBrowserPlaceholderText": "Kezdjen el gépelni, vagy válasszon egy elérési utat lent", + "Files": "Fájlok", + "Medium": "Közepes", + "NoEpisodeHistory": "Nincs epizódelőzmény", + "FeatureRequests": "Funkciókérés", + "FilterGreaterThan": "nagyobb, mint", + "KeyboardShortcutsFocusSearchBox": "Fókusz keresőmező", + "LastWriteTime": "Utolsó írási idő", + "LogFiles": "Naplófájlok", + "ManualImport": "Kézi importálás", + "MappedNetworkDrivesWindowsService": "A leképezett hálózati meghajtók nem érhetők el, ha Windows szolgáltatásként futnak, lásd a [GYIK](https:", + "MediaManagementSettingsLoadError": "Nem sikerült betölteni a médiakezelési beállításokat", + "MediaManagementSettings": "Médiakezelési beállítások", + "NoMonitoredEpisodesSeason": "Ebben az évadban nincsenek felügyelt epizódok", + "LogFilesLocation": "A naplófájlok itt találhatók: {location}", + "LanguagesLoadError": "Nem lehet betölteni a nyelveket", + "Lowercase": "Kisbetűs", + "NoResultsFound": "Nincs találat", + "NoLogFiles": "Nincsenek naplófájlok", + "NoEpisodesInThisSeason": "Ebben az évadban nincs epizód", + "EditAutoTag": "Automatikus címke szerkesztése", + "EditRestriction": "Korlátozás szerkesztése", + "FilterGreaterThanOrEqual": "nagyobb vagy egyenlő", + "MediaManagement": "Médiakezelés", + "MegabytesPerMinute": "Megabájt percenként", + "MissingNoItems": "Nincsenek hiányzó elemek", + "NoMinimumForAnyRuntime": "Nincs minimális futásidő", + "NoSeriesFoundImportOrAdd": "Nem található sorozat, a kezdéshez importálnia kell meglévő sorozatát, vagy új sorozatot kell hozzáadnia.", + "NoSeriesHaveBeenAdded": "Még nem adott hozzá sorozatot. Először importálni szeretné a sorozatok egy részét vagy az egészet?", + "NotificationStatusSingleClientHealthCheckMessage": "Az értesítések a következő hibák miatt nem érhetők el: {notificationNames}", + "EditGroups": "Csoportok szerkesztése", + "EditIndexerImplementation": "Indexelő szerkesztése – {implementationName}", + "Example": "Példa", + "FileNameTokens": "Fájlnév-tokenek", + "FilterDoesNotContain": "nem tartalmaz", + "FilterEpisodesPlaceholder": "Az epizódok szűrése cím vagy szám szerint", + "FilterIsAfter": "után van", + "FilterIsBefore": "előtt van", + "FilterIsNot": "nem", + "FilterLessThanOrEqual": "kisebb vagy egyenlő", + "DownloadPropersAndRepacksHelpText": "Függetlenül attól, hogy automatikusan frissíteni kell-e a Propers-re", + "ExistingSeries": "Létező Sorozat", + "Existing": "Létező", + "Extend": "Kiterjeszt", + "External": "Külső", + "ManageEpisodesSeason": "Epizódfájlok kezelése ebben az évadban", + "NamingSettingsLoadError": "Nem sikerült betölteni az elnevezési beállításokat", + "InvalidFormat": "Érvénytelen formátum", + "KeyboardShortcutsOpenModal": "Nyissa meg ezt a módot", + "KeyboardShortcutsSaveSettings": "Beállítások mentése", + "LastDuration": "Utolsó időtartam", + "LatestSeason": "Legújabb évad", + "LiberaWebchat": "Ingyenes webchat", + "LibraryImport": "Könyvtár importálása", + "ListOptionsLoadError": "A lista opciói nem tölthetők be", + "ListQualityProfileHelpText": "A minőségi profil listaelemei hozzáadásra kerülnek", + "ListRootFolderHelpText": "A gyökérmappa listaelemei hozzáadódnak", + "ListTagsHelpText": "Címkék, amelyek a listáról történő importáláskor kerülnek hozzáadásra", + "LocalAirDate": "Helyi adás dátuma", + "LocalStorageIsNotSupported": "A helyi tárolás nem támogatott vagy letiltott. Előfordulhat, hogy egy beépülő modul vagy a privát böngészés letiltotta.", + "LogLevel": "Napló szint", + "LogLevelTraceHelpTextWarning": "A nyomkövetési naplózást csak ideiglenesen szabad engedélyezni", + "Logout": "Kijelentkezés", + "MaintenanceRelease": "Karbantartási kiadás: hibajavítások és egyéb fejlesztések. További részletekért lásd: Github Commit History", + "ManageEpisodes": "Epizódok kezelése", + "Mapping": "Térképezés", + "MarkAsFailed": "Megjelölés sikertelenként", + "MarkAsFailedConfirmation": "Biztosan sikertelenként szeretné megjelölni a(z) \"{sourceTitle}\" elemet?", + "MassSearchCancelWarning": "Ezt a(z) {appName} újraindítása vagy az összes indexelő letiltása nélkül nem lehet visszavonni.", + "MatchedToSeason": "Szezonhoz illesztve", + "MatchedToSeries": "Sorozathoz illesztve", + "Max": "Max", + "MaximumLimits": "Maximális korlátok", + "MaximumSingleEpisodeAge": "Egy epizód maximális kora", + "MaximumSingleEpisodeAgeHelpText": "A teljes évados keresés során csak az évadcsomagok engedélyezettek, ha az évad utolsó epizódja régebbi ennél a beállításnál. Csak szabványos sorozat. Használja a 0-t a letiltáshoz.", + "MaximumSize": "Maximális méret", + "MetadataPlexSettingsSeriesPlexMatchFile": "Sorozat Plex Match File", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Létrehoz egy .plexmatch fájlt a sorozat mappájában", + "MetadataSettings": "Metaadat-beállítások", + "MetadataSettingsEpisodeImages": "Epizód képek", + "MetadataSettingsEpisodeMetadata": "Epizód Metaadat", + "MetadataSettingsEpisodeMetadataImageThumbs": "Az epizód metaadatainak képei", + "MetadataSettingsSeasonImages": "Szezon képek", + "MetadataSettingsSeriesImages": "Sorozatképek", + "MetadataSettingsSeriesMetadata": "Sorozat metaadatai", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Útmutató a sorozat metaadataihoz", + "MetadataSettingsSeriesMetadataUrl": "A sorozat metaadatainak URL-je", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo a sorozat teljes metaadataival", + "MonitorMissingEpisodes": "Hiányzó epizódok", + "MonitorNoEpisodesDescription": "Egyetlen epizódot sem figyelnek meg", + "MonitorRecentEpisodesDescription": "Az elmúlt 90 napban sugárzott epizódok és a jövőbeli epizódok figyelése", + "MonitorSpecialEpisodesDescription": "Kövesse nyomon az összes speciális epizódot anélkül, hogy megváltoztatná a többi epizód monitorozott állapotát", + "MonitoredEpisodesHelpText": "Töltse le a sorozat felügyelt epizódjait", + "MoveFiles": "Fájlok áthelyezése", + "MoreDetails": "További részletek", + "MoveSeriesFoldersToRootFolder": "Szeretné áthelyezni a sorozat mappáit a következő helyre: \"{destinationRootFolder}\"?", + "MultiEpisode": "Több epizód", + "MustNotContain": "Nem tartalmazhat", + "NamingSettings": "Elnevezési beállítások", + "Negate": "Negatív", + "NegateHelpText": "Ha be van jelölve, az egyéni formátum nem érvényesül, ha ez a {implementationName} feltétel megfelel.", + "NoChanges": "Nincs változás", + "NoEpisodeInformation": "Nem áll rendelkezésre információ az epizódról.", + "NoEpisodeOverview": "Nincs epizód áttekintése", + "NoHistory": "Nincs előzmény", + "NoUpdatesAreAvailable": "Nem érhetők el frissítések", + "NotSeasonPack": "Nem Season Pack", + "NotificationTriggers": "Értesítési triggerek", + "NotificationTriggersHelpText": "Válassza ki, hogy mely események váltsák ki ezt az értesítést", + "NotificationsAppriseSettingsNotificationType": "Értesítés típusa", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Módosítsa az „importáláskor” értesítéshez átadott mezőket", + "NotificationsAppriseSettingsPasswordHelpText": "HTTP alapszintű hitelesítési jelszó", + "NotificationsDiscordSettingsUsernameHelpText": "A közzétételhez használt felhasználónév alapértelmezett Discord webhook", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Egy vagy több URL, vesszővel elválasztva, jelezve, hogy hova kell küldeni az értesítést. Hagyja üresen, ha állandó tárolást használ.", + "NotificationsEmailSettingsBccAddressHelpText": "A titkos másolat címzettjeinek vesszővel elválasztott listája", + "NotificationsEmailSettingsCcAddressHelpText": "Az e-mail másolatának címzettjeinek vesszővel elválasztott listája", + "NotificationsEmailSettingsRecipientAddress": "Címzett címe(i)", + "NotificationsEmailSettingsRecipientAddressHelpText": "Az e-mail címzettjeinek vesszővel elválasztott listája", + "NotificationsCustomScriptSettingsArguments": "Érvek", + "NotificationsAppriseSettingsTagsHelpText": "Opcionálisan csak a megfelelően megcímkézetteket értesítse.", + "NotificationsCustomScriptSettingsProviderMessage": "A tesztelés végrehajtja a szkriptet az EventType értéke {eventTypeTest} értékkel, győződjön meg róla, hogy a szkript megfelelően kezeli", + "NotificationsCustomScriptValidationFileDoesNotExist": "A fájl nem létezik", + "NotificationsCustomScriptSettingsName": "Egyéni szkript", + "NotificationsDiscordSettingsAuthorHelpText": "Az értesítésnél megjelenő beágyazás szerzőjének felülbírálása. Üres a példány neve", + "NotificationsDiscordSettingsAvatar": "Avatar", + "LastUsed": "Utoljára használt", + "LongDateFormat": "Hosszú dátum formátum", + "ManualGrab": "Megfog", + "MinimumCustomFormatScore": "Minimális egyéni formátum pontszám", + "MinimumLimits": "Minimális korlátok", + "Monitor": "Monitor", + "MinimumAge": "Minimális kor", + "MustContain": "Tartalmaznia kell", + "MultiEpisodeStyle": "Több epizód stílus", + "NoBackupsAreAvailable": "Nincsenek biztonsági mentések", + "DownloadFailedEpisodeTooltip": "Az epizód letöltése nem sikerült", + "MonitorNoEpisodes": "Egyik sem", + "MustContainHelpText": "A kiadásnak tartalmaznia kell legalább egy ilyen kifejezést (a kis- és nagybetűket nem különbözteti meg)", + "MustNotContainHelpText": "A kiadás elutasításra kerül, ha egy vagy több kifejezést tartalmaz (nem különbözteti meg a kis- és nagybetűket)", + "MonitorAllEpisodes": "Minden epizód", + "MonitorAllEpisodesDescription": "Kövesse nyomon az összes epizódot, kivéve a különlegességeket", + "MonitorFutureEpisodes": "Jövőbeni epizódok", + "MonitoredOnly": "Csak megfigyelt", + "MonitoredStatus": "Felügyelt/állapot", + "MoreInfo": "Több információ", + "MoveAutomatically": "Automatikus áthelyezés", + "MissingEpisodes": "Hiányzó epizódok", + "MissingLoadError": "Hiba a hiányzó elemek betöltésekor", + "NotificationsEmailSettingsServer": "Szerver", + "DownloadClients": "Letöltő kliensek", + "DownloadClientsSettingsSummary": "Kliensek letöltése, letöltéskezelés és távoli útvonalleképezések", + "DownloadIgnoredEpisodeTooltip": "Az epizódletöltés figyelmen kívül hagyva", + "EditDelayProfile": "Késleltetési profil szerkesztése", + "EditConnectionImplementation": "Kapcsolat szerkesztése - {implementationName}", + "EditCustomFormat": "Egyéni formátum szerkesztése", + "EditImportListImplementation": "Importálási lista szerkesztése – {implementationName}", + "EditReleaseProfile": "Kiadási profil szerkesztése", + "EditRemotePathMapping": "Távoli útvonal-leképezés szerkesztése", + "ExistingTag": "Létező címke", + "ExpandAll": "Bontsa ki az összeset", + "FailedToLoadSystemStatusFromApi": "Nem sikerült betölteni a rendszerállapotot az API-ból", + "FailedToLoadTagsFromApi": "Nem sikerült betölteni a címkéket az API-ból", + "FailedToLoadTranslationsFromApi": "Nem sikerült betölteni a fordításokat az API-ból", + "FailedToUpdateSettings": "Nem sikerült frissíteni a beállításokat", + "FilterDoesNotEndWith": "nem ér véget", + "Level": "Szint", + "LibraryImportTipsQualityInEpisodeFilename": "Győződjön meg arról, hogy a fájlok fájlnevében szerepel a minőség. például. `episode.s02e15.bluray.mkv`", + "LibraryImportTipsSeriesUseRootFolder": "Mutasson a(z) {appName} mappára, amely az összes tévéműsorát tartalmazza, ne egy konkrétat. például. \"`{goodFolderExample}`\" és nem \"`{badFolderExample}`\". Ezenkívül minden sorozatnak saját mappájában kell lennie a gyökérben", + "Links": "Linkek", + "MetadataSourceSettingsSeriesSummary": "Információ arról, hogy a(z) {appName} honnan szerezheti be a sorozat- és epizódinformációkat", + "MonitorAllSeasons": "Minden évad", + "MonitorNewSeasonsHelpText": "Mely új évadokat kell automatikusan figyelni", + "MonitorNoNewSeasons": "Nincsenek új évadok", + "MonitorNoNewSeasonsDescription": "Ne figyeljen automatikusan új évadokat", + "MonitorPilotEpisodeDescription": "Csak az első évad első epizódját figyeld", + "MonitorRecentEpisodes": "Legutóbbi epizódok", + "MetadataSource": "Metaadatforrás", + "MonitorAllSeasonsDescription": "Az összes új évszak automatikus figyelése", + "MonitorFirstSeasonDescription": "Kövesse nyomon az első évad összes epizódját. Az összes többi évszakot figyelmen kívül hagyjuk", + "MonitorLastSeason": "Utolsó évad", + "MetadataSettingsSeriesSummary": "Hozzon létre metaadatfájlokat epizódok importálásakor vagy sorozatok frissítésekor", + "MonitorLastSeasonDescription": "Kövesse nyomon az elmúlt évad összes epizódját", + "MonitorNewSeasons": "Kövesse az új évadokat", + "NoMonitoredEpisodes": "Ebben a sorozatban nincsenek felügyelt epizódok", + "MediaManagementSettingsSummary": "Elnevezések, fájlkezelési beállítások és gyökérmappák", + "MonitorExistingEpisodesDescription": "Figyelje meg azokat az epizódokat, amelyekben vannak fájlok, vagy amelyeket még nem adtak le", + "MonitorMissingEpisodesDescription": "Figyelje meg azokat az epizódokat, amelyekhez nem tartoznak fájlok, vagy amelyeket még nem adtak le", + "MonitorNewItems": "Új elemek figyelése", + "NoLimitForAnyRuntime": "Nincs korlátozás semmilyen futási időre", + "NotificationStatusAllClientHealthCheckMessage": "Az összes értesítés nem érhető el hibák miatt" } diff --git a/src/NzbDrone.Core/Localization/Core/id.json b/src/NzbDrone.Core/Localization/Core/id.json index 00a5ac357..7a47d2b22 100644 --- a/src/NzbDrone.Core/Localization/Core/id.json +++ b/src/NzbDrone.Core/Localization/Core/id.json @@ -1,6 +1,5 @@ { "Added": "Ditambahkan", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Mencegah {appName} memperoleh rilis ini secara otomatis", "Delete": "Hapus", "Close": "Tutup", "EnableAutomaticSearch": "Aktifkan Penelusuran Otomatis", diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index a6b57cc6a..ff5e3a7f4 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -21,7 +21,6 @@ "ApplyTagsHelpTextHowToApplyIndexers": "Hoe tags toepassen op de geselecteerde indexeerders", "CountDownloadClientsSelected": "{count} download client(s) geselecteerd", "ApplyTagsHelpTextHowToApplySeries": "Hoe tags toepassen op de geselecteerde series", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Verbied {appName} om deze release opnieuw automatisch te downloaden", "Delete": "Verwijder", "ApplyTagsHelpTextRemove": "Verwijderen: Verwijder de ingevoerde tags", "ApplyTagsHelpTextReplace": "Vervangen: Vervang de tags met de ingevoerde tags (vul geen tags in om alle tags te wissen)", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 9c1b2357b..816da29fe 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -86,7 +86,6 @@ "RemotePathMappingWrongOSPathHealthCheckMessage": "O cliente de download remoto {downloadClientName} coloca os downloads em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' não pode ser gravada pelo usuário '{userName}'.", "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' está em uma pasta de translocação do App.", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Inicia uma busca por este episódio novamente e impede que esta versão seja capturada novamente", "BlocklistReleases": "Lançamentos na lista de bloqueio", "CloneCondition": "Clonar Condição", "CloneCustomFormat": "Clonar formato personalizado", @@ -100,7 +99,6 @@ "Negated": "Negado", "Remove": "Remover", "RemoveFromDownloadClient": "Remover Do Cliente de Download", - "RemoveFromDownloadClientHelpTextWarning": "A remoção removerá o download e o(s) arquivo(s) do cliente de download.", "RemoveSelectedItem": "Remover Item Selecionado", "RemoveSelectedItemQueueMessageText": "Tem certeza de que deseja remover 1 item da fila?", "RemoveSelectedItems": "Remover Itens Selecionados", @@ -2007,5 +2005,7 @@ "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Incluir etiquetas de miniatura de imagem no nome do arquivo .nfo (requer 'Metadados de Episódio')", "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Incluir elemento de guia de episódios formatado em JSON em tvshow.nfo (requer 'Metadados da Série')", "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo com metadados da série completa", - "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')" + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')", + "NotificationsEmailSettingsUseEncryption": "Usar Criptografia", + "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index f37e97d20..3dd8540ee 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -92,7 +92,6 @@ "RemoveTagsAutomaticallyHelpText": "Автоматически удалять теги, если условия не выполняются", "DeleteSelectedIndexers": "Удалить индексатор(ы)", "DeleteTagMessageText": "Вы уверены, что хотите удалить тэг '{label}'?", - "RemoveFromDownloadClientHelpTextWarning": "Удаление приведет к удалению загрузки и файла(ов) из клиента загрузки.", "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить Ваш API ключ?", "ResetDefinitionTitlesHelpText": "Сбросить названия определений, а также значения", "Socks4": "Socks4", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index c24b4be3b..420acb9d9 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -47,7 +47,6 @@ "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器", "AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除", "BlocklistRelease": "黑名单版本", - "BlocklistReleaseSearchEpisodeAgainHelpText": "再次启动对此集的搜索并阻止再次获取此版本", "BlocklistReleases": "黑名单版本", "CloneCustomFormat": "复制自定义命名格式", "Close": "关闭", @@ -309,7 +308,6 @@ "Special": "特色", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName}无法导入剧集。查看日志以了解详细信息。", "RemotePathMappingWrongOSPathHealthCheckMessage": "远程下载客户端{downloadClientName}将文件下载在{path}中,但这不是有效的{osName}路径。查看远程路径映射并更新下载客户端设置。", - "RemoveFromDownloadClientHelpTextWarning": "删除将从下载客户端删除下载和文件。", "Renamed": "已重命名", "RootFolderPath": "根目录路径", "Runtime": "时长", From 68c326ae27b9f72bae165f611cfd6f592b901be1 Mon Sep 17 00:00:00 2001 From: The Dark <12370876+CheAle14@users.noreply.github.com> Date: Sat, 27 Jan 2024 05:55:52 +0000 Subject: [PATCH 068/762] New: Import list clean library option Closes #5201 --- frontend/src/App/State/SettingsAppState.ts | 7 + .../ImportLists/ImportListSettings.js | 22 +- .../ImportLists/Options/ImportListOptions.tsx | 148 +++++++ .../Actions/Settings/importListOptions.js | 64 +++ frontend/src/Store/Actions/settingsActions.js | 5 + .../createSettingsSectionSelector.js | 32 -- .../createSettingsSectionSelector.ts | 49 +++ .../src/typings/ImportListOptionsSettings.ts | 10 + frontend/src/typings/pending.ts | 9 + .../FetchAndParseImportListFixture.cs | 219 +++++++++++ .../ImportListItemServiceFixture.cs | 57 +++ .../ImportListSyncServiceFixture.cs | 363 +++++++++++++++++- .../Configuration/ConfigService.cs | 13 + .../Configuration/IConfigService.cs | 4 + .../Migration/193_add_import_list_items.cs | 26 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 + .../ImportLists/AniList/AniListImportBase.cs | 3 +- .../ImportLists/AniList/List/AniListImport.cs | 6 +- .../ImportLists/Custom/CustomImport.cs | 6 +- .../FetchAndParseImportListService.cs | 56 ++- .../ImportLists/HttpImportListBase.cs | 8 +- src/NzbDrone.Core/ImportLists/IImportList.cs | 4 +- .../ImportLists/ImportListBase.cs | 19 +- .../ImportListItemRepository.cs | 43 +++ .../ImportListItems/ImportListItemService.cs | 59 +++ .../ImportLists/ImportListStatus.cs | 1 + .../ImportLists/ImportListStatusService.cs | 29 +- .../ImportLists/ImportListSyncService.cs | 130 ++++++- .../ImportLists/ListSyncLevelType.cs | 10 + .../ImportLists/Plex/PlexImport.cs | 3 +- .../ImportLists/Rss/RssImportBase.cs | 4 +- .../ImportLists/Simkl/SimklImportBase.cs | 7 +- .../ImportLists/Sonarr/SonarrImport.cs | 7 +- .../ImportLists/Trakt/TraktImportBase.cs | 3 +- .../ImportLists/Trakt/TraktParser.cs | 3 +- src/NzbDrone.Core/Localization/Core/en.json | 9 + .../Parser/Model/ImportListItemInfo.cs | 3 +- .../Config/ImportListConfigController.cs | 27 ++ .../Config/ImportListConfigResource.cs | 24 ++ 39 files changed, 1383 insertions(+), 112 deletions(-) create mode 100644 frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx create mode 100644 frontend/src/Store/Actions/Settings/importListOptions.js delete mode 100644 frontend/src/Store/Selectors/createSettingsSectionSelector.js create mode 100644 frontend/src/Store/Selectors/createSettingsSectionSelector.ts create mode 100644 frontend/src/typings/ImportListOptionsSettings.ts create mode 100644 frontend/src/typings/pending.ts create mode 100644 src/NzbDrone.Core.Test/ImportListTests/FetchAndParseImportListFixture.cs create mode 100644 src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/193_add_import_list_items.cs create mode 100644 src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs create mode 100644 src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs create mode 100644 src/NzbDrone.Core/ImportLists/ListSyncLevelType.cs create mode 100644 src/Sonarr.Api.V3/Config/ImportListConfigController.cs create mode 100644 src/Sonarr.Api.V3/Config/ImportListConfigResource.cs diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e249f2d20..cb0c78ba8 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,6 +7,7 @@ import AppSectionState, { import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; +import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; @@ -35,10 +36,15 @@ export interface QualityProfilesAppState extends AppSectionState<QualityProfile>, AppSectionSchemaState<QualityProfile> {} +export interface ImportListOptionsSettingsAppState + extends AppSectionItemState<ImportListOptionsSettings>, + AppSectionSaveState {} + export type LanguageSettingsAppState = AppSectionState<Language>; export type UiSettingsAppState = AppSectionItemState<UiSettings>; interface SettingsAppState { + advancedSettings: boolean; downloadClients: DownloadClientAppState; importLists: ImportListAppState; indexers: IndexerAppState; @@ -46,6 +52,7 @@ interface SettingsAppState { notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; ui: UiSettingsAppState; + importListOptions: ImportListOptionsSettingsAppState; } export default SettingsAppState; diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index 32a365860..de1d486b6 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -10,6 +10,7 @@ import translate from 'Utilities/String/translate'; import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; +import ImportListOptions from './Options/ImportListOptions'; class ImportListSettings extends Component { @@ -19,7 +20,10 @@ class ImportListSettings extends Component { constructor(props, context) { super(props, context); + this._saveCallback = null; + this.state = { + isSaving: false, hasPendingChanges: false, isManageImportListsOpen: false }; @@ -28,6 +32,14 @@ class ImportListSettings extends Component { // // Listeners + setChildSave = (saveCallback) => { + this._saveCallback = saveCallback; + }; + + onChildStateChange = (payload) => { + this.setState(payload); + }; + setListOptionsRef = (ref) => { this._listOptions = ref; }; @@ -47,7 +59,9 @@ class ImportListSettings extends Component { }; onSavePress = () => { - this._listOptions.getWrappedInstance().save(); + if (this._saveCallback) { + this._saveCallback(); + } }; // @@ -93,6 +107,12 @@ class ImportListSettings extends Component { <PageContentBody> <ImportListsConnector /> + + <ImportListOptions + setChildSave={this.setChildSave} + onChildStateChange={this.onChildStateChange} + /> + <ImportListsExclusionsConnector /> <ManageImportListsModal isOpen={isManageImportListsOpen} diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx new file mode 100644 index 000000000..28d06b1dc --- /dev/null +++ b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx @@ -0,0 +1,148 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import { inputTypes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchImportListOptions, + saveImportListOptions, + setImportListOptionsValue, +} from 'Store/Actions/settingsActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import translate from 'Utilities/String/translate'; + +const SECTION = 'importListOptions'; +const cleanLibraryLevelOptions = [ + { key: 'disabled', value: () => translate('Disabled') }, + { key: 'logOnly', value: () => translate('LogOnly') }, + { key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorSeries') }, + { key: 'keepAndTag', value: () => translate('KeepAndTagSeries') }, +]; + +function createImportListOptionsSelector() { + return createSelector( + (state: AppState) => state.settings.advancedSettings, + createSettingsSectionSelector(SECTION), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + save: sectionSettings.isSaving, + ...sectionSettings, + }; + } + ); +} + +interface ImportListOptionsPageProps { + setChildSave(saveCallback: () => void): void; + onChildStateChange(payload: unknown): void; +} + +function ImportListOptions(props: ImportListOptionsPageProps) { + const { setChildSave, onChildStateChange } = props; + const selected = useSelector(createImportListOptionsSelector()); + + const { + isSaving, + hasPendingChanges, + advancedSettings, + isFetching, + error, + settings, + hasSettings, + } = selected; + + const { listSyncLevel, listSyncTag } = settings; + + const dispatch = useDispatch(); + + const onInputChange = useCallback( + ({ name, value }: { name: string; value: unknown }) => { + // @ts-expect-error 'setImportListOptionsValue' isn't typed yet + dispatch(setImportListOptionsValue({ name, value })); + }, + [dispatch] + ); + + const onTagChange = useCallback( + ({ name, value }: { name: string; value: number[] }) => { + const id = value.length === 0 ? 0 : value.pop(); + // @ts-expect-error 'setImportListOptionsValue' isn't typed yet + dispatch(setImportListOptionsValue({ name, value: id })); + }, + [dispatch] + ); + + useEffect(() => { + dispatch(fetchImportListOptions()); + setChildSave(() => dispatch(saveImportListOptions())); + + return () => { + dispatch(clearPendingChanges({ section: SECTION })); + }; + }, [dispatch, setChildSave]); + + useEffect(() => { + onChildStateChange({ + isSaving, + hasPendingChanges, + }); + }, [onChildStateChange, isSaving, hasPendingChanges]); + + const translatedLevelOptions = cleanLibraryLevelOptions.map( + ({ key, value }) => { + return { + key, + value: value(), + }; + } + ); + + return advancedSettings ? ( + <FieldSet legend={translate('Options')}> + {isFetching ? <LoadingIndicator /> : null} + + {!isFetching && error ? ( + <div>{translate('UnableToLoadListOptions')}</div> + ) : null} + + {hasSettings && !isFetching && !error ? ( + <Form> + <FormGroup advancedSettings={advancedSettings} isAdvanced={true}> + <FormLabel>{translate('CleanLibraryLevel')}</FormLabel> + <FormInputGroup + type={inputTypes.SELECT} + name="listSyncLevel" + values={translatedLevelOptions} + helpText={translate('ListSyncLevelHelpText')} + onChange={onInputChange} + {...listSyncLevel} + /> + </FormGroup> + {listSyncLevel.value === 'keepAndTag' ? ( + <FormGroup advancedSettings={advancedSettings} isAdvanced={true}> + <FormLabel>{translate('ListSyncTag')}</FormLabel> + <FormInputGroup + {...listSyncTag} + type={inputTypes.TAG} + name="listSyncTag" + value={listSyncTag.value === 0 ? [] : [listSyncTag.value]} + helpText={translate('ListSyncTagHelpText')} + onChange={onTagChange} + /> + </FormGroup> + ) : null} + </Form> + ) : null} + </FieldSet> + ) : null; +} + +export default ImportListOptions; diff --git a/frontend/src/Store/Actions/Settings/importListOptions.js b/frontend/src/Store/Actions/Settings/importListOptions.js new file mode 100644 index 000000000..e33c80770 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/importListOptions.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import { createThunk } from 'Store/thunks'; + +// +// Variables + +const section = 'settings.importListOptions'; + +// +// Actions Types + +export const FETCH_IMPORT_LIST_OPTIONS = 'settings/importListOptions/fetchImportListOptions'; +export const SAVE_IMPORT_LIST_OPTIONS = 'settings/importListOptions/saveImportListOptions'; +export const SET_IMPORT_LIST_OPTIONS_VALUE = 'settings/importListOptions/setImportListOptionsValue'; + +// +// Action Creators + +export const fetchImportListOptions = createThunk(FETCH_IMPORT_LIST_OPTIONS); +export const saveImportListOptions = createThunk(SAVE_IMPORT_LIST_OPTIONS); +export const setImportListOptionsValue = createAction(SET_IMPORT_LIST_OPTIONS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_IMPORT_LIST_OPTIONS]: createFetchHandler(section, '/config/importlist'), + [SAVE_IMPORT_LIST_OPTIONS]: createSaveHandler(section, '/config/importlist') + }, + + // + // Reducers + + reducers: { + [SET_IMPORT_LIST_OPTIONS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index c1e6e5135..32ec41f8a 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -10,6 +10,7 @@ import downloadClientOptions from './Settings/downloadClientOptions'; import downloadClients from './Settings/downloadClients'; import general from './Settings/general'; import importListExclusions from './Settings/importListExclusions'; +import importListOptions from './Settings/importListOptions'; import importLists from './Settings/importLists'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; @@ -33,6 +34,7 @@ export * from './Settings/delayProfiles'; export * from './Settings/downloadClients'; export * from './Settings/downloadClientOptions'; export * from './Settings/general'; +export * from './Settings/importListOptions'; export * from './Settings/importLists'; export * from './Settings/importListExclusions'; export * from './Settings/indexerOptions'; @@ -69,6 +71,7 @@ export const defaultState = { general: general.defaultState, importLists: importLists.defaultState, importListExclusions: importListExclusions.defaultState, + importListOptions: importListOptions.defaultState, indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, languages: languages.defaultState, @@ -112,6 +115,7 @@ export const actionHandlers = handleThunks({ ...general.actionHandlers, ...importLists.actionHandlers, ...importListExclusions.actionHandlers, + ...importListOptions.actionHandlers, ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...languages.actionHandlers, @@ -146,6 +150,7 @@ export const reducers = createHandleActions({ ...general.reducers, ...importLists.reducers, ...importListExclusions.reducers, + ...importListOptions.reducers, ...indexerOptions.reducers, ...indexers.reducers, ...languages.reducers, diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js deleted file mode 100644 index a9f6cbff6..000000000 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.js +++ /dev/null @@ -1,32 +0,0 @@ -import { createSelector } from 'reselect'; -import selectSettings from 'Store/Selectors/selectSettings'; - -function createSettingsSectionSelector(section) { - return createSelector( - (state) => state.settings[section], - (sectionSettings) => { - const { - isFetching, - isPopulated, - error, - item, - pendingChanges, - isSaving, - saveError - } = sectionSettings; - - const settings = selectSettings(item, pendingChanges, saveError); - - return { - isFetching, - isPopulated, - error, - isSaving, - saveError, - ...settings - }; - } - ); -} - -export default createSettingsSectionSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts new file mode 100644 index 000000000..f43e4e59b --- /dev/null +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts @@ -0,0 +1,49 @@ +import { createSelector } from 'reselect'; +import AppSectionState, { + AppSectionItemState, +} from 'App/State/AppSectionState'; +import AppState from 'App/State/AppState'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; + +type SettingNames = keyof Omit<AppState['settings'], 'advancedSettings'>; +type GetSectionState<Name extends SettingNames> = AppState['settings'][Name]; +type GetSettingsSectionItemType<Name extends SettingNames> = + GetSectionState<Name> extends AppSectionItemState<infer R> + ? R + : GetSectionState<Name> extends AppSectionState<infer R> + ? R + : never; + +type AppStateWithPending<Name extends SettingNames> = { + item?: GetSettingsSectionItemType<Name>; + pendingChanges?: Partial<GetSettingsSectionItemType<Name>>; + saveError?: Error; +} & GetSectionState<Name>; + +function createSettingsSectionSelector<Name extends SettingNames>( + section: Name +) { + return createSelector( + (state: AppState) => state.settings[section], + (sectionSettings) => { + const { item, pendingChanges, saveError, ...other } = + sectionSettings as AppStateWithPending<Name>; + + const { settings, ...rest } = selectSettings( + item, + pendingChanges, + saveError + ); + + return { + ...other, + saveError, + settings: settings as PendingSection<GetSettingsSectionItemType<Name>>, + ...rest, + }; + } + ); +} + +export default createSettingsSectionSelector; diff --git a/frontend/src/typings/ImportListOptionsSettings.ts b/frontend/src/typings/ImportListOptionsSettings.ts new file mode 100644 index 000000000..4eb7d2039 --- /dev/null +++ b/frontend/src/typings/ImportListOptionsSettings.ts @@ -0,0 +1,10 @@ +export type ListSyncLevel = + | 'disabled' + | 'logOnly' + | 'keepAndUnmonitor' + | 'keepAndTag'; + +export default interface ImportListOptionsSettings { + listSyncLevel: ListSyncLevel; + listSyncTag: number; +} diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts new file mode 100644 index 000000000..53e885bcb --- /dev/null +++ b/frontend/src/typings/pending.ts @@ -0,0 +1,9 @@ +export interface Pending<T> { + value: T; + errors: any[]; + warnings: any[]; +} + +export type PendingSection<T> = { + [K in keyof T]: Pending<T[K]>; +}; diff --git a/src/NzbDrone.Core.Test/ImportListTests/FetchAndParseImportListFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/FetchAndParseImportListFixture.cs new file mode 100644 index 000000000..16600d698 --- /dev/null +++ b/src/NzbDrone.Core.Test/ImportListTests/FetchAndParseImportListFixture.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.ImportLists; +using NzbDrone.Core.ImportLists.ImportListItems; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ImportListTests +{ + [TestFixture] + public class FetchAndParseImportListServiceFixture : CoreTest<FetchAndParseImportListService> + { + private List<IImportList> _importLists; + private List<ImportListItemInfo> _listSeries; + + [SetUp] + public void Setup() + { + _importLists = new List<IImportList>(); + + Mocker.GetMock<IImportListFactory>() + .Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>())) + .Returns(_importLists); + + _listSeries = Builder<ImportListItemInfo>.CreateListOfSize(5) + .Build().ToList(); + + Mocker.GetMock<ISearchForNewSeries>() + .Setup(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>())) + .Returns((string value) => new List<Tv.Series>() { new Tv.Series() { ImdbId = value } }); + } + + private Mock<IImportList> WithList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null) + { + return CreateListResult(id, enabled, enabledAuto, fetchResult, minRefresh, lastSyncOffset, syncDeletedCount); + } + + private Mock<IImportList> CreateListResult(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null) + { + var refreshInterval = minRefresh ?? TimeSpan.FromHours(12); + var importListDefinition = new ImportListDefinition { Id = id, Enable = enabled, EnableAutomaticAdd = enabledAuto, MinRefreshInterval = refreshInterval }; + + var mockImportList = new Mock<IImportList>(); + mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition); + mockImportList.Setup(s => s.Fetch()).Returns(fetchResult); + mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(refreshInterval); + + DateTime? lastSync = lastSyncOffset.HasValue ? DateTime.UtcNow.AddHours(lastSyncOffset.Value) : null; + Mocker.GetMock<IImportListStatusService>() + .Setup(v => v.GetListStatus(id)) + .Returns(new ImportListStatus() { LastInfoSync = lastSync }); + + if (syncDeletedCount.HasValue) + { + Mocker.GetMock<IImportListItemService>() + .Setup(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), id)) + .Returns(syncDeletedCount.Value); + } + + _importLists.Add(mockImportList.Object); + + return mockImportList; + } + + [Test] + public void should_skip_recently_fetched_list() + { + var fetchResult = new ImportListFetchResult(); + var list = WithList(1, true, true, fetchResult, lastSyncOffset: 0); + + var result = Subject.Fetch(); + + list.Verify(f => f.Fetch(), Times.Never()); + result.Series.Count.Should().Be(0); + result.AnyFailure.Should().BeFalse(); + } + + [Test] + public void should_skip_recent_and_fetch_good() + { + var fetchResult = new ImportListFetchResult(); + var recent = WithList(1, true, true, fetchResult, lastSyncOffset: 0); + var old = WithList(2, true, true, fetchResult); + + var result = Subject.Fetch(); + + recent.Verify(f => f.Fetch(), Times.Never()); + old.Verify(f => f.Fetch(), Times.Once()); + result.AnyFailure.Should().BeFalse(); + } + + [Test] + public void should_return_failure_if_single_list_fails() + { + var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true }; + WithList(1, true, true, fetchResult); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeTrue(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(It.IsAny<int>(), It.IsAny<bool>()), Times.Never()); + } + + [Test] + public void should_return_failure_if_any_list_fails() + { + var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true }; + WithList(1, true, true, fetchResult1); + var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(2, true, true, fetchResult2); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeTrue(); + } + + [Test] + public void should_return_early_if_no_available_lists() + { + var listResult = Subject.Fetch(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.GetListStatus(It.IsAny<int>()), Times.Never()); + + listResult.Series.Count.Should().Be(0); + listResult.AnyFailure.Should().BeFalse(); + } + + [Test] + public void should_store_series_if_list_doesnt_fail() + { + var listId = 1; + var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(listId, true, true, fetchResult); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeFalse(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(listId, false), Times.Once()); + Mocker.GetMock<IImportListItemService>() + .Verify(v => v.SyncSeriesForList(_listSeries, listId), Times.Once()); + } + + [Test] + public void should_not_store_series_if_list_fails() + { + var listId = 1; + var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true }; + WithList(listId, true, true, fetchResult); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeTrue(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(listId, false), Times.Never()); + Mocker.GetMock<IImportListItemService>() + .Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), listId), Times.Never()); + } + + [Test] + public void should_only_store_series_for_lists_that_dont_fail() + { + var passedListId = 1; + var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(passedListId, true, true, fetchResult1); + var failedListId = 2; + var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true }; + WithList(failedListId, true, true, fetchResult2); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeTrue(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(passedListId, false), Times.Once()); + Mocker.GetMock<IImportListItemService>() + .Verify(v => v.SyncSeriesForList(_listSeries, passedListId), Times.Once()); + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(failedListId, false), Times.Never()); + Mocker.GetMock<IImportListItemService>() + .Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), failedListId), Times.Never()); + } + + [Test] + public void should_return_all_results_for_all_lists() + { + var passedListId = 1; + var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(passedListId, true, true, fetchResult1); + var secondListId = 2; + var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(secondListId, true, true, fetchResult2); + + var listResult = Subject.Fetch(); + listResult.AnyFailure.Should().BeFalse(); + listResult.Series.Count.Should().Be(5); + } + + [Test] + public void should_set_removed_flag_if_list_has_removed_items() + { + var listId = 1; + var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false }; + WithList(listId, true, true, fetchResult, syncDeletedCount: 500); + + var result = Subject.Fetch(); + result.AnyFailure.Should().BeFalse(); + + Mocker.GetMock<IImportListStatusService>() + .Verify(v => v.UpdateListSyncStatus(listId, true), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs new file mode 100644 index 000000000..661e2b357 --- /dev/null +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.ImportLists.ImportListItems; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ImportListTests +{ + public class ImportListItemServiceFixture : CoreTest<ImportListItemService> + { + [SetUp] + public void SetUp() + { + var existing = Builder<ImportListItemInfo>.CreateListOfSize(3) + .TheFirst(1) + .With(s => s.TvdbId = 6) + .With(s => s.ImdbId = "6") + .TheNext(1) + .With(s => s.TvdbId = 7) + .With(s => s.ImdbId = "7") + .TheNext(1) + .With(s => s.TvdbId = 8) + .With(s => s.ImdbId = "8") + .Build().ToList(); + Mocker.GetMock<IImportListItemInfoRepository>() + .Setup(v => v.GetAllForLists(It.IsAny<List<int>>())) + .Returns(existing); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing() + { + var newItems = Builder<ImportListItemInfo>.CreateListOfSize(3) + .TheFirst(1) + .With(s => s.TvdbId = 5) + .TheNext(1) + .With(s => s.TvdbId = 6) + .TheNext(1) + .With(s => s.TvdbId = 7) + .Build().ToList(); + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 5)), Times.Once()); + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 2 && s[0].TvdbId == 6 && s[1].TvdbId == 7)), Times.Once()); + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 8)), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 870dd66fc..3cf62f649 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -1,9 +1,13 @@ +using System; using System.Collections.Generic; using System.Linq; +using FizzWare.NBuilder; using Moq; using NUnit.Framework; +using NzbDrone.Core.Configuration; using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists.Exclusions; +using NzbDrone.Core.ImportLists.ImportListItems; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -13,17 +17,61 @@ namespace NzbDrone.Core.Test.ImportListTests { public class ImportListSyncServiceFixture : CoreTest<ImportListSyncService> { - private List<ImportListItemInfo> _importListReports; + private ImportListFetchResult _importListFetch; + private List<ImportListItemInfo> _list1Series; + private List<ImportListItemInfo> _list2Series; + + private List<Series> _existingSeries; + private List<IImportList> _importLists; + private ImportListSyncCommand _commandAll; + private ImportListSyncCommand _commandSingle; [SetUp] public void SetUp() { - var importListItem1 = new ImportListItemInfo + _importLists = new List<IImportList>(); + + var item1 = new ImportListItemInfo() { Title = "Breaking Bad" }; - _importListReports = new List<ImportListItemInfo> { importListItem1 }; + _list1Series = new List<ImportListItemInfo>() { item1 }; + + _existingSeries = Builder<Series>.CreateListOfSize(3) + .TheFirst(1) + .With(s => s.TvdbId = 6) + .With(s => s.ImdbId = "6") + .TheNext(1) + .With(s => s.TvdbId = 7) + .With(s => s.ImdbId = "7") + .TheNext(1) + .With(s => s.TvdbId = 8) + .With(s => s.ImdbId = "8") + .Build().ToList(); + + _list2Series = Builder<ImportListItemInfo>.CreateListOfSize(3) + .TheFirst(1) + .With(s => s.TvdbId = 6) + .With(s => s.ImdbId = "6") + .TheNext(1) + .With(s => s.TvdbId = 7) + .With(s => s.ImdbId = "7") + .TheNext(1) + .With(s => s.TvdbId = 8) + .With(s => s.ImdbId = "8") + .Build().ToList(); + + _importListFetch = new ImportListFetchResult(_list1Series, false); + + _commandAll = new ImportListSyncCommand + { + }; + + _commandSingle = new ImportListSyncCommand + { + DefinitionId = 1 + }; var mockImportList = new Mock<IImportList>(); @@ -31,6 +79,10 @@ namespace NzbDrone.Core.Test.ImportListTests .Setup(v => v.AllSeriesTvdbIds()) .Returns(new List<int>()); + Mocker.GetMock<ISeriesService>() + .Setup(v => v.GetAllSeries()) + .Returns(_existingSeries); + Mocker.GetMock<ISearchForNewSeries>() .Setup(v => v.SearchForNewSeries(It.IsAny<string>())) .Returns(new List<Series>()); @@ -41,15 +93,19 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock<IImportListFactory>() .Setup(v => v.All()) - .Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = MonitorTypes.All } }); + .Returns(() => _importLists.Select(x => x.Definition as ImportListDefinition).ToList()); + + Mocker.GetMock<IImportListFactory>() + .Setup(v => v.GetAvailableProviders()) + .Returns(_importLists); Mocker.GetMock<IImportListFactory>() .Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>())) - .Returns(new List<IImportList> { mockImportList.Object }); + .Returns(() => _importLists.Where(x => (x.Definition as ImportListDefinition).EnableAutomaticAdd).ToList()); Mocker.GetMock<IFetchAndParseImportList>() .Setup(v => v.Fetch()) - .Returns(_importListReports); + .Returns(_importListFetch); Mocker.GetMock<IImportListExclusionService>() .Setup(v => v.All()) @@ -58,19 +114,19 @@ namespace NzbDrone.Core.Test.ImportListTests private void WithTvdbId() { - _importListReports.First().TvdbId = 81189; + _list1Series.First().TvdbId = 81189; } private void WithImdbId() { - _importListReports.First().ImdbId = "tt0496424"; + _list1Series.First().ImdbId = "tt0496424"; } private void WithExistingSeries() { Mocker.GetMock<ISeriesService>() .Setup(v => v.AllSeriesTvdbIds()) - .Returns(new List<int> { _importListReports.First().TvdbId }); + .Returns(new List<int> { _list1Series.First().TvdbId }); } private void WithExcludedSeries() @@ -81,22 +137,281 @@ namespace NzbDrone.Core.Test.ImportListTests { new ImportListExclusion { - TvdbId = 81189 + TvdbId = _list1Series.First().TvdbId } }); } private void WithMonitorType(MonitorTypes monitor) { + _importLists.ForEach(li => (li.Definition as ImportListDefinition).ShouldMonitor = monitor); + } + + private void WithCleanLevel(ListSyncLevelType cleanLevel, int? tagId = null) + { + Mocker.GetMock<IConfigService>() + .SetupGet(v => v.ListSyncLevel) + .Returns(cleanLevel); + if (tagId.HasValue) + { + Mocker.GetMock<IConfigService>() + .SetupGet(v => v.ListSyncTag) + .Returns(tagId.Value); + } + } + + private void WithList(int id, bool enabledAuto, int lastSyncHoursOffset = 0, bool pendingRemovals = true, DateTime? disabledTill = null) + { + var importListDefinition = new ImportListDefinition { Id = id, EnableAutomaticAdd = enabledAuto }; + Mocker.GetMock<IImportListFactory>() - .Setup(v => v.All()) - .Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = monitor } }); + .Setup(v => v.Get(id)) + .Returns(importListDefinition); + + var mockImportList = new Mock<IImportList>(); + mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition); + mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(TimeSpan.FromHours(12)); + + var status = new ImportListStatus() + { + LastInfoSync = DateTime.UtcNow.AddHours(lastSyncHoursOffset), + HasRemovedItemSinceLastClean = pendingRemovals, + DisabledTill = disabledTill + }; + + if (disabledTill.HasValue) + { + _importListFetch.AnyFailure = true; + } + + Mocker.GetMock<IImportListStatusService>() + .Setup(v => v.GetListStatus(id)) + .Returns(status); + + _importLists.Add(mockImportList.Object); + } + + private void VerifyDidAddTag(int expectedSeriesCount, int expectedTagId) + { + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(x => x.Count == expectedSeriesCount && x.All(series => series.Tags.Contains(expectedTagId))), true), Times.Once()); + } + + [Test] + public void should_not_clean_library_if_lists_have_not_removed_any_items() + { + _importListFetch.Series = _existingSeries.Select(x => new ImportListItemInfo() { TvdbId = x.TvdbId }).ToList(); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true, pendingRemovals: false); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Never()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), true), Times.Never()); + } + + [Test] + public void should_not_clean_library_if_config_value_disable() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithCleanLevel(ListSyncLevelType.Disabled); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Never()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Never()); + } + + [Test] + public void should_log_only_on_clean_library_if_config_value_logonly() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithCleanLevel(ListSyncLevelType.LogOnly); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Once()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Once()); + } + + [Test] + public void should_unmonitor_on_clean_library_if_config_value_keepAndUnmonitor() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var monitored = _existingSeries.Count(x => x.Monitored); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Once()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == monitored && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_tvdb_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + Mocker.GetMock<IImportListItemService>() + .Setup(v => v.Exists(6, It.IsAny<string>())) + .Returns(true); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_imdb_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + var x = _importLists; + + Mocker.GetMock<IImportListItemService>() + .Setup(v => v.Exists(It.IsAny<int>(), "6")) + .Returns(true); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_tag_series_on_clean_library_if_config_value_keepAndTag() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndTag, 1); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Once()); + + VerifyDidAddTag(_existingSeries.Count, 1); + } + + [Test] + public void should_not_clean_if_list_failures() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true, disabledTill: DateTime.UtcNow.AddHours(1)); + WithCleanLevel(ListSyncLevelType.LogOnly); + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.GetAllSeries(), Times.Never()); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), It.IsAny<bool>()), Times.Never()); + Mocker.GetMock<ISeriesService>() + .Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never()); + } + + [Test] + public void should_add_new_series_from_single_list_to_library() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithCleanLevel(ListSyncLevelType.Disabled); + + Subject.Execute(_commandAll); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once()); + } + + [Test] + public void should_add_new_series_from_multiple_list_to_library() + { + _list2Series.ForEach(m => m.ImportListId = 2); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + _importListFetch.Series.AddRange(_list2Series); + + WithList(1, true); + WithList(2, true); + + WithCleanLevel(ListSyncLevelType.Disabled); + + Subject.Execute(_commandAll); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 4), true), Times.Once()); + } + + [Test] + public void should_add_new_series_to_library_only_from_enabled_lists() + { + _list2Series.ForEach(m => m.ImportListId = 2); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + _importListFetch.Series.AddRange(_list2Series); + + WithList(1, true); + WithList(2, false); + + WithCleanLevel(ListSyncLevelType.Disabled); + + Subject.Execute(_commandAll); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once()); + } + + [Test] + public void should_not_add_duplicate_series_from_seperate_lists() + { + _list2Series.ForEach(m => m.ImportListId = 2); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + _importListFetch.Series.AddRange(_list2Series); + _importListFetch.Series[0].TvdbId = 6; + + WithList(1, true); + WithList(2, true); + + WithCleanLevel(ListSyncLevelType.Disabled); + + Subject.Execute(_commandAll); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 3), true), Times.Once()); } [Test] public void should_search_if_series_title_and_no_series_id() { - Subject.Execute(new ImportListSyncCommand()); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + Subject.Execute(_commandAll); Mocker.GetMock<ISearchForNewSeries>() .Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once()); @@ -105,8 +420,10 @@ namespace NzbDrone.Core.Test.ImportListTests [Test] public void should_not_search_if_series_title_and_series_id() { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithTvdbId(); - Subject.Execute(new ImportListSyncCommand()); + Subject.Execute(_commandAll); Mocker.GetMock<ISearchForNewSeries>() .Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Never()); @@ -115,8 +432,10 @@ namespace NzbDrone.Core.Test.ImportListTests [Test] 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(new ImportListSyncCommand()); + Subject.Execute(_commandAll); Mocker.GetMock<ISearchForNewSeries>() .Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once()); @@ -125,10 +444,12 @@ namespace NzbDrone.Core.Test.ImportListTests [Test] public void should_not_add_if_existing_series() { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithTvdbId(); WithExistingSeries(); - Subject.Execute(new ImportListSyncCommand()); + Subject.Execute(_commandAll); Mocker.GetMock<IAddSeriesService>() .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>())); @@ -138,10 +459,12 @@ namespace NzbDrone.Core.Test.ImportListTests [TestCase(MonitorTypes.All, true)] public void should_add_if_not_existing_series(MonitorTypes monitor, bool expectedSeriesMonitored) { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithTvdbId(); WithMonitorType(monitor); - Subject.Execute(new ImportListSyncCommand()); + Subject.Execute(_commandAll); Mocker.GetMock<IAddSeriesService>() .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1 && t.First().Monitored == expectedSeriesMonitored), It.IsAny<bool>())); @@ -150,10 +473,12 @@ namespace NzbDrone.Core.Test.ImportListTests [Test] public void should_not_add_if_excluded_series() { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithTvdbId(); WithExcludedSeries(); - Subject.Execute(new ImportListSyncCommand()); + Subject.Execute(_commandAll); Mocker.GetMock<IAddSeriesService>() .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>())); @@ -177,7 +502,7 @@ namespace NzbDrone.Core.Test.ImportListTests { Mocker.GetMock<IFetchAndParseImportList>() .Setup(v => v.Fetch()) - .Returns(new List<ImportListItemInfo>()); + .Returns(new ImportListFetchResult()); Subject.Execute(new ImportListSyncCommand()); diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 2193b182b..65eb7a5be 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.ImportLists; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; @@ -276,6 +277,18 @@ namespace NzbDrone.Core.Configuration set { SetValue("ChownGroup", value); } } + public ListSyncLevelType ListSyncLevel + { + get { return GetValueEnum("ListSyncLevel", ListSyncLevelType.Disabled); } + set { SetValue("ListSyncLevel", value); } + } + + public int ListSyncTag + { + get { return GetValueInt("ListSyncTag"); } + set { SetValue("ListSyncTag", value); } + } + public int FirstDayOfWeek { get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 2bcd7b923..00aaf94c3 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.ImportLists; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Qualities; @@ -52,6 +53,9 @@ namespace NzbDrone.Core.Configuration int MaximumSize { get; set; } int MinimumAge { get; set; } + ListSyncLevelType ListSyncLevel { get; set; } + int ListSyncTag { get; set; } + // UI int FirstDayOfWeek { get; set; } string CalendarWeekColumnHeader { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/193_add_import_list_items.cs b/src/NzbDrone.Core/Datastore/Migration/193_add_import_list_items.cs new file mode 100644 index 000000000..3f0ed5003 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/193_add_import_list_items.cs @@ -0,0 +1,26 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(193)] + public class add_import_list_items : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("ImportListItems") + .WithColumn("ImportListId").AsInt32() + .WithColumn("Title").AsString() + .WithColumn("TvdbId").AsInt32() + .WithColumn("Year").AsInt32().Nullable() + .WithColumn("TmdbId").AsInt32().Nullable() + .WithColumn("ImdbId").AsString().Nullable() + .WithColumn("MalId").AsInt32().Nullable() + .WithColumn("AniListId").AsInt32().Nullable() + .WithColumn("ReleaseDate").AsDateTimeOffset().Nullable(); + + Alter.Table("ImportListStatus") + .AddColumn("HasRemovedItemSinceLastClean").AsBoolean().WithDefaultValue(false); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 4ada8a42b..6496c5466 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -81,6 +81,9 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.MinRefreshInterval) .Ignore(i => i.Enable); + Mapper.Entity<ImportListItemInfo>("ImportListItems").RegisterModel() + .Ignore(i => i.ImportList); + Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel() .Ignore(x => x.ImplementationName) .Ignore(i => i.SupportsOnGrab) diff --git a/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs b/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs index b5818775b..b6bd395a3 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/AniListImportBase.cs @@ -5,7 +5,6 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.AniList @@ -65,7 +64,7 @@ namespace NzbDrone.Core.ImportLists.AniList return new { }; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { CheckToken(); return base.Fetch(); diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs index ad62eb1a3..f2ecb26e7 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs @@ -44,10 +44,11 @@ namespace NzbDrone.Core.ImportLists.AniList.List return new AniListParser(Settings); } - protected override IList<ImportListItemInfo> FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false) + protected override ImportListFetchResult FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false) { var releases = new List<ImportListItemInfo>(); var url = string.Empty; + var anyFailure = true; try { @@ -77,6 +78,7 @@ namespace NzbDrone.Core.ImportLists.AniList.List while (hasNextPage); _importListStatusService.RecordSuccess(Definition.Id); + anyFailure = false; } catch (WebException webException) { @@ -149,7 +151,7 @@ namespace NzbDrone.Core.ImportLists.AniList.List _logger.Error(ex, "An error occurred while processing feed. {0}", url); } - return CleanupListItems(releases); + return new ImportListFetchResult(CleanupListItems(releases), anyFailure); } } } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs index 3d4645c49..f82e2f9f9 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs @@ -30,9 +30,10 @@ namespace NzbDrone.Core.ImportLists.Custom _customProxy = customProxy; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { var series = new List<ImportListItemInfo>(); + var anyFailure = false; try { @@ -50,12 +51,13 @@ namespace NzbDrone.Core.ImportLists.Custom } catch (Exception ex) { + anyFailure = true; _logger.Debug(ex, "Failed to fetch data for list {0} ({1})", Definition.Name, Name); _importListStatusService.RecordFailure(Definition.Id); } - return CleanupListItems(series); + return new ImportListFetchResult(CleanupListItems(series), anyFailure); } public override object RequestAction(string action, IDictionary<string, string> query) diff --git a/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs b/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs index d8cb3f218..5c6250d40 100644 --- a/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs +++ b/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs @@ -4,32 +4,34 @@ using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common.TPL; -using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ImportLists.ImportListItems; namespace NzbDrone.Core.ImportLists { public interface IFetchAndParseImportList { - List<ImportListItemInfo> Fetch(); - List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition); + ImportListFetchResult Fetch(); + ImportListFetchResult FetchSingleList(ImportListDefinition definition); } public class FetchAndParseImportListService : IFetchAndParseImportList { private readonly IImportListFactory _importListFactory; private readonly IImportListStatusService _importListStatusService; + private readonly IImportListItemService _importListItemService; private readonly Logger _logger; - public FetchAndParseImportListService(IImportListFactory importListFactory, IImportListStatusService importListStatusService, Logger logger) + public FetchAndParseImportListService(IImportListFactory importListFactory, IImportListStatusService importListStatusService, IImportListItemService importListItemService, Logger logger) { _importListFactory = importListFactory; _importListStatusService = importListStatusService; + _importListItemService = importListItemService; _logger = logger; } - public List<ImportListItemInfo> Fetch() + public ImportListFetchResult Fetch() { - var result = new List<ImportListItemInfo>(); + var result = new ImportListFetchResult(); var importLists = _importListFactory.AutomaticAddEnabled(); @@ -47,7 +49,7 @@ namespace NzbDrone.Core.ImportLists foreach (var importList in importLists) { var importListLocal = importList; - var importListStatus = _importListStatusService.GetLastSyncListInfo(importListLocal.Definition.Id); + var importListStatus = _importListStatusService.GetListStatus(importListLocal.Definition.Id).LastInfoSync; if (importListStatus.HasValue) { @@ -64,16 +66,23 @@ namespace NzbDrone.Core.ImportLists { try { - var importListReports = importListLocal.Fetch(); + var fetchResult = importListLocal.Fetch(); + var importListReports = fetchResult.Series; lock (result) { _logger.Debug("Found {0} reports from {1} ({2})", importListReports.Count, importList.Name, importListLocal.Definition.Name); - result.AddRange(importListReports); - } + if (!fetchResult.AnyFailure) + { + importListReports.ForEach(s => s.ImportListId = importList.Definition.Id); + result.Series.AddRange(importListReports); + var removed = _importListItemService.SyncSeriesForList(importListReports, importList.Definition.Id); + _importListStatusService.UpdateListSyncStatus(importList.Definition.Id, removed > 0); + } - _importListStatusService.UpdateListSyncStatus(importList.Definition.Id); + result.AnyFailure |= fetchResult.AnyFailure; + } } catch (Exception e) { @@ -86,16 +95,16 @@ namespace NzbDrone.Core.ImportLists Task.WaitAll(taskList.ToArray()); - result = result.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList(); + result.Series = result.Series.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList(); - _logger.Debug("Found {0} total reports from {1} lists", result.Count, importLists.Count); + _logger.Debug("Found {0} total reports from {1} lists", result.Series.Count, importLists.Count); return result; } - public List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition) + public ImportListFetchResult FetchSingleList(ImportListDefinition definition) { - var result = new List<ImportListItemInfo>(); + var result = new ImportListFetchResult(); var importList = _importListFactory.GetInstance(definition); @@ -114,16 +123,25 @@ namespace NzbDrone.Core.ImportLists { try { - var importListReports = importListLocal.Fetch(); + var fetchResult = importListLocal.Fetch(); + var importListReports = fetchResult.Series; lock (result) { _logger.Debug("Found {0} reports from {1} ({2})", importListReports.Count, importList.Name, importListLocal.Definition.Name); - result.AddRange(importListReports); + if (!fetchResult.AnyFailure) + { + importListReports.ForEach(s => s.ImportListId = importList.Definition.Id); + result.Series.AddRange(importListReports); + var removed = _importListItemService.SyncSeriesForList(importListReports, importList.Definition.Id); + _importListStatusService.UpdateListSyncStatus(importList.Definition.Id, removed > 0); + } + + result.AnyFailure |= fetchResult.AnyFailure; } - _importListStatusService.UpdateListSyncStatus(importList.Definition.Id); + result.AnyFailure |= fetchResult.AnyFailure; } catch (Exception e) { @@ -135,8 +153,6 @@ namespace NzbDrone.Core.ImportLists Task.WaitAll(taskList.ToArray()); - result = result.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList(); - return result; } } diff --git a/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs b/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs index 25057c2fd..ebb948a89 100644 --- a/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/HttpImportListBase.cs @@ -38,15 +38,16 @@ namespace NzbDrone.Core.ImportLists _httpClient = httpClient; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { return FetchItems(g => g.GetListItems(), true); } - protected virtual IList<ImportListItemInfo> FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false) + protected virtual ImportListFetchResult FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false) { var releases = new List<ImportListItemInfo>(); var url = string.Empty; + var anyFailure = true; try { @@ -92,6 +93,7 @@ namespace NzbDrone.Core.ImportLists } _importListStatusService.RecordSuccess(Definition.Id); + anyFailure = false; } catch (WebException webException) { @@ -163,7 +165,7 @@ namespace NzbDrone.Core.ImportLists _logger.Error(ex, "An error occurred while processing feed. {0}", url); } - return CleanupListItems(releases); + return new ImportListFetchResult(CleanupListItems(releases), anyFailure); } protected virtual bool IsValidItem(ImportListItemInfo listItem) diff --git a/src/NzbDrone.Core/ImportLists/IImportList.cs b/src/NzbDrone.Core/ImportLists/IImportList.cs index 4970c5261..43047f97a 100644 --- a/src/NzbDrone.Core/ImportLists/IImportList.cs +++ b/src/NzbDrone.Core/ImportLists/IImportList.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.ImportLists @@ -9,6 +7,6 @@ namespace NzbDrone.Core.ImportLists { ImportListType ListType { get; } TimeSpan MinRefreshInterval { get; } - IList<ImportListItemInfo> Fetch(); + ImportListFetchResult Fetch(); } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListBase.cs b/src/NzbDrone.Core/ImportLists/ImportListBase.cs index 5806e3095..9011a9213 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListBase.cs @@ -11,6 +11,23 @@ using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.ImportLists { + public class ImportListFetchResult + { + public ImportListFetchResult() + { + Series = new List<ImportListItemInfo>(); + } + + public ImportListFetchResult(IEnumerable<ImportListItemInfo> series, bool anyFailure) + { + Series = series.ToList(); + AnyFailure = anyFailure; + } + + public List<ImportListItemInfo> Series { get; set; } + public bool AnyFailure { get; set; } + } + public abstract class ImportListBase<TSettings> : IImportList where TSettings : IImportListSettings, new() { @@ -63,7 +80,7 @@ namespace NzbDrone.Core.ImportLists protected TSettings Settings => (TSettings)Definition.Settings; - public abstract IList<ImportListItemInfo> Fetch(); + public abstract ImportListFetchResult Fetch(); protected virtual IList<ImportListItemInfo> CleanupListItems(IEnumerable<ImportListItemInfo> releases) { diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs new file mode 100644 index 000000000..abe546026 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.ImportListItems +{ + public interface IImportListItemInfoRepository : IBasicRepository<ImportListItemInfo> + { + List<ImportListItemInfo> GetAllForLists(List<int> listIds); + bool Exists(int tvdbId, string imdbId); + } + + public class ImportListItemRepository : BasicRepository<ImportListItemInfo>, IImportListItemInfoRepository + { + public ImportListItemRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List<ImportListItemInfo> GetAllForLists(List<int> listIds) + { + return Query(x => listIds.Contains(x.ImportListId)); + } + + public bool Exists(int tvdbId, string imdbId) + { + List<ImportListItemInfo> items; + + if (string.IsNullOrWhiteSpace(imdbId)) + { + items = Query(x => x.TvdbId == tvdbId); + } + else + { + items = Query(x => x.TvdbId == tvdbId || x.ImdbId == imdbId); + } + + return items.Any(); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs new file mode 100644 index 000000000..852a30ee5 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.ImportLists.ImportListItems +{ + public interface IImportListItemService + { + List<ImportListItemInfo> GetAllForLists(List<int> listIds); + int SyncSeriesForList(List<ImportListItemInfo> listSeries, int listId); + bool Exists(int tvdbId, string imdbId); + } + + public class ImportListItemService : IImportListItemService, IHandleAsync<ProviderDeletedEvent<IImportList>> + { + private readonly IImportListItemInfoRepository _importListSeriesRepository; + private readonly Logger _logger; + + public ImportListItemService(IImportListItemInfoRepository importListSeriesRepository, + Logger logger) + { + _importListSeriesRepository = importListSeriesRepository; + _logger = logger; + } + + public int SyncSeriesForList(List<ImportListItemInfo> listSeries, int listId) + { + var existingListSeries = GetAllForLists(new List<int> { listId }); + + listSeries.ForEach(l => l.Id = existingListSeries.FirstOrDefault(e => e.TvdbId == l.TvdbId)?.Id ?? 0); + + _importListSeriesRepository.InsertMany(listSeries.Where(l => l.Id == 0).ToList()); + _importListSeriesRepository.UpdateMany(listSeries.Where(l => l.Id > 0).ToList()); + var toDelete = existingListSeries.Where(l => !listSeries.Any(x => x.TvdbId == l.TvdbId)).ToList(); + _importListSeriesRepository.DeleteMany(toDelete); + + return toDelete.Count; + } + + public List<ImportListItemInfo> GetAllForLists(List<int> listIds) + { + return _importListSeriesRepository.GetAllForLists(listIds).ToList(); + } + + public void HandleAsync(ProviderDeletedEvent<IImportList> message) + { + var seriesOnList = _importListSeriesRepository.GetAllForLists(new List<int> { message.ProviderId }); + _importListSeriesRepository.DeleteMany(seriesOnList); + } + + public bool Exists(int tvdbId, string imdbId) + { + return _importListSeriesRepository.Exists(tvdbId, imdbId); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListStatus.cs b/src/NzbDrone.Core/ImportLists/ImportListStatus.cs index 60d6aa3a0..b7eeabe7b 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListStatus.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListStatus.cs @@ -6,5 +6,6 @@ namespace NzbDrone.Core.ImportLists public class ImportListStatus : ProviderStatusBase { public DateTime? LastInfoSync { get; set; } + public bool HasRemovedItemSinceLastClean { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListStatusService.cs b/src/NzbDrone.Core/ImportLists/ImportListStatusService.cs index bbef4b179..35005d145 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListStatusService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListStatusService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Messaging.Events; @@ -8,9 +9,10 @@ namespace NzbDrone.Core.ImportLists { public interface IImportListStatusService : IProviderStatusServiceBase<ImportListStatus> { - DateTime? GetLastSyncListInfo(int importListId); + ImportListStatus GetListStatus(int importListId); - void UpdateListSyncStatus(int importListId); + void UpdateListSyncStatus(int importListId, bool removedItems); + void MarkListsAsCleaned(); } public class ImportListStatusService : ProviderStatusServiceBase<IImportList, ImportListStatus>, IImportListStatusService @@ -20,21 +22,38 @@ namespace NzbDrone.Core.ImportLists { } - public DateTime? GetLastSyncListInfo(int importListId) + public ImportListStatus GetListStatus(int importListId) { - return GetProviderStatus(importListId).LastInfoSync; + return GetProviderStatus(importListId); } - public void UpdateListSyncStatus(int importListId) + public void UpdateListSyncStatus(int importListId, bool removedItems) { lock (_syncRoot) { var status = GetProviderStatus(importListId); status.LastInfoSync = DateTime.UtcNow; + status.HasRemovedItemSinceLastClean |= removedItems; _providerStatusRepository.Upsert(status); } } + + public void MarkListsAsCleaned() + { + lock (_syncRoot) + { + var toUpdate = new List<ImportListStatus>(); + + foreach (var status in _providerStatusRepository.All()) + { + status.HasRemovedItemSinceLastClean = false; + toUpdate.Add(status); + } + + _providerStatusRepository.UpdateMany(toUpdate); + } + } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 12109a94a..f6d414562 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -3,41 +3,85 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.ImportLists.Exclusions; +using NzbDrone.Core.ImportLists.ImportListItems; +using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.Tv; namespace NzbDrone.Core.ImportLists { - public class ImportListSyncService : IExecute<ImportListSyncCommand> + public class ImportListSyncService : IExecute<ImportListSyncCommand>, IHandleAsync<ProviderDeletedEvent<IImportList>> { private readonly IImportListFactory _importListFactory; + private readonly IImportListStatusService _importListStatusService; private readonly IImportListExclusionService _importListExclusionService; + private readonly IImportListItemService _importListItemService; private readonly IFetchAndParseImportList _listFetcherAndParser; private readonly ISearchForNewSeries _seriesSearchService; private readonly ISeriesService _seriesService; private readonly IAddSeriesService _addSeriesService; + private readonly IConfigService _configService; + private readonly ITaskManager _taskManager; private readonly Logger _logger; public ImportListSyncService(IImportListFactory importListFactory, + IImportListStatusService importListStatusService, IImportListExclusionService importListExclusionService, + IImportListItemService importListItemService, IFetchAndParseImportList listFetcherAndParser, ISearchForNewSeries seriesSearchService, ISeriesService seriesService, IAddSeriesService addSeriesService, + IConfigService configService, + ITaskManager taskManager, Logger logger) { _importListFactory = importListFactory; + _importListStatusService = importListStatusService; _importListExclusionService = importListExclusionService; + _importListItemService = importListItemService; _listFetcherAndParser = listFetcherAndParser; _seriesSearchService = seriesSearchService; _seriesService = seriesService; _addSeriesService = addSeriesService; + _configService = configService; + _taskManager = taskManager; _logger = logger; } + private bool AllListsSuccessfulWithAPendingClean() + { + var lists = _importListFactory.AutomaticAddEnabled(false); + var anyRemoved = false; + + foreach (var list in lists) + { + var status = _importListStatusService.GetListStatus(list.Definition.Id); + + if (status.DisabledTill.HasValue) + { + // list failed the last time it was synced. + return false; + } + + if (!status.LastInfoSync.HasValue) + { + // list has never been synced. + return false; + } + + anyRemoved |= status.HasRemovedItemSinceLastClean; + } + + return anyRemoved; + } + private void SyncAll() { if (_importListFactory.AutomaticAddEnabled().Empty()) @@ -49,18 +93,26 @@ namespace NzbDrone.Core.ImportLists _logger.ProgressInfo("Starting Import List Sync"); - var listItems = _listFetcherAndParser.Fetch().ToList(); + var result = _listFetcherAndParser.Fetch(); + + var listItems = result.Series.ToList(); ProcessListItems(listItems); + + TryCleanLibrary(); } private void SyncList(ImportListDefinition definition) { _logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name)); - var listItems = _listFetcherAndParser.FetchSingleList(definition).ToList(); + var result = _listFetcherAndParser.FetchSingleList(definition); + + var listItems = result.Series.ToList(); ProcessListItems(listItems); + + TryCleanLibrary(); } private void ProcessListItems(List<ImportListItemInfo> items) @@ -90,6 +142,11 @@ namespace NzbDrone.Core.ImportLists var importList = importLists.Single(x => x.Id == item.ImportListId); + if (!importList.EnableAutomaticAdd) + { + continue; + } + // Map by IMDb ID if we have it if (item.TvdbId <= 0 && item.ImdbId.IsNotNullOrWhiteSpace()) { @@ -180,10 +237,10 @@ namespace NzbDrone.Core.ImportLists SeasonFolder = importList.SeasonFolder, Tags = importList.Tags, AddOptions = new AddSeriesOptions - { - SearchForMissingEpisodes = importList.SearchForMissingEpisodes, - Monitor = importList.ShouldMonitor - } + { + SearchForMissingEpisodes = importList.SearchForMissingEpisodes, + Monitor = importList.ShouldMonitor + } }); } } @@ -206,5 +263,64 @@ namespace NzbDrone.Core.ImportLists SyncAll(); } } + + private void TryCleanLibrary() + { + if (_configService.ListSyncLevel == ListSyncLevelType.Disabled) + { + return; + } + + if (AllListsSuccessfulWithAPendingClean()) + { + CleanLibrary(); + } + } + + private void CleanLibrary() + { + if (_configService.ListSyncLevel == ListSyncLevelType.Disabled) + { + return; + } + + var seriesToUpdate = new List<Series>(); + var seriesInLibrary = _seriesService.GetAllSeries(); + + foreach (var series in seriesInLibrary) + { + var seriesExists = _importListItemService.Exists(series.TvdbId, series.ImdbId); + + if (!seriesExists) + { + switch (_configService.ListSyncLevel) + { + case ListSyncLevelType.LogOnly: + _logger.Info("{0} was in your library, but not found in your lists --> You might want to unmonitor or remove it", series); + break; + case ListSyncLevelType.KeepAndUnmonitor when series.Monitored: + _logger.Info("{0} was in your library, but not found in your lists --> Keeping in library but unmonitoring it", series); + series.Monitored = false; + seriesToUpdate.Add(series); + break; + case ListSyncLevelType.KeepAndTag when !series.Tags.Contains(_configService.ListSyncTag): + _logger.Info("{0} was in your library, but not found in your lists --> Keeping in library but tagging it", series); + series.Tags.Add(_configService.ListSyncTag); + seriesToUpdate.Add(series); + break; + default: + break; + } + } + } + + _seriesService.UpdateSeries(seriesToUpdate, true); + _importListStatusService.MarkListsAsCleaned(); + } + + public void HandleAsync(ProviderDeletedEvent<IImportList> message) + { + TryCleanLibrary(); + } } } diff --git a/src/NzbDrone.Core/ImportLists/ListSyncLevelType.cs b/src/NzbDrone.Core/ImportLists/ListSyncLevelType.cs new file mode 100644 index 000000000..cdbf90c41 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/ListSyncLevelType.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.ImportLists +{ + public enum ListSyncLevelType + { + Disabled, + LogOnly, + KeepAndUnmonitor, + KeepAndTag + } +} diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs index 393bdb692..ee50b3540 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs @@ -8,7 +8,6 @@ using NzbDrone.Core.Exceptions; using NzbDrone.Core.Localization; using NzbDrone.Core.Notifications.Plex.PlexTv; using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Plex @@ -35,7 +34,7 @@ namespace NzbDrone.Core.ImportLists.Plex public override string Name => _localizationService.GetLocalizedString("ImportListsPlexSettingsWatchlistName"); public override int PageSize => 50; - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { Settings.Validate().Filter("AccessToken").ThrowOnError(); diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs index ef6b6a7ef..f00a66253 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.ImportLists.Rss { @@ -26,7 +24,7 @@ namespace NzbDrone.Core.ImportLists.Rss { } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { return FetchItems(g => g.GetListItems()); } diff --git a/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs b/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs index 8592fb1cc..bc0240c07 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/SimklImportBase.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.ImportLists.Simkl _importListRepository = netImportRepository; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError(); _logger.Trace($"Access token expires at {Settings.Expires}"); @@ -47,13 +47,14 @@ namespace NzbDrone.Core.ImportLists.Simkl RefreshToken(); } - var lastFetch = _importListStatusService.GetLastSyncListInfo(Definition.Id); + var lastFetch = _importListStatusService.GetListStatus(Definition.Id).LastInfoSync; var lastActivity = GetLastActivity(); // Check to see if user has any activity since last sync, if not return empty to avoid work if (lastFetch.HasValue && lastActivity < lastFetch.Value.AddHours(-2)) { - return Array.Empty<ImportListItemInfo>(); + // mark failure to avoid deleting series due to emptyness + return new ImportListFetchResult(new List<ImportListItemInfo>(), true); } var generator = GetRequestGenerator(); diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs index 369b10e52..3a35ecf6d 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs @@ -31,10 +31,10 @@ namespace NzbDrone.Core.ImportLists.Sonarr _sonarrV3Proxy = sonarrV3Proxy; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { var series = new List<ImportListItemInfo>(); - + var anyFailure = false; try { var remoteSeries = _sonarrV3Proxy.GetSeries(Settings); @@ -75,9 +75,10 @@ namespace NzbDrone.Core.ImportLists.Sonarr _logger.Debug(ex, "Failed to fetch data for list {0} ({1})", Definition.Name, Name); _importListStatusService.RecordFailure(Definition.Id); + anyFailure = true; } - return CleanupListItems(series); + return new ImportListFetchResult(CleanupListItems(series), anyFailure); } public override object RequestAction(string action, IDictionary<string, string> query) diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs index 848fe9553..b7397138c 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs @@ -6,7 +6,6 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Trakt @@ -36,7 +35,7 @@ namespace NzbDrone.Core.ImportLists.Trakt _importListRepository = netImportRepository; } - public override IList<ImportListItemInfo> Fetch() + public override ImportListFetchResult Fetch() { Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError(); _logger.Trace($"Access token expires at {Settings.Expires}"); diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs index ffde5a462..46f41b6d5 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs @@ -35,7 +35,8 @@ namespace NzbDrone.Core.ImportLists.Trakt series.AddIfNotNull(new ImportListItemInfo() { Title = traktResponse.Show.Title, - TvdbId = traktResponse.Show.Ids.Tvdb.GetValueOrDefault() + TvdbId = traktResponse.Show.Ids.Tvdb.GetValueOrDefault(), + ImdbId = traktResponse.Show.Ids.Imdb }); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f5e78bbd4..153a894cd 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -208,6 +208,7 @@ "ChownGroup": "chown Group", "ChownGroupHelpText": "Group name or gid. Use gid for remote file systems.", "ChownGroupHelpTextWarning": "This only works if the user running {appName} is the owner of the file. It's better to ensure the download client uses the same group as {appName}.", + "CleanLibraryLevel": "Clean Library Level", "Clear": "Clear", "ClearBlocklist": "Clear blocklist", "ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?", @@ -790,6 +791,7 @@ "ImportListSearchForMissingEpisodes": "Search for Missing Episodes", "ImportListSearchForMissingEpisodesHelpText": "After series is added to {appName} automatically search for missing episodes", "ImportListSettings": "Import List Settings", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "All lists require manual interaction due to possible partial fetches", "ImportListStatusAllUnavailableHealthCheckMessage": "All lists are unavailable due to failures", "ImportListStatusUnavailableHealthCheckMessage": "Lists unavailable due to failures: {importListNames}", "ImportLists": "Import Lists", @@ -1010,6 +1012,8 @@ "Interval": "Interval", "InvalidFormat": "Invalid Format", "InvalidUILanguage": "Your UI is set to an invalid language, correct it and save your settings", + "KeepAndTagSeries": "Keep and Tag Series", + "KeepAndUnmonitorSeries": "Keep and Unmonitor Series", "KeyboardShortcuts": "Keyboard Shortcuts", "KeyboardShortcutsCloseModal": "Close Current Modal", "KeyboardShortcutsConfirmModal": "Accept Confirmation Modal", @@ -1038,6 +1042,9 @@ "ListOptionsLoadError": "Unable to load list options", "ListQualityProfileHelpText": "Quality Profile list items will be added with", "ListRootFolderHelpText": "Root Folder list items will be added to", + "ListSyncLevelHelpText": "Series in library will be handled based on your selection if they fall off or do not appear on your list(s)", + "ListSyncTag": "List Sync Tag", + "ListSyncTagHelpText": "This tag will be added when a series falls off or is no longer on your list(s)", "ListTagsHelpText": "Tags that will be added on import from this list", "ListWillRefreshEveryInterval": "List will refresh every {refreshInterval}", "ListsLoadError": "Unable to load Lists", @@ -1050,6 +1057,7 @@ "LogFilesLocation": "Log files are located in: {location}", "LogLevel": "Log Level", "LogLevelTraceHelpTextWarning": "Trace logging should only be enabled temporarily", + "LogOnly": "Log Only", "Logging": "Logging", "Logout": "Logout", "Logs": "Logs", @@ -1946,6 +1954,7 @@ "Umask777Description": "{octal} - Everyone write", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", + "UnableToLoadListOptions": "Unable to load list options", "UnableToUpdateSonarrDirectly": "Unable to update {appName} directly,", "Unavailable": "Unavailable", "Underscore": "Underscore", diff --git a/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs b/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs index 7c521a4e6..8d0e37534 100644 --- a/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs @@ -1,8 +1,9 @@ using System; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Parser.Model { - public class ImportListItemInfo + public class ImportListItemInfo : ModelBase { public int ImportListId { get; set; } public string ImportList { get; set; } diff --git a/src/Sonarr.Api.V3/Config/ImportListConfigController.cs b/src/Sonarr.Api.V3/Config/ImportListConfigController.cs new file mode 100644 index 000000000..dba64a441 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/ImportListConfigController.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.ImportLists; +using NzbDrone.Core.Validation; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Config +{ + [V3ApiController("config/importlist")] + + public class ImportListConfigController : ConfigController<ImportListConfigResource> + { + public ImportListConfigController(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(x => x.ListSyncTag) + .ValidId() + .WithMessage("Tag must be specified") + .When(x => x.ListSyncLevel == ListSyncLevelType.KeepAndTag); + } + + protected override ImportListConfigResource ToResource(IConfigService model) + { + return ImportListConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/ImportListConfigResource.cs b/src/Sonarr.Api.V3/Config/ImportListConfigResource.cs new file mode 100644 index 000000000..b3f80af54 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/ImportListConfigResource.cs @@ -0,0 +1,24 @@ +using NzbDrone.Core.Configuration; +using NzbDrone.Core.ImportLists; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class ImportListConfigResource : RestResource + { + public ListSyncLevelType ListSyncLevel { get; set; } + public int ListSyncTag { get; set; } + } + + public static class ImportListConfigResourceMapper + { + public static ImportListConfigResource ToResource(IConfigService model) + { + return new ImportListConfigResource + { + ListSyncLevel = model.ListSyncLevel, + ListSyncTag = model.ListSyncTag, + }; + } + } +} From 9e3f9f961823c83d3e61381b1dad9874f3b309a2 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:38:43 +0200 Subject: [PATCH 069/762] Fixed: Testing for disabled Notifications --- src/NzbDrone.Core/Notifications/NotificationDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index c4530c700..a18376d0d 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -30,6 +30,6 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnApplicationUpdate { get; set; } public bool SupportsOnManualInteractionRequired { get; set; } - public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || OnEpisodeFileDeleteForUpgrade || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; + public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; } } From 0ea189d03c8c5e02c00b96a4281dd9e668d6a9ae Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 24 Jan 2024 16:34:46 -0800 Subject: [PATCH 070/762] Fixed: History retention for Newsbin --- .../DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs | 2 ++ src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index b81633b51..9b74e81e8 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -454,6 +454,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests [TestCase("0")] [TestCase("15d")] + [TestCase("")] + [TestCase(null)] public void should_set_history_removes_completed_downloads_false(string historyRetention) { _config.Misc.history_retention = historyRetention; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index a1c856cfb..8beaa97a2 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -278,7 +278,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; } - if (config.Misc.history_retention.IsNotNullOrWhiteSpace() && config.Misc.history_retention.EndsWith("d")) + if (config.Misc.history_retention.IsNullOrWhiteSpace()) + { + status.RemovesCompletedDownloads = false; + } + else if (config.Misc.history_retention.EndsWith("d")) { int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1), out var daysRetention); From 07cbd7c8d29f2e0731f0a11c685f898f7e83b0c8 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 27 Jan 2024 06:59:43 +0100 Subject: [PATCH 071/762] Fixed: Validating DownloadStation output path Closes #6421 --- .../DownloadStation/TorrentDownloadStation.cs | 39 ++++++++---------- .../DownloadStation/UsenetDownloadStation.cs | 40 +++++++++---------- 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 8ecda831e..42efcaedf 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -310,40 +310,35 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { try { - var downloadDir = GetDefaultDir(); + var downloadDir = GetDownloadDirectory(); if (downloadDir == null) { - return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination") + return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestination")) { DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary<string, object> { { "username", Settings.Username } }) }; } - downloadDir = GetDownloadDirectory(); + var sharedFolder = downloadDir.Split('\\', '/')[0]; + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); - if (downloadDir != null) + var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); + + if (folderInfo.Additional == null) { - var sharedFolder = downloadDir.Split('\\', '/')[0]; - var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); - - var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); - - if (folderInfo.Additional == null) + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) { - return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) - { - DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } }) - }; - } + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } }) + }; + } - if (!folderInfo.IsDir) + if (!folderInfo.IsDir) + { + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) { - return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) - { - DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) - }; - } + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) + }; } return null; @@ -460,7 +455,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var destDir = GetDefaultDir(); - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (destDir.IsNotNullOrWhiteSpace() && Settings.TvCategory.IsNotNullOrWhiteSpace()) { return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 0571847e2..55b321cc3 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -211,40 +211,36 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { try { - var downloadDir = GetDefaultDir(); + var downloadDir = GetDownloadDirectory(); if (downloadDir == null) { - return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination") + return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestination")) { DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary<string, object> { { "username", Settings.Username } }) }; } - downloadDir = GetDownloadDirectory(); + var sharedFolder = downloadDir.Split('\\', '/')[0]; + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); - if (downloadDir != null) + var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); + + if (folderInfo.Additional == null) { - var sharedFolder = downloadDir.Split('\\', '/')[0]; - var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); - - var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); - - if (folderInfo.Additional == null) + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) { - return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) - { - DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } }) - }; - } + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", + new Dictionary<string, object> { { "sharedFolder", sharedFolder } }) + }; + } - if (!folderInfo.IsDir) + if (!folderInfo.IsDir) + { + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) { - return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) - { - DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) - }; - } + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) + }; } return null; @@ -439,7 +435,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var destDir = GetDefaultDir(); - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (destDir.IsNotNullOrWhiteSpace() && Settings.TvCategory.IsNotNullOrWhiteSpace()) { return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; } From d9acbf5682a10312583e3846b10b2a9fc83f95ee Mon Sep 17 00:00:00 2001 From: ta264 <ta264@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:01:18 +0100 Subject: [PATCH 072/762] Fixed: FolderWritable check for CIFS shares mounted in Unix This reverts commit 8c892a732ed57af9bb1f39743e0c16361f41b50f. (cherry picked from commit 96384521c59233dab5bd8289e7c84043f75b84a2) --- .../DiskTests/DiskProviderFixtureBase.cs | 10 ++++++++++ src/NzbDrone.Common/Disk/DiskProviderBase.cs | 13 +++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs index b1994d103..421ffbcf6 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs @@ -10,6 +10,16 @@ namespace NzbDrone.Common.Test.DiskTests public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider { + [Test] + public void writealltext_should_truncate_existing() + { + var file = GetTempFilePath(); + + Subject.WriteAllText(file, "A pretty long string"); + Subject.WriteAllText(file, "A short string"); + Subject.ReadAllText(file).Should().Be("A short string"); + } + [Test] [Retry(5)] public void directory_exist_should_be_able_to_find_existing_folder() diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 5d30c074f..9c206709f 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -131,7 +131,7 @@ namespace NzbDrone.Common.Disk { var testPath = Path.Combine(path, "sonarr_write_test.txt"); var testContent = $"This file was created to verify if '{path}' is writable. It should've been automatically deleted. Feel free to delete it."; - File.WriteAllText(testPath, testContent); + WriteAllText(testPath, testContent); File.Delete(testPath); return true; } @@ -311,7 +311,16 @@ namespace NzbDrone.Common.Disk { Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs); RemoveReadOnly(filename); - File.WriteAllText(filename, contents); + + // File.WriteAllText is broken on net core when writing to some CIFS mounts + // This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617 + using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + using (var writer = new StreamWriter(fs)) + { + writer.Write(contents); + } + } } public void FolderSetLastWriteTime(string path, DateTime dateTime) From e9f0c962494ef0aba9fc8f5aa4444b99eeba6601 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 26 Jan 2024 21:01:36 -0800 Subject: [PATCH 073/762] Fixed: Specials not allowing multi-episode select in Manual Import Closes #6429 --- .../Interactive/InteractiveImportModalContent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 071ec650c..b778388a5 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -298,14 +298,20 @@ function InteractiveImportModalContent( return acc; } + const lastSelectedSeason = acc.lastSelectedSeason; + acc.seasonSelectDisabled ||= !item.series; - acc.episodeSelectDisabled ||= !item.seasonNumber; + acc.episodeSelectDisabled ||= + item.seasonNumber === undefined || + (lastSelectedSeason >= 0 && item.seasonNumber !== lastSelectedSeason); + acc.lastSelectedSeason = item.seasonNumber ?? -1; return acc; }, { seasonSelectDisabled: false, episodeSelectDisabled: false, + lastSelectedSeason: -1, } ); From 350600607d5879bc2e2d3ba50903b96a203a4590 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 27 Jan 2024 05:56:59 +0000 Subject: [PATCH 074/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Alexander <a.burdun@gmail.com> Co-authored-by: Crocmou <slaanesh8854@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: diaverso <alexito_perez.95@hotmail.com> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: reloxx <reloxx@interia.pl> Co-authored-by: zichichi <sollami@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 5 +- src/NzbDrone.Core/Localization/Core/es.json | 163 +++++++++++- src/NzbDrone.Core/Localization/Core/fi.json | 241 ++++++++++++++++-- src/NzbDrone.Core/Localization/Core/fr.json | 22 +- src/NzbDrone.Core/Localization/Core/hu.json | 182 ++++++++++++- src/NzbDrone.Core/Localization/Core/it.json | 9 +- .../Localization/Core/pt_BR.json | 34 ++- src/NzbDrone.Core/Localization/Core/uk.json | 26 +- 8 files changed, 626 insertions(+), 56 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 8d1de1a3f..aa3316c93 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -552,7 +552,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.", @@ -791,5 +791,6 @@ "UpdateScriptPathHelpText": "Pfad zu einem benutzerdefinierten Skript, das ein extrahiertes Update-Paket übernimmt und den Rest des Update-Prozesses abwickelt", "Branch": "Branch", "Airs": "Wird ausgestrahlt", - "AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden" + "AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden", + "IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 36a65a99c..8455a5cf7 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -131,7 +131,7 @@ "AddNew": "Añadir Nuevo", "ApplyTagsHelpTextAdd": "Añadir: Añadir las etiquetas la lista existente de etiquetas", "ApplyTagsHelpTextRemove": "Eliminar: Eliminar las etiquetas introducidas", - "Blocklist": "Bloqueadas", + "Blocklist": "Lista de bloqueos", "Grabbed": "Añadido", "Genres": "Géneros", "Indexer": "Indexador", @@ -235,7 +235,7 @@ "CountImportListsSelected": "{count} lista(s) de importación seleccionada(s)", "DelayingDownloadUntil": "Retrasar la descarga hasta {date} a {time}", "DeleteIndexerMessageText": "Seguro que quieres eliminar el indexer '{name}'?", - "BlocklistLoadError": "No se han podido cargar las bloqueadas", + "BlocklistLoadError": "No se ha podido cargar la lista de bloqueos", "BypassDelayIfAboveCustomFormatScore": "Omitir si está por encima de la puntuación del formato personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para evitar el retraso del protocolo preferido", "DeleteDownloadClientMessageText": "Seguro que quieres eliminar el gestor de descargas '{name}'?", @@ -254,13 +254,13 @@ "DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?", "AddImportListImplementation": "Añadir lista de importación - {implementationName}", "AddIndexerImplementation": "Añadir Indexador - {implementationName}", - "AutoRedownloadFailed": "La descarga ha fallado", + "AutoRedownloadFailed": "Descarga fallida", "ConnectionLostReconnect": "Radarr intentará conectarse automáticamente, o haz clic en el botón de recarga abajo.", "CustomFormatJson": "Formato JSON personalizado", "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", "DeleteImportList": "Eliminar Lista(s) de Importación", "DeleteImportListMessageText": "Seguro que quieres eliminar la lista '{name}'?", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "La búsqueda automática para intentar descargar una versión diferente cuando en la búsqueda interactiva se obtiene una versión fallida", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Búsqueda automática e intento de descarga de una versión diferente cuando se obtiene una versión fallida de la búsqueda interactiva", "AutoRedownloadFailedFromInteractiveSearch": "Fallo al volver a descargar desde la búsqueda interactiva", "DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?", "DeleteSelectedImportListsMessageText": "Seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", @@ -268,7 +268,7 @@ "DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?", "DisabledForLocalAddresses": "Desactivado para direcciones locales", "DeletedReasonManual": "El archivo fue borrado por vía UI", - "ClearBlocklist": "Limpiar lista de bloqueadas", + "ClearBlocklist": "Limpiar lista de bloqueos", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", "MonitorPilotEpisode": "Episodio Piloto", "MonitorRecentEpisodesDescription": "Monitorizar episodios emitidos en los últimos 90 días y los episodios futuros", @@ -276,7 +276,7 @@ "MonitorSeries": "Monitorizar Series", "NoHistory": "Sin historial", "NoHistoryFound": "No se encontró historial", - "NoHistoryBlocklist": "Sin lista de bloqueo de historial", + "NoHistoryBlocklist": "Sin historial de la lista de bloqueos", "QueueIsEmpty": "La cola está vacía", "Quality": "Calidad", "RefreshAndScanTooltip": "Actualizar información y escanear disco", @@ -304,7 +304,7 @@ "MonitorNoNewSeasons": "Sin Nuevas Temporadas", "MonitorSpecialEpisodesDescription": "Monitorizar todos los episodios especiales sin cambiar el estado de monitorizado de otros episodios", "Calendar": "Calendario", - "BlocklistRelease": "Lista de lanzamientos bloqueados", + "BlocklistRelease": "Lista de bloqueos de lanzamiento", "CountSeasons": "{count} Temporadas", "BranchUpdate": "Rama a usar para actualizar {appName}", "ChmodFolder": "Carpeta chmod", @@ -391,10 +391,10 @@ "CancelProcessing": "Procesando cancelacion", "Category": "Categoria", "WhatsNew": "Que es lo nuevo?", - "BlocklistReleases": "Lista de lanzamientos bloqueados", + "BlocklistReleases": "Lista de bloqueos de lanzamientos", "BypassDelayIfHighestQuality": "Pasar sí es la calidad más alta", "ChownGroupHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga use el mismo grupo que {appName}.", - "ClearBlocklistMessageText": "¿Estás seguro de que quieres borrar todos los elementos de la lista de bloqueo?", + "ClearBlocklistMessageText": "¿Estás seguro de que quieres borrar todos los elementos de la lista de bloqueos?", "FormatAgeDay": "día", "FormatAgeDays": "días", "FormatAgeHour": "hora", @@ -562,8 +562,8 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "La versión debe ser menor o igual a este tamaño", "CustomFormatsSpecificationMinimumSize": "Tamaño mínimo", "CustomFormatsSpecificationMinimumSizeHelpText": "La versión debe ser mayor que este tamaño", - "CustomFormatsSpecificationRegularExpression": "Idioma", - "CustomFormatsSpecificationRegularExpressionHelpText": "El formato RegEx personalizado no distingue mayúsculas de minúsculas", + "CustomFormatsSpecificationRegularExpression": "Expresión regular", + "CustomFormatsSpecificationRegularExpressionHelpText": "El formato personalizado RegEx no distingue mayúsculas de minúsculas", "CustomFormatsSpecificationReleaseGroup": "Grupo de publicación", "CustomFormatsSpecificationResolution": "Resolución", "CustomFormatsSpecificationSource": "Fuente", @@ -738,5 +738,144 @@ "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", - "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador" + "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador", + "EnableRss": "Habilitar RSS", + "Ended": "Finalizado", + "EpisodeFileRenamed": "Archivo de episodio renombrado", + "EpisodeFileRenamedTooltip": "Archivo de episodio renombrado", + "EpisodeFileMissingTooltip": "Archivo de episodio faltante", + "EpisodeImported": "Episodio importado", + "EpisodeImportedTooltip": "Episodio descargado correctamente y recogido del cliente de descarga", + "EpisodeSearchResultsLoadError": "No se puede cargar resultado para la búsqueda de este episodio. Inténtelo de nuevo más tarde", + "ExistingSeries": "Series existentes", + "ExpandAll": "Expandir todo", + "FailedToLoadSeriesFromApi": "Error al cargar series desde la API", + "FailedToLoadSonarr": "Error al cargar {appName}", + "FilterContains": "contiene", + "FilterDoesNotContain": "no contiene", + "FilterNotInNext": "no en el próximo", + "FilterSeriesPlaceholder": "Filtrar series", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiples carpetas raíz faltan para importar listas: {rootFolderInfo}", + "RemoveFromBlocklist": "Eliminar de la lista de bloqueos", + "ErrorRestoringBackup": "Error restaurando la copia de seguridad", + "FileNameTokens": "Tokens de nombre de archivos", + "MonitorMissingEpisodes": "Episodios faltantes", + "BlocklistAndSearch": "Lista de bloqueos y búsqueda", + "BlocklistAndSearchHint": "Inicia la búsqueda de un sustituto tras añadir a la lista de bloqueos", + "FilterGreaterThanOrEqual": "menor o igual que", + "GrabRelease": "Capturar lanzamiento", + "EndedOnly": "Solo finalizado", + "EpisodeNumbers": "Número(s) de episodio(s)", + "EpisodeProgress": "Progreso del episodio", + "EpisodeTitle": "Título del episodio", + "ErrorLoadingContent": "Hubo un error cargando este contenido", + "General": "General", + "EnableSsl": "Habilitar SSL", + "EpisodeAirDate": "Fecha de emisión del episodio", + "EpisodeIsNotMonitored": "El episodio no está monitorizado", + "EpisodesLoadError": "No se puede cargar los episodios", + "File": "Archivo", + "HardlinkCopyFiles": "Enlace permanente/Copiar archivos", + "EpisodeDownloaded": "Episodio descargado", + "FileBrowser": "Explorador de archivos", + "FilterDoesNotStartWith": "no empieza con", + "FilterEndsWith": "termina en", + "FilterDoesNotEndWith": "no termina en", + "Fixed": "Arreglado", + "Global": "Global", + "Enabled": "Habilitado", + "EpisodeHistoryLoadError": "No se puede cargar el historial del episodio", + "EpisodeIsDownloading": "El episodio se está descargando", + "EpisodeHasNotAired": "El episodio no está en emisión", + "ErrorLoadingItem": "Hubo un error cargando este elemento", + "ErrorLoadingPage": "Hubo un error cargando esta página", + "ErrorLoadingContents": "Error cargando contenidos", + "Events": "Eventos", + "FailedToLoadCustomFiltersFromApi": "Error al cargar filtros personalizados desde la API", + "FailedToLoadTranslationsFromApi": "Error al cargar traducciones desde la API", + "ExtraFileExtensionsHelpTextsExamples": "Ejemplos: '.sub, .nfo' o 'sub,nfo'", + "FilterInLast": "en el último", + "FileBrowserPlaceholderText": "Empiece a escribir o seleccione una ruta debajo", + "FullSeason": "Temporada completa", + "Folders": "Carpetas", + "GeneralSettings": "Opciones generales", + "GeneralSettingsLoadError": "No se pueden cargar las opciones generales", + "Grab": "Capturar", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} no pudo determinar para qué serie y episodio era este lanzamiento. {appName} no pudo automáticamente importar este lanzamiento. ¿Te gustaría capturar '{title}'?", + "HasMissingSeason": "Tiene temporadas faltantes", + "ImportListSearchForMissingEpisodesHelpText": "Una vez se añada la serie a {appName}, buscar automáticamente episodios faltantes", + "ImportListsSonarrValidationInvalidUrl": "La URL de {appName} es inválida. ¿Te falta la URL base?", + "FeatureRequests": "Peticiones de características", + "FileNames": "Nombres de archivos", + "Filename": "Nombre de archivo", + "FilterEpisodesPlaceholder": "Filtrar episodios por título o número", + "FilterGreaterThan": "mayor que", + "FilterIs": "es", + "FilterLessThan": "menos que", + "FinaleTooltip": "Final de serie o de temporada", + "FirstDayOfWeek": "Primer día de la semana", + "Forums": "Foros", + "FreeSpace": "Espacio libre", + "MissingNoItems": "No hay elementos faltantes", + "ImportListRootFolderMissingRootHealthCheckMessage": "Carpeta raíz faltante para importar lista(s): {rootFolderInfo}", + "MissingLoadError": "Error cargando elementos faltantes", + "RemoveSelectedBlocklistMessageText": "¿Estás seguro que quieres eliminar los elementos seleccionados de la lista de bloqueos?", + "EventType": "Tipo de evento", + "EnableSslHelpText": "Requiere reiniciar la aplicación como administrador para que surta efecto", + "Existing": "Existentes", + "ExportCustomFormat": "Exportar formato personalizado", + "EpisodeFilesLoadError": "No se puede cargar los archivos de episodios", + "EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadCliente}", + "EpisodeInfo": "Información del episodio", + "EpisodeMissingAbsoluteNumber": "El episodio no tiene un número de episodio absoluto", + "EpisodeTitleRequired": "Título del episodio requerido", + "ExternalUpdater": "{appName} está configurado para usar un mecanismo de actualización externo", + "ExtraFileExtensionsHelpText": "Lista de archivos adicionales separados por coma para importar (.nfo será importado como .nfo-orig)", + "FailedToLoadSystemStatusFromApi": "Error al cargar el estado del sistema desde la API", + "FailedToLoadUiSettingsFromApi": "Error al cargar opciones de la interfaz de usuario desde la API", + "FileManagement": "Gestión de archivos", + "FilterLessThanOrEqual": "menos o igual que", + "FilterStartsWith": "empieza con", + "From": "desde", + "Missing": "Faltantes", + "ExistingTag": "Etiquetas existentes", + "BlocklistAndSearchMultipleHint": "Inicia búsquedas de sustitutos tras añadir a la lista de bloqueos", + "EpisodeMissingFromDisk": "Episodio faltante en el disco", + "EpisodeTitleRequiredHelpText": "Evita importarlo hasta 48 horas si el título del episodio se encuentra en el formato de nombrado y el título del episodio es TBA", + "FailedToLoadQualityProfilesFromApi": "Error al cargar perfiles de calidad desde la API", + "Exception": "Excepción", + "FailedToLoadTagsFromApi": "Error al cargar etiquetas desde la API", + "FilterIsAfter": "es después", + "FilterIsNot": "no es", + "FilterNotEqual": "no igual", + "ChangeCategoryHint": "Cambia la descarga a la 'categoría post-importación' desde el cliente de descarga", + "DoNotBlocklist": "No añadir a la lista de bloqueos", + "DoNotBlocklistHint": "Eliminar sin añadir a la lista de bloqueos", + "GrabSelected": "Capturar seleccionado", + "MissingEpisodes": "Episodios faltantes", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rechazar hashes de torrents en la lista de bloqueos durante la captura", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Si un torrent es bloqueado por hash puede no ser adecuadamente rechazado durante RSS o búsqueda por algunos indexadores, habilitar esto permitirá que el torrent sea rechazado una vez sea capturado, pero antes es enviado al cliente.", + "GeneralSettingsSummary": "Puerto, SSL, usuario/contraseña, proxy, analíticas y actualizaciones", + "GrabId": "Capturar ID", + "BlocklistReleaseHelpText": "Bloquea este lanzamiento de volver a ser descargado por {appName} vía RSS o búsqueda automática", + "ChangeCategory": "Cambiar categoría", + "ChangeCategoryMultipleHint": "Cambia las descargas a la 'categoría post-importación' desde el cliente de descarga", + "DatabaseMigration": "Migración de la base de datos", + "BlocklistOnlyHint": "Añadir a la lista de bloqueos sin buscar un sustituto", + "BlocklistMultipleOnlyHint": "Añadir a la lista de bloqueos sin buscar sustitutos", + "BlocklistOnly": "Solo añadir a la lista de bloqueos", + "EpisodeNaming": "Nombre del episodio", + "Files": "Archivos", + "Filter": "Filtro", + "FilterEqual": "igual", + "FilterInNext": "en el próximo", + "FilterIsBefore": "es antes", + "FilterNotInLast": "no en el último", + "Group": "Grupo", + "ImportListSearchForMissingEpisodes": "Buscar episodios faltantes", + "EnableProfileHelpText": "Señalar 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", + "EpisodeFileDeletedTooltip": "Archivo de episodio eliminado" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 01d426e89..b869fa617 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -123,7 +123,7 @@ "Imported": "Tuotu", "AddListError": "Virhe lisättäessä listaa. Yritä uudelleen.", "AddListExclusionError": "Virhe lisättäessä listapoikkeusta. Yritä uudelleen.", - "AddNotificationError": "Virhe lisättäessä ilmoitusta. Yritä uudelleen.", + "AddNotificationError": "Kytköksen lisäys epäonnistui. Yritä uudelleen.", "AddQualityProfile": "Lisää laatuprofiili", "AddNewRestriction": "Lisää uusi rajoitus", "All": "Kaikki", @@ -329,7 +329,7 @@ "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tomorrow": "Huomenna", "TestParsing": "Testaa jäsennystä", - "ToggleMonitoredSeriesUnmonitored ": "Valvonttu-tilan kytkentä ei ole mahdollista, kun sarjaa ei valvota", + "ToggleMonitoredSeriesUnmonitored ": "Valvontatilaa ei ole mahdollista muuttaa, jos sarjaa ei valvota.", "Trace": "Jäljitys", "TotalRecords": "Rivien kokonaismäärä: {totalRecords}", "TotalSpace": "Kokonaistila", @@ -383,7 +383,7 @@ "ApplyTagsHelpTextRemove": "- \"Poista\" tyhjentää syötetyt tunnisteet", "ApplyTagsHelpTextReplace": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi", "CustomFormatScore": "Mukautetun muodon pisteytys", - "SeriesMatchType": "Sarjan täsmäyksen tyyppi", + "SeriesMatchType": "Sarjan kohdistustyyppi", "RemotePathMappingLocalPathHelpText": "Polku, jonka kautta etäsijaintia tulee käyttää paikallisesti.", "SonarrTags": "{appName}in tunnisteet", "CalendarLoadError": "Virhe ladattaessa kalenteria", @@ -410,7 +410,7 @@ "DownloadClientDownloadStationValidationSharedFolderMissing": "Jaettua kansiota ei ole olemassa", "DownloadClientFreeboxSettingsAppId": "Sovelluksen ID", "DownloadClientFreeboxSettingsApiUrl": "API:n URL-osoite", - "DownloadClientFreeboxSettingsAppToken": "Sovelluksen tunniste", + "DownloadClientFreeboxSettingsAppToken": "Sovellustunniste", "DownloadClientFreeboxUnableToReachFreebox": "Freebox API:a ei tavoiteta. Tarkista \"Osoite\", \"Portti\" ja \"Käytä SSL-salausta\" -asetukset. (Virhe: {exceptionMessage})", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Lataa tiedostot järjestyksessä (qBittorrent 4.1.0+).", "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd develop -versio, oletettavasti versio 3.0.0 tai korkeampi.", @@ -492,7 +492,7 @@ "DeletedSeriesDescription": "Sarja on poistettu TheTVDB.comista.", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", "NotificationStatusSingleClientHealthCheckMessage": "Ilmoitukset eivät ole ongelmien vuoksi käytettävissä: {notificationNames}", - "NotificationsLoadError": "Virhe ladattaessa ilmoituksia", + "NotificationsLoadError": "Kytkösten lataus epäonnistui.", "Options": "Asetukset", "OptionalName": "Valinnainen nimi", "OverviewOptions": "Yleiskatsauksen asetukset", @@ -595,7 +595,7 @@ "EpisodeDownloaded": "Jakso on ladattu", "InteractiveImportNoQuality": "Jokaisen valitun tiedoston laatu on määritettävä.", "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä edellyttää vähintään NzbGet-versiota 16.0", - "NotificationStatusAllClientHealthCheckMessage": "Kaikki ilmoitukset eivät ole ongelmien vuoksi käytettävissä", + "NotificationStatusAllClientHealthCheckMessage": "Mikään ilmoituspavelu ei ole ongelmien vuoksi käytettävissä.", "DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent lataa metatietoja", "DownloadClientQbittorrentTorrentStateError": "qBittorrent ilmoittaa virheestä", @@ -1107,7 +1107,7 @@ "Repeat": "Toista", "InteractiveImport": "Manuaalituonti", "NotificationsKodiSettingAlwaysUpdate": "Päivitä aina", - "NotificationsKodiSettingAlwaysUpdateHelpText": "Päivitetäänkö kirjasto myös videotoiston aikana?", + "NotificationsKodiSettingAlwaysUpdateHelpText": "Määrittää päivitetäänkö kirjasto myös videotoiston aikana.", "Connection": "Yhteys", "Date": "Päiväys", "DeleteSpecification": "Poista määritys", @@ -1323,18 +1323,18 @@ "KeyboardShortcutsOpenModal": "Avaa tämä ruutu", "ManageImportLists": "Tuontilistojen hallinta", "ManageLists": "Listojen hallunta", - "MatchedToSeason": "Täsmätty kauteen", - "Min": "Vähimmäis", + "MatchedToSeason": "Kohdistettu kauteen", + "Min": "Alin", "MultiSeason": "Useita kausia", - "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth -salasana.", + "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth -todennuksen salasana.", "NotificationsNtfySettingsAccessTokenHelpText": "Valinnainen tunnistepohjainen todennus. Ensisijainen ennen käyttäjätunnusta ja salasanaa.", "NotificationsPlexSettingsAuthToken": "Todennustunniste", - "NotificationsPlexSettingsAuthenticateWithPlexTv": "Tunnistaud Plex.tv-palvelussa", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Plex.tv-tunnistautuminen", "Existing": "On jo olemassa", - "NotificationsTelegramSettingsChatId": "Keskustelu ID", + "NotificationsTelegramSettingsChatId": "Keskustelun ID", "NotificationsSlackSettingsUsernameHelpText": "Slack-julkaisulle käytettävä käyttäjätunnus", "NotificationsTelegramSettingsSendSilently": "Lähetä äänettömästi", - "NotificationsTelegramSettingsTopicId": "Aiheen ID", + "NotificationsTelegramSettingsTopicId": "Ketjun ID", "ProcessingFolders": "Käsittelykansiot", "Preferred": "Haluttu", "SslCertPasswordHelpText": "Pfx-tiedoston salasana", @@ -1386,12 +1386,12 @@ "UsenetDelayHelpText": "Minuuttiviive, joka odotetaan ennen julkaisun Usenet-kaappausta.", "EpisodeFileMissingTooltip": "Jaksotiedosto puuttuu", "EpisodeNaming": "Jaksojen nimeäminen", - "NotificationsAppriseSettingsUsernameHelpText": "HTTP Basic Auth -käyttäjätunnus", - "NotificationsDiscordSettingsUsernameHelpText": "Julkaisuun käytettävä käyttäjätunnus. Oletusarvo on Discordin webhook-oletus.", - "NotificationsNtfySettingsPasswordHelpText": "Valinnainen salasana", - "NotificationsNtfySettingsUsernameHelpText": "Valinnainen käyttäjätunnus", + "NotificationsAppriseSettingsUsernameHelpText": "HTTP Basic Auth -todennuksen käyttäjätunnus.", + "NotificationsDiscordSettingsUsernameHelpText": "Julkaisussa käytettävä käyttäjätunnus. Oletusarvo on Discordin webhook-oletus.", + "NotificationsNtfySettingsPasswordHelpText": "Valinnainen salasana.", + "NotificationsNtfySettingsUsernameHelpText": "Valinnainen käyttäjätunnus.", "NotificationsPlexValidationNoTvLibraryFound": "Ainakin yksi televisiokirjasto tarvitaan.", - "NotificationsTelegramSettingsBotToken": "Bottitunniste", + "NotificationsTelegramSettingsBotToken": "Botin tunniste", "EnableSsl": "Käytä RSS-yhteyttä", "NotificationsValidationInvalidUsernamePassword": "Virheellinen käyttäjätunnus tai salasana", "QueueFilterHasNoItems": "Mikään kohde ei vastaa valittua jonon suodatinta", @@ -1402,7 +1402,7 @@ "EpisodeFileDeleted": "Jaksotiedosto poistettiin", "Folder": "Kansio", "Links": "Linkit", - "Max": "Enimmäis", + "Max": "Korkein", "MaximumLimits": "Enimmäisrajoitukset", "MinimumLimits": "Vähimmäisrajoitukset", "NoDelay": "Ei viivettä", @@ -1467,5 +1467,206 @@ "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai Trakt-listoilta, ja listapoikkeusten hallinta.", "LongDateFormat": "Pitkän päiväyksen esitys", "UnknownEventTooltip": "Tuntematon tapahtuma", - "UnknownDownloadState": "Tuntematon lataustila: {state}" + "UnknownDownloadState": "Tuntematon lataustila: {state}", + "ImportScriptPath": "Tuontikomentosarjan sijainti", + "NotificationsAppriseSettingsTags": "Apprisen tunnisteet", + "NotificationsAppriseSettingsServerUrlHelpText": "Apprise-palvelimen URL-osoite. SIsällytä myös http(s):// ja portti (tarvittaessa).", + "DownloadClientSettingsUseSslHelpText": "Yhdistä työkaluun \"{clientName}\" SSL-protokollan välityksellä.", + "NotificationsKodiSettingsCleanLibraryHelpText": "Siivoa kirjasto päivityksen jälkeen.", + "NotificationsJoinSettingsDeviceNamesHelpText": "Pilkuin eroteltu listaus täydellisistä tai osittaisista laitenimistä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", + "FormatDateTime": "{formattedDate} {formattedTime}", + "NotificationsCustomScriptSettingsArguments": "Argumentit", + "NotificationsCustomScriptSettingsName": "Oma komentosarja", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Komentosarjalle välitettävät argumentit.", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Määritä kaappausilmoituksissa välitettäviä tietueet.", + "NotificationsDiscordSettingsOnImportFields": "Tuonti-ilmoitusten tietueet", + "NotificationsEmailSettingsName": "Sähköposti", + "NotificationsDiscordSettingsOnManualInteractionFields": "Toimenpidetarveilmoitusten tietueet", + "NotificationsKodiSettingsCleanLibrary": "Siivoa kirjasto", + "NotificationsKodiSettingsGuiNotification": "Ilmoita käyttöliittymässä", + "NotificationsKodiSettingsDisplayTimeHelpText": "Määrittää ilmoituksen näyttöajan sekunteina.", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Käytä MailGunin EU-päätepistettä.", + "NotificationsNtfySettingsClickUrl": "Painalluksen URL-osoite", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Käyttäjätililtäsi löytyvä API-avain.", + "NotificationsNtfySettingsClickUrlHelpText": "Valinnainen URL-osoite, joka ilmoitusta painettaessa avataan.", + "NotificationsNtfySettingsTopics": "Topikit", + "NotificationsPushcutSettingsApiKeyHelpText": "API-avaimia voidaan hallita Puscut-sovelluksen tiliosiossa.", + "NotificationsPushoverSettingsSoundHelpText": "Ilmoituksen ääni. Käytä oletusta jättämällä tyhjäksi.", + "NotificationsSettingsWebhookMethodHelpText": "Lähetyksessä käytettävä HTTP-menetelmä.", + "NotificationsSimplepushSettingsEvent": "Tapahtuma", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Lähettää viestin äänettömästi, jolloin vastanottaja saa ilmoituksen ilman ääntä.", + "Rejections": "Hylkäykset", + "NoImportListsFound": "Tuotilistoja ei löytynyt", + "OnManualInteractionRequired": "Kun tarvitaan manuaalisia toimenpiteitä", + "RetryingDownloadOn": "Latausta yritetään uudelleen {date} klo {time}.", + "BlocklistAndSearch": "Estolista ja haku", + "BlocklistAndSearchHint": "Etsi korvaavaa kohdetta kun kohde lisätään estolistalle.", + "BlocklistOnlyHint": "Lisää estolistalle etsimättä korvaavaa kohdetta.", + "BlocklistReleaseHelpText": "Estää {appName}ia lataamasta tätä julkaisua uudelleen RSS-syötteen tai automaattihaun tulokisista.", + "ChangeCategory": "Vaihda kategoria", + "NotificationsPushoverSettingsDevicesHelpText": "Laitenimet, joihin ilmoitukset lähetetään (lähetä kaikkiin jättämällä tyhjäksi).", + "NotificationsNtfySettingsTagsEmojisHelpText": "Valinnainen pilkuin eroteltu listaus käytettävistä tunnisteista tai emjeista.", + "NotificationsSlackSettingsChannelHelpText": "Korvaa saapuvan webhook-viestin oletuskanavan (#other-channel).", + "IgnoreDownload": "Ohita lataus", + "IgnoreDownloadHint": "Estää {appName}ia käsittelemästä tätä latausta jatkossa.", + "NotificationsAppriseSettingsStatelessUrls": "Apprisen tilaton URL-osoite", + "NotificationsDiscordSettingsAvatarHelpText": "Muuta ilmoituksissa käytettävää käyttäjäkuvaketta.", + "NotificationsDiscordSettingsAvatar": "Käyttäjäkuvake", + "NotificationsEmailSettingsCcAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään kopioina.", + "NotificationsEmailSettingsFromAddress": "Lähetysosoite", + "NotificationsEmailSettingsServer": "Palvelin", + "NotificationsEmailSettingsServerHelpText": "Sähköpostipalvelimen IP-osoite tai isäntänimi.", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö palvelimen kirjasto tuonnin, uudelleennimeämisen tai poiston yhteydessä.", + "NotificationsGotifySettingIncludeSeriesPoster": "Sisällytä sarjan juliste", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Sisällytä sarjan juliste ilmoitukseen.", + "NotificationsGotifySettingsPriorityHelpText": "Ilmoituksen painotus.", + "NotificationsJoinSettingsDeviceIds": "Laite-ID:t", + "NotificationsJoinSettingsApiKeyHelpText": "Join-tilisi asetuksista löytyvä API-avain (paina Join API -painiketta).", + "NotificationsJoinSettingsDeviceNames": "Laitenimet", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö Kodin kirjasto tuonnin tai uudelleennimeämisen yhteydessä.", + "NotificationsMailgunSettingsApiKeyHelpText": "MailGunissa luotu API-avain.", + "NotificationsMailgunSettingsUseEuEndpoint": "Käytä EU-päätepistettä", + "NotificationsNtfySettingsAccessToken": "Käyttötunniste", + "NotificationsNtfySettingsServerUrl": "Palvelimen URL-osoite", + "NotificationsNtfySettingsTagsEmojis": "Ntfy-tunnisteet ja -emojit", + "NotificationsPushcutSettingsNotificationName": "Ilmoituksen nimi", + "NotificationsPushoverSettingsRetryHelpText": "Hätäilmoituksen uudelleenyritysten välinen aika.", + "NotificationsPushoverSettingsRetry": "Uudelleenyritys", + "NotificationsSettingsUpdateLibrary": "Päivitä kirjasto", + "NotificationsSettingsUpdateMapPathsFrom": "Sijaintien lähdekartoitukset", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Vastaanottavan ryhmän ID tai vastaanottajan puhelinnumero.", + "NotificationsSignalValidationSslRequired": "Näyttää siltä, että SSL-yhteys vaaditaan", + "NotificationsSignalSettingsUsernameHelpText": "Käyttäjätunnus, jolla Signal-API:lle lähetettävät pyynnöt todennetaan.", + "NotificationsTelegramSettingsTopicIdHelpText": "Lähetä ilmoitus tiettyyn ketjuun määrittämällä ketjun ID. Käytä yleistä aihetta jättämällä tyhjäksi (vain superryhmille).", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Trakt-tunnistautuminen.", + "NotificationsTwitterSettingsAccessToken": "Käyttötunniste", + "NotificationsTwitterSettingsAccessTokenSecret": "Käyttötunniste-salaisuus", + "NotificationsTwitterSettingsDirectMessage": "Suora viesti", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Kuluttajan salaisuus (consumer secret) X (Twitter) -sovelluksesta.", + "NotificationsTwitterSettingsDirectMessageHelpText": "Lähetä julkisen viestin sijaan suora viesti.", + "NotificationsTwitterSettingsMention": "Maininta", + "NotificationsValidationInvalidApiKeyExceptionMessage": "API-avain on virheellinen: {exceptionMessage}", + "NotificationsValidationInvalidApiKey": "API-avain on virheellinen", + "NotificationsValidationUnableToConnectToService": "Palvelua {serviceName} ei tavoiteta.", + "NotificationsValidationUnableToConnectToApi": "Palvelun {service} rajapintaa ei tavoiteta. Palvelinyhteys epäonnistui: ({responseCode}) {exceptionMessage}.", + "ReleaseHash": "Julkaisun hajatusarvo", + "False": "Epätosi", + "CustomFormatsSpecificationRegularExpressionHelpText": "Mukautetun muodon säännöllisen lausekkeen kirjainkokoa ei huomioida.", + "CustomFormatsSpecificationRegularExpression": "Säännöllinen lauseke", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API -määritystä tehtäessä saatu sovellustunniste (i.e. \"app_token\").", + "ImportListsPlexSettingsAuthenticateWithPlex": "Plex.tv-tunnistautuminen", + "ImportListsSettingsAccessToken": "Käyttötunniste", + "ManageClients": "Hallitse työkaluja", + "NotificationsAppriseSettingsConfigurationKey": "Apprise-määritysavain", + "NotificationTriggers": "Laukaisimet", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Pysyvää tallennustilaa käyttävän ratkaisun määritysavain. Jätä tyjäksi, jos käytetään tilatonta URL-osoitetta.", + "NotificationsAppriseSettingsServerUrl": "Apprise-palvelimen URL-osoite", + "NotificationsAppriseSettingsNotificationType": "Apprisen ilmoitustyyppi", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Yksi tai useita pilkuin eroteltuja URL-osoitteita ilmoitusten kohdistamiseen. Jätä tyhjäksi, jos käytetään pysyvää tallennustilaa.", + "NotificationsAppriseSettingsTagsHelpText": "Ilmoita vain vastaavalla tavalla merkityille kohteille.", + "NotificationsCustomScriptValidationFileDoesNotExist": "Teidostoa ei löydy", + "NotificationsCustomScriptSettingsProviderMessage": "Testaus suorittaa komentosarjan EventType-arvolla \"{eventTypeTest}\". Varmista, että komentosarjasi käsittelee tämän oikein.", + "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord-kanavan webhook-viestinnän URL-osoite.", + "NotificationsDiscordSettingsAuthor": "Julkaisija", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Määritä tuonti-ilmoituksissa välitettävät tietueet.", + "NotificationsEmailSettingsBccAddress": "Piilokopio-osoitteet", + "NotificationsEmailSettingsCcAddress": "Kopio-osoitteet", + "NotificationsEmailSettingsBccAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään piilokopioina.", + "NotificationsGotifySettingsServerHelpText": "Gotify-palvelimen URL-osoite. Sisällytä http(s):// ja portti (tarvittaessa).", + "NotificationsGotifySettingsServer": "Gotify-palvelin", + "NotificationsGotifySettingsAppToken": "Sovellustunniste", + "NotificationsEmailSettingsRecipientAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään.", + "NotificationsJoinSettingsNotificationPriority": "Ilmoituksen painotus", + "NotificationsJoinValidationInvalidDeviceId": "Laite-ID:issä näyttäisi olevan virheitä.", + "NotificationsNtfySettingsServerUrlHelpText": "Käytä julkista palvelinta jättämällä tyhjäksi ({url}).", + "NotificationsNtfySettingsTopicsHelpText": "Pilkuin eroteltu listaus topikeista, joihin ilmoitukset lähetetään.", + "NotificationsNtfyValidationAuthorizationRequired": "Tunnistautuminen vaaditaan", + "NotificationsPushBulletSettingSenderId": "Lähettäjän ID", + "NotificationsPushBulletSettingSenderIdHelpText": "Lähettävän laitteen ID. Käytä laitteen pushbullet.com-URL-osoitteen \"device_iden\"-arvoa (lähetä itseltäsi jättämällä tyhjäksi).", + "NotificationsPushBulletSettingsAccessToken": "Käyttötunniste", + "NotificationsPushBulletSettingsChannelTags": "Kanavatunnisteet", + "NotificationsPushcutSettingsTimeSensitive": "Kiireellinen", + "NotificationsPushoverSettingsDevices": "Laitteet", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Merkitsee ilmoituksen kiireelliseksi (\"Time Sensitive\").", + "NotificationsPushcutSettingsNotificationNameHelpText": "Ilmoituksen nimi Pushcut-sovelluksen ilmoitusvälilehdeltä.", + "NotificationsPushoverSettingsSound": "Ääni", + "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", + "NotificationsPushoverSettingsExpire": "Erääntyminen", + "NotificationsSendGridSettingsApiKeyHelpText": "SendGridin luoma API-avain.", + "NotificationsSimplepushSettingsEventHelpText": "Mukauta push-ilmoitusten toimintaa.", + "NotificationsSignalSettingsSenderNumber": "Lähettäjän numero", + "NotificationsSettingsWebhookMethod": "HTTP-menetelmä", + "NotificationsSignalSettingsPasswordHelpText": "Salasana, jolla Signal-API:lle lähetettävät pyynnöt todennetaan.", + "NotificationsTraktSettingsExpires": "Erääntyy", + "NotificationsSynologyValidationInvalidOs": "On oltava Synology", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Kehota paikallista localhost-synoindexiä päivittämääin kirjasto.", + "NotificationsTwitterSettingsMentionHelpText": "Mainitse tämä käyttäjä lähetettävissä twiiteissä.", + "OnApplicationUpdate": "Kun sovellus päivitetään", + "NotificationsTwitterSettingsConsumerSecret": "Kuluttajan salaisuus", + "NotificationsTwitterSettingsConnectToTwitter": "Muodosta X (Twitter) -yhteys", + "ParseModalUnableToParse": "Annetun nimikkeen jäsennys ei onnistunut. Yritä uudelleen.", + "True": "Tosi", + "Or": "tai", + "UpdateFiltered": "Päivitä suodatetut", + "DownloadClientPriorityHelpText": "Lautaustyökalujen painotus, 1– 50 (korkein-alin). Oletusarvo on 1 ja tasaveroiset erotetaan Round-Robin-tekniikalla.", + "NotificationsEmbySettingsSendNotificationsHelpText": "Ohjeista palvelinta välittämään ilmoitukset sen määritettyihin kohteisiin.", + "NotificationsEmbySettingsSendNotifications": "Lähetä ilmoitukset", + "NotificationsDiscordSettingsOnGrabFields": "Kaappausilmoitusten tietueet", + "NotificationsGotifySettingsAppTokenHelpText": "Gotifyn luoma sovellustunniste.", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Määritä toimenpidetarveilmoituksissa välitettäviä tietueet.", + "NotificationsEmailSettingsRecipientAddress": "Vastaanottajien osoitteet", + "NotificationsJoinSettingsDeviceIdsHelpText": "Vanhentunut, käytä tämän sijaan laitenimiä. Pilkuin eroteltu listaus laite-ID:istä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", + "NotificationsPushBulletSettingsDeviceIds": "Laite-ID:t", + "NotificationsKodiSettingsDisplayTime": "Näytä aika", + "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", + "NotificationsSettingsUseSslHelpText": "Muodosta yhteys 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": "Sijaintien kohdekartoitukset", + "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", + "NotificationsTraktSettingsAccessToken": "Käyttötunniste", + "NotificationsTraktSettingsAuthUser": "Todennettu käyttäjä", + "NotificationsValidationUnableToSendTestMessage": "Testiviestin lähetys ei onnistu: {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Testiviestin lähetys ei onnistu. API vastasi: {error}", + "NotificationsEmailSettingsUseEncryption": "Käytä salausta", + "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimikkeen ja näyttämään sen tiedot.", + "ImportScriptPathHelpText": "Tuonnissa suoirtettavan komentosarjan sijainti.", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Kuluttajan avain (consumer key) X (Twitter) -sovelluksesta.", + "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", + "RemoveMultipleFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", + "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista lataustyökalusta\" poistaa latauksen ja sen tiedostot.", + "UnableToLoadAutoTagging": "Automaattimerkinnän lataus epäonnistui", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein etsittäessä joiltakin tietolähteiltä RSS-syötteen tai haun välityksellä. Tämä mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, mutta ennen välitystä lataustyökalulle.", + "NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä", + "NotificationsSlackSettingsIconHelpText": "Muuta Slack-julkaisuissa käytettävää kuvaketta (emoji tai URL-osoite).", + "NotificationsTwitterSettingsConsumerKey": "Kuluttajan avain", + "Parse": "Jäsennä", + "SkipRedownloadHelpText": "Estää {appName}ia lataamasta kohteelle vaihtoehtoista julkaisua.", + "BlocklistAndSearchMultipleHint": "Etsi korvaavia kohteita kun kohteita lisätään estolistalle.", + "BlocklistMultipleOnlyHint": "Lisää estolistalle etsimättä korvaavia kohteita.", + "BlocklistOnly": "Vain esto", + "DoNotBlocklist": "Älä estä", + "DoNotBlocklistHint": "Poista lisäämättä estolistalle.", + "IgnoreDownloads": "Ohita lataukset", + "IgnoreDownloadsHint": "Estää {appName}ia käsittelemästä näitä latauksia jatkossa.", + "NotificationsMailgunSettingsSenderDomain": "Lähettäjän verkkotunnus", + "NotificationsSignalSettingsSenderNumberHelpText": "Signal-API:n lähettäjärekisterin puhelinnumero.", + "NotificationsValidationInvalidAccessToken": "Käyttötunniste on virheellinen.", + "NotificationsSlackSettingsWebhookUrlHelpText": "Slack-kanavan webhook-viestinnän URL-osoite.", + "NotificationsDiscordSettingsAuthorHelpText": "Korvaa ilmoitukselle näytettävä upotettu julkaisijatieto. Jos tyhjä, käytetään instanssin nimeä.", + "NotificationsSlackSettingsChannel": "Kanava", + "NotificationsSlackSettingsIcon": "Kuvake", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Pilkuin eroteltu listaus kanavatunnisteista, joille ilmoitukset lähetetään.", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Pilkuin eroteltu listaus laite-ID:istä, joihin ilmoitukset lähetetään (lähetä kaikille jättämällä tyhjäksi).", + "NotificationsSignalSettingsGroupIdPhoneNumber": "Ryhmän iD/puhelinnumero", + "NotificationsPushoverSettingsUserKey": "Käyttäjäavain", + "NotificationsPushoverSettingsExpireHelpText": "Hätäilmoitusten uudelleenyrityksen enimmäisaika (enimmäisarvo on 86400 sekuntia).", + "NotificationsSimplepushSettingsKey": "Avain", + "NotificationsValidationInvalidHttpCredentials": "HTTP Auth -tunnistetiedot eivät kelpaa: {exceptionMessage}", + "NotificationsValidationUnableToConnect": "Yhteydenmuodostus ei onnistu: {exceptionMessage}", + "NotificationsValidationInvalidAuthenticationToken": "Todennustunniste on virheellinen", + "RemoveFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", + "RemoveQueueItemRemovalMethod": "Poistotapa", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "\"Poista lataustyökalusta\" poistaa lataukset ja niiden tiedostot.", + "Umask": "Umask" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 1ecb4ddcf..bd6b257cf 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -67,7 +67,7 @@ "BeforeUpdate": "Avant la mise à jour", "CancelPendingTask": "Êtes-vous sur de vouloir annuler cette tâche en attente ?", "Clear": "Effacer", - "AddAutoTagError": "Impossible d'ajouter un tag automatique, réessayer.", + "AddAutoTagError": "Impossible d'ajouter un nouveau tag automatique, veuillez réessayer.", "AddConditionError": "Impossible d'ajouter une nouvelle condition, Réessayer.", "AddCondition": "Ajouter une condition", "AddAutoTag": "Ajouter un tag automatique", @@ -1431,9 +1431,9 @@ "EpisodeDownloaded": "Épisode téléchargé", "CutoffUnmetLoadError": "Erreur lors du chargement des éléments non satisfaits", "CountSelectedFile": "{selectedCount} fichier sélectionné", - "AutoRedownloadFailed": "Retélécharger les fichiers ayant échoué", - "AutoRedownloadFailedFromInteractiveSearch": "Retélécharger les fichiers ayant échoué depuis la recherche interactive", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Rechercher et tenter automatiquement de télécharger une version différente lorsque la version ayant échoué a été récupérée à partir de la recherche interactive", + "AutoRedownloadFailed": "Échec du retéléchargement", + "AutoRedownloadFailedFromInteractiveSearch": "Échec du retéléchargement à partir de la recherche interactive", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Lance une recherche automatique et une tentative de téléchargement d'une version différente si la version trouvée automatiquement échoue", "ConditionUsingRegularExpressions": "Cette condition correspond à l'aide d'expressions régulières. Notez que les caractères `\\^$.|?*+()[{` ont des significations particulières et doivent être échappés par un `\\`", "CutoffUnmet": "Seuil non atteint", "CutoffUnmetNoItems": "Aucun élément non satisfait", @@ -1879,7 +1879,7 @@ "CustomFormatsSpecificationLanguage": "Langue", "CustomFormatsSpecificationMaximumSize": "Taille maximum", "CustomFormatsSpecificationMinimumSize": "Taille maximum", - "CustomFormatsSpecificationRegularExpression": "Langue", + "CustomFormatsSpecificationRegularExpression": "Expression régulière", "CustomFormatsSpecificationReleaseGroup": "Groupe de versions", "CustomFormatsSpecificationResolution": "Résolution", "CustomFormatsSpecificationSource": "Source", @@ -1897,5 +1897,15 @@ "ImportListsSettingsRefreshToken": "Jeton d'actualisation", "ImportListsSimklSettingsAuthenticatewithSimkl": "Se connecter 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é." + "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", + "BlocklistAndSearch": "Liste de blocage et recherche", + "BlocklistAndSearchHint": "Lancer la recherche d'un remplaçant après l'inscription sur la liste de blocage", + "ChangeCategoryHint": "Modifie le téléchargement dans la \"catégorie post-importation\" du client de téléchargement", + "BlocklistAndSearchMultipleHint": "Lancer la recherche de remplaçants après l'inscription sur la liste de blocage", + "BlocklistMultipleOnlyHint": "Liste de blocage sans recherche de remplaçants", + "BlocklistOnly": "Liste de blocage uniquement", + "BlocklistOnlyHint": "Liste de blocage sans recherche de remplaçant", + "ChangeCategory": "Changer de catégorie", + "ChangeCategoryMultipleHint": "Modifie les téléchargements dans la \"catégorie post-importation\" du client de téléchargement" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 92bfab9e6..31c90a5b6 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -4,7 +4,7 @@ "Close": "Bezárás", "Delete": "Törlés", "DeleteCondition": "Feltétel törlése", - "DeleteConditionMessageText": "Biztosan törölni akarod a '{name}' feltételt?", + "DeleteConditionMessageText": "Biztosan törli a(z) „{name}” feltételt?", "DeleteCustomFormat": "Egyéni formátum törlése", "DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{customFormatName}' egyéni formátumot?", "ExportCustomFormat": "Egyéni formátum exportálása", @@ -26,8 +26,8 @@ "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", "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltési kliens elérhetetlen meghibásodások miatt", - "EditSelectedDownloadClients": "Kiválasztott letöltési kliensek szerkesztése", - "EditSelectedImportLists": "Kiválasztott import listák szerkesztése", + "EditSelectedDownloadClients": "Kijelölt letöltési kliensek szerkesztése", + "EditSelectedImportLists": "Kijelölt importálási listák szerkesztése", "EditSelectedIndexers": "Kiválasztott indexelők szerkesztése", "DownloadClientStatusSingleClientHealthCheckMessage": "Letöltési kliensek elérhetetlenek meghibásodások miatt: {downloadClientNames}", "EnableAutomaticSearch": "Automatikus keresés engedélyezése", @@ -69,7 +69,7 @@ "PreviousAiring": "Előző rész", "RecycleBinUnableToWriteHealthCheckMessage": "Nem lehet írni a konfigurált lomtár mappába {path}. Győződjön meg arról, hogy ez az elérési útvonal létezik, és az a felhasználó, aki a {appName}-t futtatja, írási jogosultsággal rendelkezik", "QualityProfile": "Minőségi profil", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker-t használ; a(z) $1{downloadClientName} letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik a konténeren belül. Ellenőrizze a távoli útvonal hozzárendeléseket, és a konténer kötet beállításait.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker-t használ; a(z) {downloadClientName} letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik a konténeren belül. Ellenőrizze a távoli útvonal hozzárendeléseket, és a konténer kötet beállításait.", "RefreshSeries": "Sorozat frissítése", "RemotePathMappingFileRemovedHealthCheckMessage": "A(z) {path} fájlt részben feldolgozás közben eltávolították.", "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "A {appName} látja, de nem tud hozzáférni a letöltött epizódhoz {path}. Valószínűleg jogosultsági hiba.", @@ -311,13 +311,13 @@ "CalendarLegendEpisodeDownloadedTooltip": "Az epizódot letöltötték és rendezték", "CalendarLegendEpisodeUnmonitoredTooltip": "Az epizód nem figyelhető", "Forums": "Fórumok", - "Release": "kiadás", + "Release": "Kiadás", "Special": "Különleges", "Version": "Verzió", "AutoRedownloadFailed": "Az újraletöltés nem sikerült", "AutoRedownloadFailedFromInteractiveSearch": "Az újraletöltés nem sikerült az interaktív keresésből", "Name": "Név", - "Real": "Igazi", + "Real": "Igaz", "Theme": "Téma", "TestAll": "Összes Tesztelése", "Week": "Hét", @@ -447,7 +447,7 @@ "AuthenticationRequiredHelpText": "Módosítsa, hogy mely kérésekhez van szükség hitelesítésre. Ne változtasson, hacsak nem érti a kockázatokat.", "BranchUpdateMechanism": "Külső frissítési mechanizmus által használt ág", "Duplicate": "Duplikált", - "Reload": "Újratöltés", + "Reload": "Újratölt", "Qualities": "Minőségek", "Reset": "Visszaállítás", "CollapseAll": "Mindet összecsuk", @@ -806,5 +806,171 @@ "MonitorMissingEpisodesDescription": "Figyelje meg azokat az epizódokat, amelyekhez nem tartoznak fájlok, vagy amelyeket még nem adtak le", "MonitorNewItems": "Új elemek figyelése", "NoLimitForAnyRuntime": "Nincs korlátozás semmilyen futási időre", - "NotificationStatusAllClientHealthCheckMessage": "Az összes értesítés nem érhető el hibák miatt" + "NotificationStatusAllClientHealthCheckMessage": "Az összes értesítés nem érhető el hibák miatt", + "SelectSeason": "Évad kiválasztása", + "QuickSearch": "Gyors keresés", + "QueueLoadError": "Nem sikerült betölteni a várakozási sort", + "RecyclingBinCleanup": "Lomtár tisztítása", + "RecyclingBin": "Lomtár", + "SeriesTitleToExcludeHelpText": "A kizárandó sorozat neve", + "SeriesMonitoring": "Sorozatfigyelés", + "ShowQualityProfile": "Minőségi Profil mutatása", + "CompletedDownloadHandling": "Befejezett letöltéskezelés", + "ConnectSettingsSummary": "Értesítések, csatlakozások médiaszerverekhez", + "CouldNotFindResults": "Nem található találat a következőre: „{term}”", + "DeletedReasonManual": "A fájlt a felhasználói felületen keresztül törölte", + "DeletedReasonUpgrade": "A fájl törlésre került a frissítés importálásához", + "DisabledForLocalAddresses": "Helyi címeknél letiltva", + "ReadTheWikiForMoreInformation": "További információkért olvassa el a Wikit", + "ReleaseProfileIndexerHelpText": "Adja meg, hogy a profil melyik indexelőre vonatkozik", + "RemotePathMappings": "Távoli útvonal-leképezések", + "SkipFreeSpaceCheck": "Kihagyja a szabad hely ellenőrzését", + "SkipRedownload": "Az újraletöltés kihagyása", + "SmartReplace": "Intelligens csere", + "BlocklistAndSearch": "Feketelista és Keresés", + "BlocklistAndSearchHint": "Indítsa el a csere keresését a tiltólistázás után", + "ChangeCategoryHint": "A módosítások letöltése az „Importálás utáni kategóriára” a Download Clientből", + "SelectLanguages": "Nyelvek kiválasztása", + "SendAnonymousUsageData": "Névtelen használati adatok küldése", + "ShowBannersHelpText": "Címek helyett szalaghirdetések megjelenítése", + "ShowUnknownSeriesItems": "Ismeretlen sorozatelemek megjelenítése", + "EnableProfile": "Profil engedélyezése", + "ShowQualityProfileHelpText": "Minőségi profil megjelenítése a plakát alatt", + "ShowRelativeDates": "Relatív dátumok megjelenítése", + "RecyclingBinCleanupHelpText": "Állítsa 0-ra az automatikus tisztítás letiltásához", + "SmartReplaceHint": "Dash vagy Space Dash névtől függően", + "RefreshAndScan": "Frissítés és Keresés", + "RefreshAndScanTooltip": "Frissítse az információkat és ellenőrizze a lemezt", + "RegularExpression": "Reguláris kifejezés", + "Rejections": "Elutasítások", + "SeriesCannotBeFound": "Sajnos ez a sorozat nem található.", + "SeriesFinale": "Sorozat finálé", + "SeriesFolderImportedTooltip": "Az epizód a sorozat mappájából importálva", + "SeriesID": "Sorozat ID", + "SeriesIndexFooterMissingUnmonitored": "Hiányzó epizódok (a sorozatot nem figyelik)", + "ShowPreviousAiring": "Előző adás megjelenítése", + "ShowPath": "Útvonal megjelenítése", + "SingleEpisodeInvalidFormat": "Egyetlen epizód: Érvénytelen formátum", + "SonarrTags": "{appName} Címkék", + "SomeResultsAreHiddenByTheAppliedFilter": "Néhány eredményt elrejtett az alkalmazott szűrő", + "DeleteAutoTagHelpText": "Biztosan törli a(z) „{name}” automatikus címkét?", + "RecyclingBinHelpText": "A fájlok törléskor ide kerülnek a végleges törlés helyett", + "ApplyTagsHelpTextReplace": "Csere: Cserélje ki a címkéket a megadott címkékkel (az összes címke törléséhez ne írjon be címkéket)", + "RegularExpressionsCanBeTested": "A reguláris kifejezések [here] tesztelhetők (http://regexstorm.net/tester).", + "SelectLanguage": "Válasszon nyelvet", + "SeriesEditor": "Sorozat szerkesztő", + "SeriesFolderFormat": "Sorozat mappa formátum", + "SeriesPremiere": "Sorozat premierje", + "SetReleaseGroup": "Release csoport beállítása", + "ShowTitle": "Cím mutatása", + "Small": "Kicsi", + "Socks4": "Socks4", + "Sort": "Fajta", + "SeriesDetailsGoTo": "Ugrás ide: {title}", + "Condition": "Feltétel", + "DeleteRemotePathMappingMessageText": "Biztosan törli ezt a távoli útvonal-leképezést?", + "RecentChanges": "Friss változások", + "ReleaseSceneIndicatorMappedNotRequested": "A feltérképezett epizódot nem kérték ebben a keresésben.", + "ChownGroupHelpText": "Csoport neve vagy gid. Távoli fájlrendszerekhez használja a gid-t.", + "ChmodFolderHelpTextWarning": "Ez csak akkor működik, ha a(z) {appName} alkalmazást futtató felhasználó a fájl tulajdonosa. Jobb, ha megbizonyosodik arról, hogy a letöltési kliens megfelelően állítja be az engedélyeket.", + "ChownGroupHelpTextWarning": "Ez csak akkor működik, ha a(z) {appName} alkalmazást futtató felhasználó a fájl tulajdonosa. Jobb, ha a letöltési kliens ugyanazt a csoportot használja, mint a {appName}.", + "CollectionsLoadError": "Nem sikerült betölteni a gyűjteményeket", + "Conditions": "Körülmények", + "ContinuingSeriesDescription": "További epizódok/újabb évad várható", + "ConnectionLostToBackend": "A(z) {appName} megszakadt a kapcsolat a háttérrendszerrel, ezért újra kell tölteni a működés visszaállításához.", + "CountIndexersSelected": "{count} indexelő(k) kiválasztva", + "CountDownloadClientsSelected": "{count} letöltési kliens kiválasztva", + "CountImportListsSelected": "{count} importlista kiválasztva", + "DeleteAutoTag": "Automatikus címke törlése", + "DeleteSelectedDownloadClients": "Letöltési kliens(ek) törlése", + "DeleteRootFolder": "Gyökérmappa törlés", + "DeleteRootFolderMessageText": "Biztosan törli a(z) '{path}' gyökérmappát?", + "DeleteSelectedImportLists": "Importálási lista(k) törlése", + "DeleteSelectedImportListsMessageText": "Biztosan törölni szeretne {count} kiválasztott importlistát?", + "DeleteSelectedIndexers": "Indexelő(k) törlése", + "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", + "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", + "RemotePathMappingHostHelpText": "Ugyanaz a gazdagép, amelyet a távoli letöltési klienshez megadott", + "SelectQuality": "Minőség kiválasztása", + "SeriesIndexFooterEnded": "Befejeződött (az összes epizód letöltve)", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "A sorozatokkal és epizódokkal kapcsolatos információkat a TheTVDB.com biztosítja. [Kérjük, fontolja meg támogatásukat](https:", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} epizódfájl", + "SeriesIsMonitored": "A sorozatot figyelik", + "SeriesTitle": "Sorozat címe", + "ShortDateFormat": "Rövid dátumformátum", + "ShowNetwork": "Hálózat megjelenítése", + "Shutdown": "Leállitás", + "ReleaseGroup": "Release csapat", + "ReleaseSceneIndicatorUnknownMessage": "A számozás ennél az epizódnál eltérő, és a kiadás nem egyezik egyetlen ismert leképezéssel sem.", + "ReleaseSceneIndicatorUnknownSeries": "Ismeretlen epizód vagy sorozat.", + "CollapseMultipleEpisodesHelpText": "Több, ugyanazon a napon sugárzott epizód összecsukása", + "DeleteTagMessageText": "Biztosan törli a „{label}” címkét?", + "DeleteImportListMessageText": "Biztosan törli a(z) „{name}” listát?", + "RejectionCount": "Elutasítások száma", + "SeriesIndexFooterMissingMonitored": "Hiányzó epizódok (sorozat figyelve)", + "ShowMonitoredHelpText": "A figyelt állapot megjelenítése a plakát alatt", + "AutomaticUpdatesDisabledDocker": "Az automatikus frissítések közvetlenül nem támogatottak a Docker frissítési mechanizmus használatakor. Frissítenie kell a tároló képét a {appName} alkalmazáson kívül, vagy szkriptet kell használnia", + "CustomFormatJson": "Egyéni formátum JSON", + "ConnectionLostReconnect": "A(z) {appName} automatikusan megpróbál csatlakozni, vagy kattintson az újratöltés gombra lent.", + "DeleteImportListExclusionMessageText": "Biztosan törli ezt az importlista-kizárást?", + "DeleteDownloadClientMessageText": "Biztosan törli a(z) \"{name}\" letöltési klienst?", + "DeleteIndexerMessageText": "Biztosan törli a(z) \"{name}\" indexelőt?", + "DeleteNotificationMessageText": "Biztosan törli a(z) „{name}” értesítést?", + "DeleteQualityProfileMessageText": "Biztosan törli a(z) „{name}” minőségi profilt?", + "RemotePathMappingRemotePathHelpText": "A letöltési kliens által elért könyvtár gyökérútvonala", + "ReleaseGroups": "Release csoportok", + "SingleEpisode": "Egyetlen Epizód", + "DownloadClientAriaSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához", + "DownloadClientPriorityHelpText": "Töltse le a kliens prioritást 1-től (legmagasabb) 50-ig (legalacsonyabb). Alapértelmezés: 1. A Round-Robint az azonos prioritású kliensek használják.", + "DownloadClientQbittorrentSettingsContentLayout": "Tartalom elrendezése", + "FormatAgeHours": "órák", + "FormatAgeMinute": "perc", + "FormatAgeMinutes": "percek", + "FormatAgeDays": "napok", + "FormatAgeHour": "óra", + "SeriesFolderFormatHelpText": "Új sorozat hozzáadásakor vagy sorozatszerkesztőn keresztüli mozgatásakor használatos", + "SeriesTypesHelpText": "A sorozattípust átnevezésre, elemzésre és keresésre használják", + "SetPermissionsLinuxHelpTextWarning": "Ha nem biztos abban, hogy ezek a beállítások mit csinálnak, ne módosítsa őket.", + "ShowMonitored": "Megfigyelt megjelenítése", + "ShowEpisodeInformation": "Epizód Információ mutatása", + "ShowEpisodeInformationHelpText": "Az epizód címének és számának megjelenítése", + "ShowEpisodes": "Epizódok megjelenítése", + "ShowSearch": "Keresés mutatása", + "RemotePathMappingLocalPathHelpText": "Útvonal, amelyet a(z) {appName} használjon a távoli elérési út helyi eléréséhez", + "SeriesIndexFooterContinuing": "Folytatás (Minden epizód letöltve)", + "SeriesEditRootFolderHelpText": "A sorozatok ugyanabba a gyökérmappába való áthelyezése használható a sorozatmappák átnevezésére, hogy megfeleljenek a frissített címnek vagy elnevezési formátumnak", + "SeriesDetailsNoEpisodeFiles": "Nincsenek epizódfájlok", + "SeriesDetailsOneEpisodeFile": "1 epizód fájl", + "SeriesDetailsRuntime": "{runtime} perc", + "SeriesLoadError": "Nem sikerült betölteni a sorozatot", + "SeriesMatchType": "Sorozat egyezési típusa", + "SeriesIsUnmonitored": "A sorozat nem figyelhető", + "ShowSearchHelpText": "Keresés gomb megjelenítése az egérrel", + "ReleaseProfiles": "Release profilok", + "SelectSeries": "Sorozat kiválasztása", + "BlocklistAndSearchMultipleHint": "Indítsa el a helyettesítők keresését a tiltólistázás után", + "BlocklistMultipleOnlyHint": "Blokklista helyettesítők keresése nélkül", + "BlocklistOnly": "Csak blokkolólista", + "BlocklistOnlyHint": "Blokklista csere keresése nélkül", + "ChangeCategory": "Kategória módosítása", + "ChangeCategoryMultipleHint": "Módosítja a letöltéseket az „Importálás utáni kategóriára” a Download Clientből", + "SeriesIndexFooterDownloading": "Folytatás (Minden epizód letöltve)", + "SeriesType": "Sorozat típus", + "SeriesTypes": "Sorozattípusok", + "SetPermissions": "Állítsa be az engedélyeket", + "ShowBanners": "Bannerek megjelenítése", + "ShowDateAdded": "Hozzáadás dátumának megjelenítése", + "ShowSeasonCount": "Szezonszám megjelenítése", + "ShowSeriesTitleHelpText": "Mutasd a sorozat címét a plakát alatt", + "ShowSizeOnDisk": "Méret megjelenítése a lemezen", + "SizeLimit": "Méretkorlát", + "DeleteSelectedDownloadClientsMessageText": "Biztosan törölni szeretné a kiválasztott {count} letöltési klienst?" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 65dbc3e61..2b0da009b 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -241,5 +241,12 @@ "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Conferma la nuova password", "ClickToChangeSeason": "Click per cambiare stagione", "AnalyseVideoFilesHelpText": "Estrai le informazioni video come risoluzione, durata e codec dai file. Questo richiede che {appName} legga delle parti dei file, ciò potrebbe causare un alto utilizzo del disco e della rete durante le scansioni.", - "DownloadClientStatusAllClientHealthCheckMessage": "Nessun client di download è disponibile a causa di errori" + "DownloadClientStatusAllClientHealthCheckMessage": "Nessun client di download è disponibile a causa di errori", + "AddImportListImplementation": "Aggiungi lista di importazione - {implementationName}", + "AddNewSeriesHelpText": "È facile aggiungere una nuova serie, basta iniziare a digitare il nome della serie che desideri aggiungere.", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Inizia la ricerca degli episodi non soddisfatti", + "AddListExclusionSeriesHelpText": "Impedisce che le serie vengano aggiunte a{appName} tramite liste", + "AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto", + "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", + "AutoRedownloadFailed": "Download fallito" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 816da29fe..11c82bb52 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -98,7 +98,7 @@ "ExportCustomFormat": "Exportar formato personalizado", "Negated": "Negado", "Remove": "Remover", - "RemoveFromDownloadClient": "Remover Do Cliente de Download", + "RemoveFromDownloadClient": "Remover do Cliente de Download", "RemoveSelectedItem": "Remover Item Selecionado", "RemoveSelectedItemQueueMessageText": "Tem certeza de que deseja remover 1 item da fila?", "RemoveSelectedItems": "Remover Itens Selecionados", @@ -122,7 +122,7 @@ "Implementation": "Implementação", "Disabled": "Desabilitado", "Edit": "Editar", - "ManageClients": "Gerenciar clientes", + "ManageClients": "Gerenciar Clientes", "ManageIndexers": "Gerenciar indexadores", "ManageDownloadClients": "Gerenciar clientes de download", "ManageImportLists": "Gerenciar listas de importação", @@ -284,7 +284,7 @@ "Size": "Tamanho", "Source": "Fonte", "Started": "Iniciado", - "StartupDirectory": "Diretório de inicialização", + "StartupDirectory": "Diretório de Inicialização", "Status": "Estado", "TestAll": "Testar Tudo", "TheLogLevelDefault": "O nível de registro é padronizado como 'Info' e pode ser alterado em [Configurações Gerais](/settings/general)", @@ -1893,8 +1893,8 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "O lançamento deve ser menor ou igual a este tamanho", "CustomFormatsSpecificationMinimumSize": "Tamanho Mínimo", "CustomFormatsSpecificationMinimumSizeHelpText": "O lançamento deve ser maior que esse tamanho", - "CustomFormatsSpecificationRegularExpression": "Idioma", - "CustomFormatsSpecificationRegularExpressionHelpText": "Regex dos Formatos Personalizados são Insensíveis à diferença de maiúscula e minúscula", + "CustomFormatsSpecificationRegularExpression": "Expressão Regular", + "CustomFormatsSpecificationRegularExpressionHelpText": "RegEx do Formato Personalizado Não Diferencia Maiúsculas de Minúsculas", "CustomFormatsSpecificationReleaseGroup": "Grupo do Lançamento", "CustomFormatsSpecificationResolution": "Resolução", "CustomFormatsSpecificationSource": "Fonte", @@ -2007,5 +2007,27 @@ "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo com metadados da série completa", "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')", "NotificationsEmailSettingsUseEncryption": "Usar Criptografia", - "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia" + "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia", + "IgnoreDownloadsHint": "Impede que {appName} processe ainda mais esses downloads", + "RemoveFromDownloadClientHint": "Remove download e arquivo(s) do cliente de download", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.", + "BlocklistMultipleOnlyHint": "Adiciona a Lista de bloqueio sem procurar substitutos", + "BlocklistOnly": "Apenas Adicionar a Lista de Bloqueio", + "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar a lista de bloqueio", + "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente por {appName} via RSS ou Pesquisa Automática", + "ChangeCategoryHint": "Altera o download para a 'Categoria Pós-Importação' do Cliente de Download", + "ChangeCategoryMultipleHint": "Altera os downloads para a 'Categoria Pós-Importação' do Cliente de Download", + "DatabaseMigration": "Migração de Banco de Dados", + "DoNotBlocklistHint": "Remover sem colocar na lista de bloqueio", + "ChangeCategory": "Alterar Categoria", + "DoNotBlocklist": "Não coloque na lista de bloqueio", + "IgnoreDownloads": "Ignorar Downloads", + "IgnoreDownloadHint": "Impede que {appName} processe ainda mais este download", + "RemoveMultipleFromDownloadClientHint": "Remove downloads e arquivos do cliente de download", + "RemoveQueueItemRemovalMethod": "Método de Remoção", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remover do Cliente de Download' removerá os downloads e os arquivos do cliente de download.", + "BlocklistAndSearch": "Lista de Bloqueio e Pesquisa", + "BlocklistAndSearchHint": "Inicie uma busca por um substituto após adicionar a lista de bloqueio", + "BlocklistOnlyHint": "Adiciona a Lista de bloqueio sem procurar um substituto", + "IgnoreDownload": "Ignorar Download" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 0967ef424..cab8c997a 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -1 +1,25 @@ -{} +{ + "CalendarOptions": "Опції Календаря", + "Activity": "Активність", + "AddAutoTag": "Додати Авто Тег", + "AddCondition": "Додати Умову", + "UpdateMechanismHelpText": "Використайте вбудоване оновлення {appName}'у або скрипт", + "Add": "Додати", + "About": "Про нас", + "Actions": "Дії", + "AddAutoTagError": "Не вдалося додати новий авто тег, спробуйте ще раз.", + "AddDownloadClientError": "Не вдається додати новий клієнт для завантаження, повторіть спробу.", + "AddExclusion": "Додати виняток", + "AddImportListExclusion": "Додати виняток до списку імпорту", + "AddDownloadClient": "Додати клієнт завантаження", + "Absolute": "Абсолютний", + "AddANewPath": "Додати новий шлях", + "AddConditionError": "Не вдалося додати нову умову, спробуйте ще раз.", + "AddConditionImplementation": "Додати умову - {implementationName}", + "AddConnection": "Додати Підключення", + "AddConnectionImplementation": "Додати Підключення - {implementationName}", + "AddCustomFilter": "Додати власний фільтр", + "AddCustomFormat": "Додати власний формат", + "AddDelayProfile": "Додати Профіль Затримки", + "AddCustomFormatError": "Неможливо додати новий власний формат, спробуйте ще раз." +} From 70807a9dcf0fbbd39768779ec8b416c78e6804f8 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Sat, 27 Jan 2024 05:58:00 +0000 Subject: [PATCH 075/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 121 +++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 51fbfb3b2..ee63aefa5 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -2956,6 +2956,101 @@ } } }, + "/api/v3/config/importlist": { + "get": { + "tags": [ + "ImportListConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/importlist/{id}": { + "put": { + "tags": [ + "ImportListConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "ImportListConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + } + }, "/api/v3/importlistexclusion": { "get": { "tags": [ @@ -8753,6 +8848,23 @@ }, "additionalProperties": false }, + "ImportListConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "listSyncLevel": { + "$ref": "#/components/schemas/ListSyncLevelType" + }, + "listSyncTag": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "ImportListExclusionResource": { "type": "object", "properties": { @@ -9104,6 +9216,15 @@ }, "additionalProperties": false }, + "ListSyncLevelType": { + "enum": [ + "disabled", + "logOnly", + "keepAndUnmonitor", + "keepAndTag" + ], + "type": "string" + }, "LocalizationLanguageResource": { "type": "object", "properties": { From 11a18b534a37dcff7b6ef03ac0cec73434e01dae Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 31 Jan 2024 04:59:05 +0000 Subject: [PATCH 076/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Crocmou <slaanesh8854@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lars <lars.erik.heloe@gmail.com> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Stas Panasiuk <temnyip@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: resi23 <x-resistant-x@gmx.de> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 3 +- src/NzbDrone.Core/Localization/Core/es.json | 194 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/fi.json | 188 +++++++++++------ src/NzbDrone.Core/Localization/Core/fr.json | 24 ++- src/NzbDrone.Core/Localization/Core/hu.json | 72 ++++++- .../Localization/Core/nb_NO.json | 3 +- .../Localization/Core/pt_BR.json | 95 +++++---- src/NzbDrone.Core/Localization/Core/uk.json | 19 +- 8 files changed, 473 insertions(+), 125 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index aa3316c93..7ba6610dd 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -792,5 +792,6 @@ "Branch": "Branch", "Airs": "Wird ausgestrahlt", "AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden", - "IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht" + "IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht", + "DownloadClientSettingsAddPaused": "Pausiert hinzufügen" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 8455a5cf7..9c26968fc 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -57,7 +57,7 @@ "Condition": "Condición", "Component": "Componente", "Custom": "Personalizado", - "Cutoff": "Requisito", + "Cutoff": "Umbral", "Dates": "Fechas", "Debug": "Debug", "Date": "Fecha", @@ -181,7 +181,7 @@ "AddNewSeriesError": "Falló al cargar los resultados de la búsqueda, inténtelo de nuevo.", "AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empiece escribiendo el nombre de la serie que desea añadir.", "AddNewSeriesRootFolderHelpText": "La subcarpeta '{folder}' será creada automáticamente", - "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con requisitos no cumplidos", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con umbrales no alcanzados", "AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes", "AddQualityProfile": "Añadir Perfil de Calidad", "AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.", @@ -436,7 +436,7 @@ "CustomFormatsLoadError": "No se pueden cargar formatos personalizados", "CustomFormatsSettings": "Configuración de formatos personalizados", "CustomFormatsSettingsSummary": "Formatos y configuraciones personalizados", - "CutoffUnmet": "Límite no alcanzado", + "CutoffUnmet": "Umbrales no alcanzados", "DailyEpisodeFormat": "Formato de episodio diario", "Database": "Base de datos", "DelayMinutes": "{delay} Minutos", @@ -469,8 +469,8 @@ "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo", "ContinuingSeriesDescription": "Se esperan más episodios u otra temporada", - "CutoffUnmetLoadError": "Error cargando objetos con condiciones de corte incumplidas", - "CutoffUnmetNoItems": "Sin objetos sin condiciones de corte incumplidas", + "CutoffUnmetLoadError": "Error cargando elementos con umbrales no alcanzados", + "CutoffUnmetNoItems": "No hay elementos con umbrales no alcanzados", "DelayProfile": "Perfil de retardo", "Delete": "Eliminar", "DeleteDelayProfile": "Eliminar Perfil de Retardo", @@ -877,5 +877,187 @@ "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", - "EpisodeFileDeletedTooltip": "Archivo de episodio eliminado" + "EpisodeFileDeletedTooltip": "Archivo de episodio eliminado", + "Health": "Salud", + "Here": "aquí", + "Host": "Host", + "HideEpisodes": "Ocultar episodios", + "Hostname": "Nombre de host", + "ICalSeasonPremieresOnlyHelpText": "Solo el primer episodio de una temporada estará en el feed", + "ImportExistingSeries": "Importar series existentes", + "ImportErrors": "Importar errores", + "ImportList": "Importar lista", + "ImportListSettings": "Importar ajustes de lista", + "ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o desde listas de Trakt y gestionar listas de exclusiones", + "IncludeCustomFormatWhenRenamingHelpText": "Incluir en formato de renombrado {Custom Formats}", + "QualityCutoffNotMet": "Calidad del umbral que no ha sido alcanzado", + "SearchForCutoffUnmetEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios en Umbrales no alcanzados?", + "IndexerOptionsLoadError": "No se pudo cargar las opciones del indexador", + "IndexerIPTorrentsSettingsFeedUrl": "URL de feed", + "ICalFeed": "Feed de iCal", + "Import": "Importar", + "ImportFailed": "La importación falló: {sourceTitle}", + "HiddenClickToShow": "Oculto, click para mostrar", + "HttpHttps": "HTTP(S)", + "ICalLink": "Enlace de iCal", + "IconForFinalesHelpText": "Muestra un icono para finales de series/temporadas basado en la información de episodio disponible", + "HideAdvanced": "Ocultar avanzado", + "ICalShowAsAllDayEventsHelpText": "Los eventos aparecerán como eventos para todo el día en tu calendario", + "IRCLinkText": "#sonarr en Libera", + "IconForSpecialsHelpText": "Muestra un icono para episodios especiales (temporada 0)", + "IgnoredAddresses": "Ignorar direcciones", + "ImdbId": "ID de IMDb", + "ImportCustomFormat": "Importar formato personalizado", + "ImportExtraFiles": "Importar archivos adicionales", + "ImportExtraFilesEpisodeHelpText": "Importa archivos adicionales (subtítulos, nfo, etc) tras importar un archivo de episodio", + "ImportListExclusionsLoadError": "No se pudo cargar Importar lista de exclusiones", + "ImportListStatusUnavailableHealthCheckMessage": "Listas no disponibles debido a fallos: {importListNames}", + "ImportListsAniListSettingsAuthenticateWithAniList": "Autenticar con AniList", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: Vistos por completo", + "ImportListsAniListSettingsImportDroppedHelpText": "Lista: Abandonados", + "ImportListsAniListSettingsImportFinished": "Importación Finalizados", + "ImportListsAniListSettingsImportDropped": "Importar Abandonados", + "ImportListsAniListSettingsImportFinishedHelpText": "Medios: Todos los episodios se han emitido", + "ImportListsAniListSettingsImportHiatus": "Importar En hiatus", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Medios: La emisión aún no ha empezado", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: En espera", + "ImportListsAniListSettingsImportPaused": "Importar En pausa", + "ImportListsAniListSettingsImportReleasingHelpText": "Medios: Nuevos episodios actualmente en emisión", + "ImportListsAniListSettingsImportWatching": "Importar Viendo", + "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: Revisionando actualmente", + "ImportListsAniListSettingsImportRepeating": "Importar Repitiendo", + "ImportListsAniListSettingsImportReleasing": "Importar En lanzamiento", + "ImportListsAniListSettingsUsernameHelpText": "Nombre de usuario de la lista de la que importar", + "ImportListsCustomListSettingsUrlHelpText": "La URL para la lista de series", + "ImportListsCustomListValidationAuthenticationFailure": "Fallo de autenticación", + "ImportListsCustomListValidationConnectionError": "No se pudo hacer la petición a esa URL. Código de estado: {exceptionStatusCode}", + "ImportListsImdbSettingsListId": "ID de lista", + "ImportListsImdbSettingsListIdHelpText": "ID de lista de IMDb (p. ej. ls12345678)", + "ImportListsLoadError": "No se pudo cargas Importar listas", + "ImportListsPlexSettingsAuthenticateWithPlex": "Autenticar con Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Lista de seguimiento de Plex", + "ImportListsSettingsExpires": "Expira", + "ImportListsSettingsAccessToken": "Token de acceso", + "ImportListsSettingsRefreshToken": "Token de refresco", + "ImportListsSettingsAuthUser": "Autenticación de usuario", + "ImportListsSettingsRssUrl": "URL de RSS", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Autenticar con Simkl", + "ImportListsSimklSettingsListType": "Tipo de lista", + "ImportListsSimklSettingsShowType": "Mostrar tipo", + "ImportListsSimklSettingsShowTypeHelpText": "Tipo de show del que deseas importar", + "ImportListsSimklSettingsUserListTypeCompleted": "Completados", + "ImportListsSimklSettingsUserListTypeHold": "En espera", + "ImportListsSimklSettingsUserListTypeDropped": "Abandonados", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Planeado ver", + "ImportListsSimklSettingsUserListTypeWatching": "Viendo", + "ImportListsSonarrSettingsApiKeyHelpText": "Clave API de la instancia de {appName} de la que importar", + "ImportListsSonarrSettingsFullUrl": "URL completa", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Perfiles de calidad de la instancia de la fuente de la que importar", + "ImportListsSonarrSettingsRootFoldersHelpText": "Carpetas raíz de la instancia de la fuente de la que importar", + "ImportListsTraktSettingsAdditionalParameters": "Parámetros adicionales", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Parámetros adicionales de la API de Trakt", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Autenticar con Trakt", + "ImportListsTraktSettingsGenresHelpText": "Filtrar series por género de Trakt (separados por coma) solo para listas populares", + "ImportListsTraktSettingsGenres": "Géneros", + "ImportListsTraktSettingsLimitHelpText": "Limita el número de series a obtener", + "ImportListsTraktSettingsLimit": "Limitar", + "ImportListsTraktSettingsListType": "Tipo de lista", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Shows recomendados de todos los tiempos", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Shows recomendados por semana", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Shows recomendados por mes", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Top de series vistas de todos los tiempos", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Top de series vistas por mes", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Top de series vistas por año", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Shows en tendencia", + "ImportListsTraktSettingsPopularName": "Lista popular de Trakt", + "ImportListsTraktSettingsRating": "Calificación", + "ImportListsTraktSettingsRatingHelpText": "Filtrar series por rango de calificación (0-100)", + "ImportListsTraktSettingsUserListName": "Usuario de Trakt", + "ImportListsTraktSettingsUserListTypeCollection": "Lista de colecciones de usuario", + "ImportListsTraktSettingsUserListTypeWatch": "Lista de seguimiento de usuario", + "ImportListsTraktSettingsUserListTypeWatched": "Lista de vistos de usuario", + "ImportListsTraktSettingsUserListUsernameHelpText": "Usuario para la lista de la que importar (dejar vacío para usar Autenticación de usuario)", + "ImportListsTraktSettingsUsernameHelpText": "Usuario para la lista de la que importar", + "ImportListsTraktSettingsWatchedListFilter": "Filtrar lista de vistos", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Si el tipo de vista es Vistos, selecciona el tipo de series que quieres importar", + "ImportListsTraktSettingsWatchedListSorting": "Ordenar la lista de vistos", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Si el tipo de lista es Vistos, selecciona el orden para ordenar la lista", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% vistos", + "ImportListsTraktSettingsWatchedListTypeAll": "Todos", + "ImportListsTraktSettingsWatchedListTypeInProgress": "En progreso", + "ImportListsTraktSettingsYears": "Años", + "ImportListsValidationInvalidApiKey": "La clave API es inválida", + "ImportListsValidationTestFailed": "El test fue abortado debido a un error: {exceptionMessage}", + "ImportScriptPathHelpText": "La ruta al script a usar para importar", + "ImportUsingScriptHelpText": "Copia archivos a importar usando un script (p. ej. para transcodificación)", + "Importing": "Importando", + "IncludeUnmonitored": "Incluir sin monitorizar", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a fallos durante más de 6 horas", + "IRC": "IRC", + "ICalShowAsAllDayEvents": "Mostrar como eventos para todo el día", + "IndexerHDBitsSettingsCategories": "Categorías", + "IndexerHDBitsSettingsCategoriesHelpText": "Si no se especifica, se usan todas las opciones.", + "HomePage": "Página principal", + "ImportSeries": "Importar series", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a fallos durante más de 6 horas: {indexerNames}", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "La URL completa de feed RSS generada por IPTorrents, usa solo las categorías que seleccionaste (HD, SD, x264, etc...)", + "ICalIncludeUnmonitoredEpisodesHelpText": "Incluye episodios sin monitorizar en el feed de iCal", + "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.", + "IndexerPriority": "Prioridad del indexador", + "IconForFinales": "Icono para Finales", + "IgnoreDownload": "Ignorar descarga", + "IgnoreDownloads": "Ignorar descargas", + "IgnoreDownloadHint": "Detiene {appName} de procesar esta descarga más adelante", + "IgnoreDownloadsHint": "Detiene {appName} de procesar estas descargas más adelante", + "Images": "Imágenes", + "ImportCountSeries": "Importar {selectedCount} series", + "ImportListStatusAllUnavailableHealthCheckMessage": "Ninguna lista está disponible debido a fallos", + "ImportLists": "Importar listas", + "ImportListsAniListSettingsImportHiatusHelpText": "Medios: Series en hiatus", + "ImportListsAniListSettingsImportCompleted": "Importación Completados", + "ImportListsAniListSettingsImportCancelledHelpText": "Medios: Series que están canceladas", + "ImportListsCustomListSettingsName": "Lista personalizada", + "ImportListsCustomListSettingsUrl": "URL de la lista", + "ImportListsPlexSettingsWatchlistRSSName": "RSS de lista de seguimiento de Plex", + "ImportListsSimklSettingsListTypeHelpText": "Tipo de lista de la que deseas importar", + "ImportListsSimklSettingsName": "Lista de seguimiento de usuario de Simkl", + "ImportListsSonarrSettingsTagsHelpText": "Etiquetas de la instancia de la fuente de la que importar", + "ImportListsTraktSettingsListName": "Nombre de lista", + "ImportListsTraktSettingsListNameHelpText": "Nombre de lista para importar, la lista debe ser pública o debes tener acceso a la lista", + "ImportListsTraktSettingsListTypeHelpText": "Tipo de lista de la que deseas importar", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Shows anticipados", + "ImportListsTraktSettingsPopularListTypePopularShows": "Shows populares", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Shows recomendados por año", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Top de series vistas por semana", + "ImportListsTraktSettingsYearsHelpText": "Filtrar series por año o un rango de años", + "ImportListsValidationUnableToConnectException": "No se pudo conectar para importar la lista: {exceptionMessage}. Comprueba el registro sobre este error para más detalles.", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Habilitar Gestión de descargas completadas si es posible", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Habilitar Gestión de descargas completadas si es posible (Multi-ordenador no soportado)", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Habilitar Gestión de descargas completadas", + "ImportedTo": "Importar a", + "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado cuando se renombra", + "CleanLibraryLevel": "Limpiar el nivel de la librería", + "SearchForCutoffUnmetEpisodes": "Buscar todos los episodios en Umbrales no alcanzados", + "IconForSpecials": "Icono para Especiales", + "ImportListExclusions": "Importar lista de exclusiones", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas las listas requieren interacción manual debido a posibles búsquedas parciales", + "ImportListsAniListSettingsImportCancelled": "Importación Cancelados", + "ImportListsAniListSettingsImportPlanning": "Importar planeados", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: Planeados para ver", + "ImportListsAniListSettingsImportWatchingHelpText": "Lista: Viendo actualmente", + "ImportListsAniListSettingsImportNotYetReleased": "Importar Aún sin lanzar", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} de la que importar", + "IndexerPriorityHelpText": "Prioridad del indexador desde 1 (la más alta) hasta 50 (la más baja). Predeterminado: 25. Usada para desempatar lanzamientos iguales cuando se capturan, {appName} seguirá usando todos los indexadores habilitados para Sincronización de RSS y Búsqueda.", + "IncludeHealthWarnings": "Incluir avisos de salud", + "IndexerJackettAllHealthCheckMessage": "Indexadores usan el endpoint de Jackett no soportado 'todo': {indexerNames}", + "HourShorthand": "h", + "ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o haz click para suscribirte si tu navegador soportar WebCal", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index b869fa617..acfca2a33 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1,13 +1,13 @@ { "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}\", muttei voi käyttää sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", + "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ää.", "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.", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\" näkyy, muttei voi käyttää sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "Jaksojen tuonti epäonnistui. Katso tarkemmat tiedot lokista.", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattihakua varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName}in automaattihaku ei löydä tuloksia.", @@ -45,7 +45,7 @@ "UiLanguage": "Käyttöliittymän kieli", "UiLanguageHelpText": "{appName}in käyttöliittymän kieli.", "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 skriptillä.", - "AddListExclusionSeriesHelpText": "Estä sarjan poiminta {appName}iin listoilta.", + "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.", "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja se on käynnistettävä uudelleen.", @@ -92,7 +92,7 @@ "Actions": "Toiminnot", "Absolute": "Ehdoton", "AddANewPath": "Lisää uusi polku", - "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kartoitukset ja lataustyökalun asetukset.", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", "AddDownloadClientImplementation": "Lisäätään lataustyökalua - {implementationName}", "AddImportListExclusionError": "Virhe lisättäessä tuontilistapokkeusta. Yritä uudelleen.", "AddIndexerImplementation": "Lisätään tietolähdettä - {implementationName}", @@ -107,7 +107,7 @@ "AnimeEpisodeFormat": "Animejaksojen kaava", "CheckDownloadClientForDetails": "katso lisätietoja lataustyökalusta", "Donations": "Lahjoitukset", - "DownloadClientsSettingsSummary": "Lataustyökalut, latausten käsittely ja etäsijaintien kartoitukset.", + "DownloadClientsSettingsSummary": "Lataustyökalut, latausten käsittely ja etäsijaintien kohdistukset.", "EpisodeFileRenamed": "Jaksotiedosto nimettiin uudelleen", "EpisodeImported": "Jakso tuotiin", "EpisodeImportedTooltip": "Jakso ladattiin ja poimittiin lataustyökalulta", @@ -128,10 +128,10 @@ "AddNewRestriction": "Lisää uusi rajoitus", "All": "Kaikki", "AddReleaseProfile": "Lisää jukaisuprofiili", - "AddRemotePathMapping": "Lisää etäsijainnin kartoitus", + "AddRemotePathMapping": "Lisää etäsijainnin kohdistus", "AddQualityProfileError": "Virhe lisättäessä laatuprofiilia. Yritä uudelleen.", "AfterManualRefresh": "Manuaalisen päivityksen jälkeen", - "AddRemotePathMappingError": "Virhe lisättäessä etäsijainnin kartoitusta. Yritä uudelleen.", + "AddRemotePathMappingError": "Etäsijainnin kohdistuksen lisäys epäonnistui. Yritä uudelleen.", "ApplicationURL": "Sovelluksen URL", "AuthBasic": "Perus (ponnahdusikkuna)", "AuthForm": "Lomake (kirjautumissivu)", @@ -145,7 +145,7 @@ "Clear": "Tyhjennä", "CollectionsLoadError": "Virhe ladattaessa kokoelmia", "CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot", - "CreateEmptySeriesFoldersHelpText": "Luo puuttuvien sarjojen kansiot kirjastotarkistusten yhteydessä.", + "CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.", "CustomFormatsLoadError": "Virhe ladattaessa mukautettuja muotoja", "Debug": "Vianselvitys", "DeleteDelayProfileMessageText": "Haluatko varmasti poistaa viiveprofiilin?", @@ -162,7 +162,7 @@ "MoreInfo": "Lisätietoja", "Network": "Kanava/tuottaja", "OnGrab": "Kun julkaisu kaapataan", - "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "\"{appName}\" ei voinut lisätä tunnistettä qBittorentiin.", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} ei voinut lisätä tunnistetta qBittorrentiin.", "SeriesFolderFormat": "Sarjakansioiden kaava", "TagDetails": "Tunnisteen \"{0}\" tiedot", "DownloadClientStatusSingleClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", @@ -223,7 +223,7 @@ "MonitorNewSeasons": "Valvo uusia kausia", "MonitorLastSeasonDescription": "Valvo kaikkia viimeisen kauden jaksoja.", "MonitorNewSeasonsHelpText": "Uusien kausien automaattivalvonnan käytäntö.", - "MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjan tiedostot kohteeseen \"{destinationRootFolder}\"?", + "MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjakansiot kohteeseen \"{destinationRootFolder}\"?", "MoreDetails": "Lisätietoja", "MonitoredStatus": "Valvottu/tila", "NegateHelpText": "Jos käytössä, ei mukautettua muotoa sovelleta tämän \"{implementationName}\" -ehdon täsmätessä.", @@ -238,9 +238,9 @@ "NoLogFiles": "Lokitiedostoja ei ole", "RefreshSeries": "Päivitä sarja", "ReleaseSceneIndicatorUnknownSeries": "Tuntematon jakso tai sarja.", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kartoitukset ja lataustyökalun asetukset.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", "RemoveCompletedDownloads": "Poista valmistuneet lataukset", - "RemotePathMappingsLoadError": "Etäsijaintien kartoitusten lataus epäonnistui", + "RemotePathMappingsLoadError": "Etäsijaintien kohdistusten lataus epäonnistui", "RemoveFailedDownloads": "Poista epäonnistuneet lataukset", "RemoveFailed": "Poisto epäonnistui", "RemoveFromBlocklist": "Poista estolistalta", @@ -280,10 +280,10 @@ "ReleaseProfileTagSeriesHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", "ReleaseTitle": "Julkaisun nimike", "Reload": "Lataa uudelleen", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etsijaintien kartoitukset lataustyökalun asetukset.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etsijaintien kohdistukset ja lataustyökalun asetukset.", "RemoveTagsAutomatically": "Poista tunnisteet automaattisesti", - "RemotePathMappingsInfo": "Etäsijaintien kartoitusta tarvitaan harvoin ja jos {app} ja lataustyökalu suoritetaan samassa järjestelmässä, on parempi käyttää paikallisia polkuja. Lue lisää [wikistä]({wikiLink}).", - "RemovedSeriesMultipleRemovedHealthCheckMessage": "Sarjat {series} on poistettu TheTVDB.comista", + "RemotePathMappingsInfo": "Etäsijaintien kohdistuksia tarvitaan harvoin ja jos {appName} ja lataustyökalu suoritetaan samassa järjestelmässä, on parempi käyttää paikallisia polkuja. Lue lisää [wikistä]({wikiLink}).", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Sarjat {series} poistettiin TheTVDB:stä.", "RemovedFromTaskQueue": "Poistettu tehtäväjonosta", "RestartNow": "Käynnistä uudelleen nyt", "AllTitles": "Kaikki nimikkeet", @@ -333,12 +333,12 @@ "Trace": "Jäljitys", "TotalRecords": "Rivien kokonaismäärä: {totalRecords}", "TotalSpace": "Kokonaistila", - "TvdbIdExcludeHelpText": "Ohitettavan sarjan TheTVDB ID", + "TvdbIdExcludeHelpText": "Ohitettavan sarjan TheTVDB ID.", "Type": "Tyyppi", "TypeOfList": "{typeOfList}-lista", "Twitter": "X (Twitter)", "UiSettingsSummary": "Kalenterin, päiväyksen ja värirajoitteisten asetukset.", - "UnmappedFolders": "Kartoittamattomat kansiot", + "UnmappedFolders": "Kohdistamattomat kansiot", "UnmonitorSpecialEpisodes": "Älä valvo erikoisjaksoja", "UnmonitorSpecialsEpisodesDescription": "Lopeta kaikkien erikoisjaksojen valvonta muuttamatta muiden jakosojen tilaa.", "Unmonitored": "Valvomattomat", @@ -374,7 +374,7 @@ "Downloading": "Ladataan", "ProgressBarProgress": "Tilapalkissa {progress} %", "Queue": "Jono", - "RemotePathMappingRemotePathHelpText": "Lataustyökalun käyttämän hakemiston juurisijainti", + "RemotePathMappingRemotePathHelpText": "Lataustyökalun käyttämän kansion juurisijainti.", "Restart": "Käynnistä uudelleen", "SizeLimit": "Kokorajoitus", "TestAllIndexers": "Tietolähteiden testaus", @@ -386,7 +386,7 @@ "SeriesMatchType": "Sarjan kohdistustyyppi", "RemotePathMappingLocalPathHelpText": "Polku, jonka kautta etäsijaintia tulee käyttää paikallisesti.", "SonarrTags": "{appName}in tunnisteet", - "CalendarLoadError": "Virhe ladattaessa kalenteria", + "CalendarLoadError": "Kalenterin lataus epäonnistui.", "BeforeUpdate": "Ennen päivitystä", "Backups": "Varmuuskopiot", "BackupNow": "Varmuuskopioi nyt", @@ -480,7 +480,7 @@ "MonitorFirstSeason": "Ensimmäinen kausi", "Monitoring": "Valvotaan", "MonitorRecentEpisodesDescription": "Valvo viimeisten 90 päivän sisällä julkaistuja jaksoja ja tulevia jaksoja.", - "MoveSeriesFoldersToNewPath": "Haluatko, että sarjan tiedostot siirretään lähteestä \"{originalPath}\" kohteeseen \"{destinationPath}\"?", + "MoveSeriesFoldersToNewPath": "Haluatko, että sarjan tiedostot siirretään kansiosta \"{originalPath}\" kohteeseen \"{destinationPath}\"?", "MonitorPilotEpisodeDescription": "Valvo vain ensimmäisen kauden ensimmäistä jaksoa.", "Name": "Nimi", "NamingSettings": "Nimeämisasetukset", @@ -489,7 +489,7 @@ "DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?", "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.", "DeleteSeriesModalHeader": "Poistetaan - {title}", - "DeletedSeriesDescription": "Sarja on poistettu TheTVDB.comista.", + "DeletedSeriesDescription": "Sarja poistettiin TheTVDB:stä.", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", "NotificationStatusSingleClientHealthCheckMessage": "Ilmoitukset eivät ole ongelmien vuoksi käytettävissä: {notificationNames}", "NotificationsLoadError": "Kytkösten lataus epäonnistui.", @@ -504,9 +504,9 @@ "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 kartoitus.", - "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} ei voinut lisätä tunnistetta lataustyökaluun \"{clientName}\".", - "DownloadClientDelugeValidationLabelPluginInactive": "Label-lisäosa ei ole aktiivinen", + "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.", + "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}", "AddCustomFilter": "Lisää oma suodatin", "AddConnectionImplementation": "Lisätään kytköstä - {implementationName}", @@ -516,7 +516,7 @@ "RenameEpisodes": "Nimeä jaksot uudelleen", "RenameFiles": "Nimeä tiedostot", "RemoveTagsAutomaticallyHelpText": "Poista tunnisteet automaattisesti, jos ehdot eivät täyty.", - "RemovedSeriesSingleRemovedHealthCheckMessage": "Sarja {series} on poistettu TheTVDB.comista", + "RemovedSeriesSingleRemovedHealthCheckMessage": "Sarja {series} poistettiin TheTVDB:stä.", "RescanAfterRefreshHelpTextWarning": "{appName} ei tunnista tiedostomuutoksia automaattisesti, jos asetuksena ei ole \"Aina\".", "RequiredHelpText": "Tämän \"{implementationName}\" -ehdon on täsmättävä mukautetun muodon käyttämiseksi. Muutoin riittää yksi \"{implementationName}\" -vastaavuus.", "RescanSeriesFolderAfterRefresh": "Tarkista sarjan kansio päivityksen jälkeen", @@ -610,7 +610,7 @@ "EditIndexerImplementation": "Muokataan tietolähdettä - {implementationName}", "EditListExclusion": "Muokkaa poikkeussääntöä", "EditSeriesModalHeader": "Muokataan - {title}", - "EnableInteractiveSearch": "Käytä manuaalihakua", + "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.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", @@ -626,15 +626,15 @@ "ErrorLoadingContents": "Virhe ladattaessa sisältöjä", "EpisodesLoadError": "Virhe ladattaessa jaksoja", "ErrorLoadingContent": "Virhe ladattaessa tätä sisältöä", - "FailedToLoadCustomFiltersFromApi": "Omien suodatinten lataus API:sta epäonnistui", - "FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus API:sta epäonnistui", + "FailedToLoadCustomFiltersFromApi": "Suodatinmukautusten lataus rajapinnasta epäonnistui", + "FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus rajapinnasta epäonnistui", "CalendarFeed": "{appName}in kalenterisyöte", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Jaksoa ladataan", "ListOptionsLoadError": "Virhe ladattaessa tuontilista-asetuksia", "RemoveCompleted": "Poisto on suoritettu", "ICalShowAsAllDayEvents": "Näytä koko päivän tapahtumina", - "FailedToLoadTagsFromApi": "Tunnisteiden lataus API:sta epäonnistui", + "FailedToLoadTagsFromApi": "Tunnisteiden lataus rajapinnasta epäonnistui", "FilterContains": "sisältää", "FilterDoesNotContain": "ei sisällä", "FilterEpisodesPlaceholder": "Suodata jaksoja nimella tai numerolla", @@ -662,7 +662,7 @@ "ImportListSearchForMissingEpisodesHelpText": "{appName} aloittaa automaattisesti puuttuvien jaksojen etsinnän kun sarja lisätään.", "InteractiveSearchModalHeaderSeason": "Manuaalihaku - {season}", "InteractiveImportNoImportMode": "Tuontitila on valittava.", - "InteractiveSearch": "Manuaalihaku", + "InteractiveSearch": "Etsi manuaalisesti", "InteractiveSearchModalHeader": "Manuaalihaku", "KeyboardShortcutsCloseModal": "Sulje nykyinen ikkuna", "KeyboardShortcuts": "Pikanäppäimet", @@ -705,8 +705,8 @@ "IndexerOptionsLoadError": "Virhe ladattaessa tietolähdeasetuksia", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", - "RemotePathMappings": "Etäsijaintien kartoitukset", - "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 kartoitus.", + "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.", "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", @@ -732,10 +732,10 @@ "ChownGroupHelpTextWarning": "Toimii vain, jos {appName}in suorittava käyttäjä on tiedoston omistaja. On parempi varmistaa, että lataustyökalu käyttää samaa ryhmää kuin {appName}.", "IndexerSettingsSeasonPackSeedTimeHelpText": "Aika, joka tuotantokausipaketin sisältävää torrentia tulee jakaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi.", "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kartoitukset ja säiliön tallennusmedian asetukset.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kohdistukset ja säiliön tallennusmedian asetukset.", "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot toimittaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe)", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe)", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakorajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", "Yesterday": "Eilen", @@ -744,10 +744,10 @@ "AutoTaggingRequiredHelpText": "Tämän \"{implementationName}\" -ehdon on täsmättävä automaattimerkinnän säännön käyttämiseksi. Muutoin yksittäinen \"{implementationName}\" -vastaavuus riittää.", "LibraryImportTipsSeriesUseRootFolder": "Osoita {appName} kaikki sarjat sisältävään kansioon, ei sarjakohtaiseen kansioon. Esim. \"`{goodFolderExample}`\" eikä \"`{badFolderExample}`\". Lisäksi jokaisen sarjan on oltava kirjasto-/juurkansion alla omissa kansioissa.", "SeriesDetailsGoTo": "Avaa {title}", - "SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivitettyä nimikettä tai nimeämistyyliä.", + "SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.", "WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?", "SeriesLoadError": "Virhe ladattaessa sarjoja", - "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden katkaisutasoa ei ole vielä saavutettu.", + "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.", "DownloadClientOptionsLoadError": "Virhe ladattaessa lataustyökaluasetuksia", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirtämisen sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjesteästä riippuen).", @@ -797,7 +797,7 @@ "Theme": "Teema", "DeleteSeriesFolderCountConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa?", "DownloadClientSettings": "Lataustyökalujen asetukset", - "FailedToLoadSeriesFromApi": "Sarjan lataus API:sta epäonnistui", + "FailedToLoadSeriesFromApi": "Sarjan lataus rajapinnasta epäonnistui", "OverrideGrabModalTitle": "Ohita ja kaappaa - {title}", "ShowEpisodeInformation": "Näytä jaksojen tiedot", "ShowEpisodes": "Näytä jaksot", @@ -842,7 +842,7 @@ "DeleteSelectedDownloadClients": "Poista lataustyökalu(t)", "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) tietoläh(teen/dettä)?", "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{customFormatName}\"?", - "DeleteRemotePathMapping": "Poista etäreittien kartoitus", + "DeleteRemotePathMapping": "Poista etäsijainnin kohdistus", "DeleteSelectedImportLists": "Poista tuontilista(t)", "DetailedProgressBar": "Yksityiskohtainen tilapalkki", "DelayProfileSeriesTagsHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille.", @@ -861,14 +861,14 @@ "Details": "Tiedot", "DownloadClient": "Lataustyökalu", "DisabledForLocalAddresses": "Ei käytössä paikallisille osoitteille", - "DownloadClientDelugeValidationLabelPluginFailure": "Tunnisteen määritys epäonnistui", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorioiden käyttö edellyttää, että lataustyökalun \"{clientName}\" Label-lisäosa on käytössä.", + "DownloadClientDelugeValidationLabelPluginFailure": "Label-tunnisteen määritys epäonnistui.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorioiden käyttö edellyttää, että {clientName}n Label-tunnistelisäosa on käytössä.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisää median ominaisuuksia tunnisteina. Vihjeet ovat esimerkkejä.", "DownloadClientFloodSettingsTagsHelpText": "Latauksen alkuperäiset tunnisteet. Jotta se voidaa tunnistaa, on latauksella oltava sen alkuperäiset tunnisteet. Tämä välttää ristiriidat muiden latausten kanssa.", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Ensimmäinen ja viimeinen ensin", "Donate": "Lahjoita", "DiskSpace": "Levytila", - "DownloadClientDelugeTorrentStateError": "Deluge ilmoittaa virheestä", + "DownloadClientDelugeTorrentStateError": "Deluge ilmoittaa virhettä", "DownloadClientFreeboxApiError": "Freebox API palautti virheen: {errorDescription}", "DownloadClientFreeboxAuthenticationError": "Freebox API -todennus epäonnistui. Syy: {errorDescription}", "Download": "Lataa", @@ -1003,7 +1003,7 @@ "Uptime": "Käyttöaika", "MonitorNoEpisodes": "Ei mitään", "MonitorNoEpisodesDescription": "Mitään jaksoja ei valvota.", - "RemotePathMappingWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kartoitukset ja lataustyökalun asetukset.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", "RemoveFailedDownloadsHelpText": "Poista epäonnistuneet lataukset lataustyökalun historiasta", "RemoveSelected": "Poista valitut", "RemoveSelectedBlocklistMessageText": "Haluatko varmasti poistaa valitut kohteet estolistalta?", @@ -1014,7 +1014,7 @@ "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", "Seasons": "Kaudet", "SearchAll": "Etsi kaikkia", - "SearchByTvdbId": "Voit etsiä myös sarjan TVDB ID:llä, esim. \"tvdb:71663\".", + "SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB ID-tunnisteilla (esim. \"tvdb:71663\").", "RootFoldersLoadError": "Virhe ladattaessa juurikansioita", "SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.", "Year": "Vuosi", @@ -1094,7 +1094,7 @@ "PrefixedRange": "Etuliitealue", "Range": "Alue", "RecyclingBinCleanupHelpTextWarning": "Määritettyä päiväystä vanhemmat tiedostot poistetaan roskakorista automaattisesti.", - "ReplaceIllegalCharacters": "Kiellettyjen merkkien korvaus", + "ReplaceIllegalCharacters": "Korvaa kielletyt merkit", "AirDate": "Esitysaika", "AutoTagging": "Automaattinen tunnistemerkintä", "CloneAutoTag": "Monista automaattimerkintä", @@ -1111,8 +1111,8 @@ "Connection": "Yhteys", "Date": "Päiväys", "DeleteSpecification": "Poista määritys", - "BlackholeFolderHelpText": "Kansio, jossa {appName} säilyttää {extension}-tiedoston.", - "BlackholeWatchFolder": "Valvottu kansio", + "BlackholeFolderHelpText": "Kansio, jonne {appName} tallentaa {extension}-tiedoston.", + "BlackholeWatchFolder": "Valvontakansio", "BlackholeWatchFolderHelpText": "Kansio, josta {appName}in tulee tuoda valmistuneet lataukset.", "AptUpdater": "Asenna päivitys APT-työkalun avulla", "Original": "Alkuperäiset", @@ -1156,7 +1156,7 @@ "Directory": "Kansio", "PendingChangesDiscardChanges": "Hylkää muutokset ja poistu", "RecyclingBinHelpText": "Poistetut tiedostot siirretään tänne pysyvän poiston sijaan.", - "ReleaseSceneIndicatorAssumingTvdb": "Oletuksena TVDB-numerointi.", + "ReleaseSceneIndicatorAssumingTvdb": "Oletuksena TheTVDB-numerointi.", "ReleaseSceneIndicatorMappedNotRequested": "Valittu jakso ei sisältynyt tähän hakuun.", "ReplaceWithSpaceDash": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki\"", "ReplaceWithSpaceDashSpace": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki välilyönti\"", @@ -1219,7 +1219,7 @@ "ColonReplacementFormatHelpText": "Määritä, mitä {appName} tekee tiedostonimien kaksoispisteille.", "CountSelectedFiles": "{selectedCount} tiedostoa on valittu", "DelayProfiles": "Viiveprofiilit", - "DeleteRemotePathMappingMessageText": "Haluatko varmasti poistaa tämä etäsijainnin kartoituksen?", + "DeleteRemotePathMappingMessageText": "Haluatko varmasti poistaa tämän etäsijainnin kohdistuksen?", "Deleted": "Poistettu", "DeletedReasonManual": "Tiedosto poistettiin käyttöliittymän kautta", "DestinationPath": "Kohdesijainti", @@ -1231,7 +1231,7 @@ "DotNetVersion": ".NET", "DownloadClientPneumaticSettingsStrmFolder": "Strm-kansio", "DoNotPrefer": "Älä suosi", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää etuliitteeksi Delugen JSON-URL:n. Ks {url}.", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää etuliitteen Delugen JSON-URL-osoitteeseen (ks. {url}).", "DownloadClientDownloadStationValidationApiVersion": "Download Stationin API-versiota ei tueta. Sen tulee olla vähintään {requiredVersion} (versioita {minVersion}–{maxVersion} tuetaan).", "DownloadClientFloodSettingsRemovalInfo": "{appName} suorittaa torrenttien automaattisen poiston sen tietolähdeastuksissa määritettyjen jakoasetusten perusteella.", "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää etuliitteeksi Flood API:n (esim. {url}).", @@ -1240,10 +1240,10 @@ "MultiEpisode": "Useita jaksoja", "Negated": "Kielletty", "MultiEpisodeInvalidFormat": "Useita jaksoja: virheellinen kaava", - "AutoRedownloadFailedFromInteractiveSearch": "Uudelleenlataus manuaalihausta epäonnistui", + "AutoRedownloadFailedFromInteractiveSearch": "Uudelleenlataus manuaalihaun tuloksista epäonnistui", "Blocklist": "Estolista", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi automaattisesti ja pyri lataamaan eri julkaisu vaikka epäonnistunut julkaisu oli kaapattu manuaalihausta.", - "StandardEpisodeFormat": "Vakiojaksojen kaava", + "StandardEpisodeFormat": "Tavallisten jaksojen kaava", "SceneNumberNotVerified": "Kohtausnumeroa ei ole vielä vahvistettu", "Scene": "Kohtaus", "RssSyncIntervalHelpText": "Aikaväli minuutteina. Arvo \"0\" (nolla) kytkee toiminnon pois käytöstä pysäyttäen automaattisen julkaisukaappauksen täysin.", @@ -1268,7 +1268,7 @@ "ReplaceWithDash": "Korvaa yhdysmerkillä", "ConnectSettingsSummary": "Ilmoitukset, kuten viestintä mediapalvelimille ja soittimille, sekä omat komentosarjat.", "DockerUpdater": "Hanki päivitys päivittämällä Docker-säiliö", - "DownloadClientQbittorrentValidationCategoryRecommended": "Kategoria tulisi määrittää", + "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", @@ -1280,7 +1280,7 @@ "External": "Ulkoinen", "MaintenanceRelease": "Huoltojulkaisu: korjauksia ja muita parannuksia. Lue lisää Githubin muutoshistoriasta.", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Määrittää käytetäänkö qBittorrentista määritettyä rakennetta, torrentin alkuperäistä rakennetta vai luodaanko uusi alikansio (qBittorrent 4.3.2+).", - "EditRemotePathMapping": "Muokkaa kartoitettua etäsijaintia", + "EditRemotePathMapping": "Muokkaa etäsijainnin kohdistusta", "LastUsed": "Viimeksi käytetty", "Manual": "Manuaalinen", "MoveAutomatically": "Siirrä automaattisesti", @@ -1306,7 +1306,7 @@ "Seeders": "Jakajat", "UpgradesAllowed": "Päivitykset sallitaan", "OnUpgrade": "Päivitettäessä", - "UnmappedFilesOnly": "Vain kartoittamattomat tiedostot", + "UnmappedFilesOnly": "Vain kohdistamattomat tiedostot", "Ignored": "Ohitettu", "PreferAndUpgrade": "Suosi ja päivitä", "Failed": "Epäonnistui", @@ -1368,7 +1368,7 @@ "OneSeason": "1 kausi", "OpenBrowserOnStart": "Avaa selain käynnistettäessä", "Profiles": "Profiilit", - "ProxyBypassFilterHelpText": "Käytä aliverkkotunnusten erottimena pilkkua (,) ja jokerimerkkinä tähteä ja pistettä (*.). Esimerkkejä: www.esimerkki.fi,*.esimerkki.fi.", + "ProxyBypassFilterHelpText": "Erota aliverkkotunnukset pilkuilla ja käytä jokerimerkkinä tähteä ja pistettä (*.). Esimerkki: www.esimerkki.fi,*.esimerkki.fi).", "ProxyBadRequestHealthCheckMessage": "Välityspalvelintesti epäonnistui. Tilakoodi {statusCode}", "ProxyFailedToTestHealthCheckMessage": "Välityspalvelintesti epäonnistui: {url}", "Reason": "Syy", @@ -1471,7 +1471,7 @@ "ImportScriptPath": "Tuontikomentosarjan sijainti", "NotificationsAppriseSettingsTags": "Apprisen tunnisteet", "NotificationsAppriseSettingsServerUrlHelpText": "Apprise-palvelimen URL-osoite. SIsällytä myös http(s):// ja portti (tarvittaessa).", - "DownloadClientSettingsUseSslHelpText": "Yhdistä työkaluun \"{clientName}\" SSL-protokollan välityksellä.", + "DownloadClientSettingsUseSslHelpText": "Muodosta {clientName} -yhteys käyttäen salattua yhteyttä.", "NotificationsKodiSettingsCleanLibraryHelpText": "Siivoa kirjasto päivityksen jälkeen.", "NotificationsJoinSettingsDeviceNamesHelpText": "Pilkuin eroteltu listaus täydellisistä tai osittaisista laitenimistä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", "FormatDateTime": "{formattedDate} {formattedTime}", @@ -1502,7 +1502,7 @@ "BlocklistAndSearch": "Estolista ja haku", "BlocklistAndSearchHint": "Etsi korvaavaa kohdetta kun kohde lisätään estolistalle.", "BlocklistOnlyHint": "Lisää estolistalle etsimättä korvaavaa kohdetta.", - "BlocklistReleaseHelpText": "Estää {appName}ia lataamasta tätä julkaisua uudelleen RSS-syötteen tai automaattihaun tulokisista.", + "BlocklistReleaseHelpText": "Estää {appName}ia lataamasta tätä julkaisua uudelleen RSS-syötteen tai automaattihaun tuloksista.", "ChangeCategory": "Vaihda kategoria", "NotificationsPushoverSettingsDevicesHelpText": "Laitenimet, joihin ilmoitukset lähetetään (lähetä kaikkiin jättämällä tyhjäksi).", "NotificationsNtfySettingsTagsEmojisHelpText": "Valinnainen pilkuin eroteltu listaus käytettävistä tunnisteista tai emjeista.", @@ -1533,7 +1533,7 @@ "NotificationsPushoverSettingsRetryHelpText": "Hätäilmoituksen uudelleenyritysten välinen aika.", "NotificationsPushoverSettingsRetry": "Uudelleenyritys", "NotificationsSettingsUpdateLibrary": "Päivitä kirjasto", - "NotificationsSettingsUpdateMapPathsFrom": "Sijaintien lähdekartoitukset", + "NotificationsSettingsUpdateMapPathsFrom": "Kohdista sijainnit lähteeseen", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Vastaanottavan ryhmän ID tai vastaanottajan puhelinnumero.", "NotificationsSignalValidationSslRequired": "Näyttää siltä, että SSL-yhteys vaaditaan", "NotificationsSignalSettingsUsernameHelpText": "Käyttäjätunnus, jolla Signal-API:lle lähetettävät pyynnöt todennetaan.", @@ -1621,7 +1621,7 @@ "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", "NotificationsSettingsUseSslHelpText": "Muodosta yhteys 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": "Sijaintien kohdekartoitukset", + "NotificationsSettingsUpdateMapPathsTo": "Kohdista sijainnit kohteeseen", "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", "NotificationsTraktSettingsAccessToken": "Käyttötunniste", "NotificationsTraktSettingsAuthUser": "Todennettu käyttäjä", @@ -1668,5 +1668,73 @@ "RemoveFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", "RemoveQueueItemRemovalMethod": "Poistotapa", "RemoveQueueItemsRemovalMethodHelpTextWarning": "\"Poista lataustyökalusta\" poistaa lataukset ja niiden tiedostot.", - "Umask": "Umask" + "Umask": "Umask", + "FilterGreaterThan": "on suurempi kuin", + "TheTvdb": "TheTVDB", + "FilterIsNot": "ei ole", + "FilterLessThan": "on pienempi kuin", + "FilterNotEqual": "ei ole sama kuin", + "FilterLessThanOrEqual": "on pienempi kuin tai sama", + "FilterNotInLast": "ei kuluneina", + "FilterNotInNext": "ei seuraavina", + "IndexerValidationJackettNoResultsInConfiguredCategories": "Kysely onnistui, mutta tietolähteesi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "TvdbId": "TheTVDB ID", + "DownloadClientSettingsInitialState": "Virheellinen tila", + "DownloadClientSettingsRecentPriority": "Uusien painotus", + "DownloadClientSettingsUrlBaseHelpText": "Lisää etuliite lataustuökalun {clientName} URL-osoitteeseen, kuten {url}.", + "DownloadClientSettingsInitialStateHelpText": "Lataustyökaluun {clientName} lisättyjen torrentien aloitustila.", + "DownloadClientSettingsPostImportCategoryHelpText": "Kategoria, jonka {appName} asettaa tuonnin jälkeen. {appName} ei poista tämän kategorian torrenteja vaikka jakaminen olisi päättynyt. Säilytä alkuperäinen kategoria jättämällä tyhjäksi.", + "ImportListsImdbSettingsListId": "Listan ID", + "ImportListsImdbSettingsListIdHelpText": "IMDb-listan ID (esim. \"ls12345678\").", + "ImportListsSonarrSettingsRootFoldersHelpText": "Lähdeinstanssin juurikansiot, joista tuodaan.", + "ImportListsTraktSettingsRatingHelpText": "Suodata sarjat arviovälin perusteella (0–100)", + "ImportListsTraktSettingsWatchedListFilter": "Katselulistan suodatin", + "ImportListsTraktSettingsYearsHelpText": "Suodata sarjat vuoden tai vuosivälin perusteella", + "MetadataSettingsEpisodeImages": "Jaksojen kuvat", + "MetadataSettingsEpisodeMetadata": "Jaksojen metatiedot", + "MetadataSettingsSeriesImages": "Sarjojen kuvat", + "MetadataSettingsEpisodeMetadataImageThumbs": "Jaksojen metatietojen pienoiskuvat", + "MetadataSettingsSeasonImages": "Kausien kuvat", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Sisällytä thumb-kuvien tunnisteet <filename>.nfo-tiedostoihin (vaatii \"Jaksojen metatiedot\" -asetuksen).", + "MetadataXmbcSettingsSeriesMetadataHelpText": "Sisällytä sarjojen täydelliset metatiedot tvshow.nfo-tiedostoihin.", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Sisällytä JSON-muotoiset jakso-oppaat tvshow.nfo-tiedostoihin (vaatii \"Sarjan metatiedot\" -asetuksen).", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Sisällytä sarjojen TheTVDB-URL-osoitteet tvshow.nfo-tiedostoihin (voidaan käyttään yhdessä \"Sarjan metatiedot\" -asetuksen kanssa).", + "DownloadClientValidationCategoryMissingDetail": "Syötettyä kategoriaa ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.", + "FailedToLoadTranslationsFromApi": "Käännösten lataus rajapinnasta epäonnistui", + "FailedToLoadUiSettingsFromApi": "Käyttöliittymäasetusten lataus rajapinnasta epäonnistui", + "FilterEqual": "on sama kuin", + "FilterInLast": "kuluneina", + "Mapping": "Kohdistus", + "RemotePathMappingFileRemovedHealthCheckMessage": "Tiedosto \"{path}\" poistettiin kesken käsittelyn.", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "14 päivän sisällä julkaistujen elokuvien kaappauksille käytettävä painotus.", + "FilterEndsWith": "päättyy", + "FilterIsBefore": "on ennen", + "FilterDoesNotStartWith": "päättyy", + "FilterIsAfter": "on jälkeen", + "FilterGreaterThanOrEqual": "on suurempi kuin tai sama", + "FilterDoesNotEndWith": "ei pääty", + "FilterInNext": "seuraavina", + "ImdbId": "IMDb ID", + "MaximumSingleEpisodeAgeHelpText": "Täysiä tuotantokausia etsittäessä hyväksytään vain kausipaketit, joiden uusin jakso on tätä asetusta vanhempi. Koskee vain vakiosarjoja. Poista käytöstä asettamalla arvoksi \"0\" (nolla).", + "FailedToLoadSystemStatusFromApi": "Järjestelmän tilan lataus rajapinnasta epäonnistui", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Sinun on poistettava televisiojärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", + "IndexerValidationJackettNoRssFeedQueryAvailable": "RSS-syötekyselyä ei ole käytettävissä. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sinun on poistettava päiväysjärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Sinun on poistettava elokuvien järjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", + "DownloadClientSettingsCategoryHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa.", + "DownloadClientSettingsDestinationHelpText": "Määrittää manuaalisen tallennuskohteen. Käytä oletusta jättämällä tyhjäksi.", + "DownloadClientSettingsOlderPriority": "Vanhojen painotus", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Yli 14 päivää sitten julkaistujen jaksojen kaappauksille käytettävä painotus.", + "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Traktin lajityyppien Slug-määrityksen perusteella (pilkuin eroteltuna) vain suosituille listoille.", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse sarjatyyppi, jonka haluat tuoda.", + "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja [UKK:sta](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server).", + "MetadataSettingsSeriesMetadata": "Sarjojen metatiedot", + "MetadataSettingsSeriesMetadataUrl": "Sarjojen metatietojen URL", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Sarjojen metatietojen jakso-opas", + "SomeResultsAreHiddenByTheAppliedFilter": "Aktiivinen suodatin piilottaa joitakin tuloksia.", + "ChangeCategoryHint": "Vaihtaa latauksen kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", + "ChangeCategoryMultipleHint": "Vaihtaa latausten kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", + "DownloadClientAriaSettingsDirectoryHelpText": "Valinnainen latuasten tallennussijainti. Käytä Aria2-oletusta jättämällä tyhjäksi.", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrentien jonotus ei ole käytössä qBittorent-asetuksissasi. Ota se käyttöön tai valitse painotukseksi \"Viimeiseksi\".", + "DownloadClientSettingsCategorySubFolderHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa. Tämä luo latauskansioon [kategoria]-alikansion." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index bd6b257cf..2eb48efaf 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -314,7 +314,7 @@ "DeleteSelectedIndexersMessageText": "Voulez-vous vraiment supprimer les {count} indexeur(s) sélectionné(s) ?", "DeleteRootFolder": "Supprimer le dossier racine", "DisabledForLocalAddresses": "Désactivée pour les adresses IP locales", - "DeleteTagMessageText": "Voulez-vous vraiment supprimer l'étiquette « {label} » ?", + "DeleteTagMessageText": "Voulez-vous vraiment supprimer l'étiquette '{label}' ?", "Filter": "Filtrer", "FilterContains": "contient", "FilterIs": "est", @@ -881,7 +881,7 @@ "ReleaseProfileTagSeriesHelpText": "Les profils de version s'appliqueront aux séries avec au moins une balise correspondante. Laisser vide pour appliquer à toutes les séries", "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Le client de téléchargement à distance {downloadClientName} place les téléchargements dans {path} mais ce répertoire ne semble pas exister. Mappage de chemin distant probablement manquant ou incorrect.", "RemoveCompletedDownloads": "Supprimer les téléchargements terminés", - "RemoveQueueItemConfirmation": "Êtes-vous sûr de vouloir supprimer « {sourceTitle} » de la file d'attente ?", + "RemoveQueueItemConfirmation": "Êtes-vous sûr de vouloir retirer '{sourceTitle}' de la file d'attente ?", "RemoveSelectedBlocklistMessageText": "Êtes-vous sûr de vouloir supprimer les éléments sélectionnés de la liste de blocage ?", "RescanAfterRefreshSeriesHelpText": "Analysez à nouveau le dossier de la série après avoir actualisé la série", "RetryingDownloadOn": "Nouvelle tentative de téléchargement le {date} à {time}", @@ -890,7 +890,7 @@ "StandardEpisodeTypeDescription": "Épisodes publiés avec le modèle SxxEyy", "Rejections": "Rejets", "RemoveFromQueue": "Supprimer de la file d'attente", - "RemoveQueueItem": "Supprimer – {sourceTitle}", + "RemoveQueueItem": "Retirer - {sourceTitle}", "Search": "Rechercher", "Seeders": "Seeders", "SelectEpisodesModalTitle": "{modalTitle} – Sélectionnez un ou plusieurs épisodes", @@ -1307,7 +1307,7 @@ "CloneAutoTag": "Cloner la balise automatique", "Failed": "Échoué", "Daily": "Tous les jours", - "ContinuingOnly": "Continuer seulement", + "ContinuingOnly": "Continuant uniquement", "CurrentlyInstalled": "Actuellement installé", "Donations": "Dons", "EpisodeCount": "Nombre d'épisodes", @@ -1907,5 +1907,19 @@ "BlocklistOnly": "Liste de blocage uniquement", "BlocklistOnlyHint": "Liste de blocage sans recherche de remplaçant", "ChangeCategory": "Changer de catégorie", - "ChangeCategoryMultipleHint": "Modifie les téléchargements dans la \"catégorie post-importation\" du client de téléchargement" + "ChangeCategoryMultipleHint": "Modifie les téléchargements dans la \"catégorie post-importation\" du client de téléchargement", + "RemoveFromDownloadClientHint": "Supprime le téléchargement et le(s) fichier(s) du client de téléchargement", + "RemoveMultipleFromDownloadClientHint": "Supprime les téléchargements et les fichiers du client de téléchargement", + "RemoveQueueItemRemovalMethod": "Méthode de suppression", + "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Supprimer du client de téléchargement\" supprimera le téléchargement et le(s) fichier(s) du client de téléchargement.", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "Supprimer du client de téléchargement\" supprimera les téléchargements et les fichiers du client de téléchargement.", + "DoNotBlocklistHint": "Supprimer sans mettre sur liste noire", + "DoNotBlocklist": "Ne pas mettre sur liste noire", + "IgnoreDownloadHint": "Empêche {appName} de poursuivre le traitement de ce téléchargement", + "IgnoreDownload": "Ignorer le téléchargement", + "IgnoreDownloads": "Ignorer les téléchargements", + "IgnoreDownloadsHint": "Empêche {appName} de poursuivre le traitement de ces téléchargements", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrents bloqués lors de la saisie", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 31c90a5b6..727b9c278 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -10,7 +10,7 @@ "ExportCustomFormat": "Egyéni formátum exportálása", "IndexerJackettAllHealthCheckMessage": "A nem támogatott Jackett 'all' végpontot használó indexelők: {indexerNames}", "Remove": "Eltávolítás", - "RemoveFromDownloadClient": "Eltávolítás a letöltési kliensből", + "RemoveFromDownloadClient": "Eltávolítás a Letöltési kliensből", "RemoveSelectedItem": "Kijelölt elem eltávolítása", "RemoveSelectedItemQueueMessageText": "Biztosan el akar távolítani 1 elemet a várólistáról?", "RemoveSelectedItems": "Kijelölt elemek eltávolítása", @@ -21,7 +21,7 @@ "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", + "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", @@ -352,7 +352,7 @@ "Today": "Ma", "TimeFormat": "Idő formátum", "Torrents": "Torrentek", - "TorrentsDisabled": "Kikapcsolt torrentek", + "TorrentsDisabled": "Deaktivált torrentek", "Total": "Összesen", "TotalFileSize": "Összesített fájlméret", "Twitter": "Twitter", @@ -480,7 +480,7 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "A kioldásnak kisebbnek vagy egyenlőnek kell lennie ennél a méretnél", "CustomFormatsSpecificationMinimumSize": "Minimum méret", "CustomFormatsSpecificationMinimumSizeHelpText": "A kibocsátásnak nagyobbnak kell lennie ennél a méretnél", - "CustomFormatsSpecificationRegularExpression": "Nyelv", + "CustomFormatsSpecificationRegularExpression": "Reguláris kifejezés", "CustomFormatsSpecificationSource": "Forrás", "CustomFormatsSpecificationReleaseGroup": "Release Csoport", "CustomFormatsSpecificationResolution": "Felbontás", @@ -627,7 +627,7 @@ "LastWriteTime": "Utolsó írási idő", "LogFiles": "Naplófájlok", "ManualImport": "Kézi importálás", - "MappedNetworkDrivesWindowsService": "A leképezett hálózati meghajtók nem érhetők el, ha Windows szolgáltatásként futnak, lásd a [GYIK](https:", + "MappedNetworkDrivesWindowsService": "A leképezett hálózati meghajtók nem érhetők el, ha Windows szolgáltatásként futnak, lásd a [GYIK](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) for more information.", "MediaManagementSettingsLoadError": "Nem sikerült betölteni a médiakezelési beállításokat", "MediaManagementSettings": "Médiakezelési beállítások", "NoMonitoredEpisodesSeason": "Ebben az évadban nincsenek felügyelt epizódok", @@ -784,7 +784,7 @@ "FilterDoesNotEndWith": "nem ér véget", "Level": "Szint", "LibraryImportTipsQualityInEpisodeFilename": "Győződjön meg arról, hogy a fájlok fájlnevében szerepel a minőség. például. `episode.s02e15.bluray.mkv`", - "LibraryImportTipsSeriesUseRootFolder": "Mutasson a(z) {appName} mappára, amely az összes tévéműsorát tartalmazza, ne egy konkrétat. például. \"`{goodFolderExample}`\" és nem \"`{badFolderExample}`\". Ezenkívül minden sorozatnak saját mappájában kell lennie a gyökérben", + "LibraryImportTipsSeriesUseRootFolder": "Mutasson a(z) {appName} mappára, amely az összes tévéműsorát tartalmazza, ne egy konkrétat. például. \"`{goodFolderExample}`\" és nem \"`{badFolderExample}`\". Ezenkívül minden sorozatnak saját mappájában kell lennie a gyökérben.", "Links": "Linkek", "MetadataSourceSettingsSeriesSummary": "Információ arról, hogy a(z) {appName} honnan szerezheti be a sorozat- és epizódinformációkat", "MonitorAllSeasons": "Minden évad", @@ -901,7 +901,7 @@ "RemotePathMappingHostHelpText": "Ugyanaz a gazdagép, amelyet a távoli letöltési klienshez megadott", "SelectQuality": "Minőség kiválasztása", "SeriesIndexFooterEnded": "Befejeződött (az összes epizód letöltve)", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "A sorozatokkal és epizódokkal kapcsolatos információkat a TheTVDB.com biztosítja. [Kérjük, fontolja meg támogatásukat](https:", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "A sorozatokkal és epizódokkal kapcsolatos információkat a TheTVDB.com biztosítja. [Kérjük, fontolja meg támogatásukat](https://www.thetvdb.com/subscribe).", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} epizódfájl", "SeriesIsMonitored": "A sorozatot figyelik", "SeriesTitle": "Sorozat címe", @@ -972,5 +972,61 @@ "ShowSeriesTitleHelpText": "Mutasd a sorozat címét a plakát alatt", "ShowSizeOnDisk": "Méret megjelenítése a lemezen", "SizeLimit": "Méretkorlát", - "DeleteSelectedDownloadClientsMessageText": "Biztosan törölni szeretné a kiválasztott {count} letöltési klienst?" + "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", + "StartImport": "Indítsa el az Importálást", + "SupportedListsMoreInfo": "Az egyes listákkal kapcsolatos további információkért kattintson a további információ gombokra.", + "TestAllClients": "Minden ügyfél tesztelése", + "UiSettings": "Felület Beállítások", + "Security": "Biztonság", + "SetTags": "Címkék beállítása", + "SslCertPathHelpText": "A pfx fájl elérési útja", + "TestAllLists": "Minden lista tesztelése", + "ToggleUnmonitoredToMonitored": "Nem figyelt, kattintson a figyeléshez", + "UiLanguage": "Felület nyelv", + "UiLanguageHelpText": "Nyelv, amelyet a {appName} a felhasználói felülethez fog használni", + "SelectAll": "Mindet kiválaszt", + "SelectReleaseGroup": "Release csoport kiválasztása", + "SourcePath": "Forrás útvonala", + "TableOptionsButton": "Táblázat opciók gomb", + "TheTvdb": "TheTVDB", + "ToggleMonitoredSeriesUnmonitored ": "Nem lehet átkapcsolni a figyelt állapotot, ha a sorozat nem figyelhető", + "Ui": "Felület", + "UiSettingsLoadError": "Nem sikerült betölteni a felhasználói felület beállításait", + "SelectDropdown": "Válassz...", + "SelectEpisodes": "Epizód(ok) kiválasztása", + "SkipFreeSpaceCheckWhenImportingHelpText": "Akkor használja, ha a(z) {appName} nem tud szabad helyet észlelni a gyökérmappában a fájlimportálás során", + "SkipRedownloadHelpText": "Megakadályozza, hogy {appName} megpróbáljon letölteni egy alternatív kiadást ehhez az elemhez", + "Specials": "Különlegességek", + "SslCertPasswordHelpText": "Jelszó a pfx fájlhoz", + "StandardEpisodeTypeDescription": "SxxEyy mintával megjelent epizódok", + "StopSelecting": "Kiválasztás leállítása", + "SupportedIndexersMoreInfo": "Az egyes indexelőkkel kapcsolatos további információkért kattintson a további információ gombokra.", + "TableColumnsHelpText": "Válassza ki, hogy mely oszlopok legyenek láthatók, és milyen sorrendben jelenjenek meg", + "TvdbIdExcludeHelpText": "A kizárandó sorozat TVDB azonosítója", + "StandardEpisodeTypeFormat": "Évad- és epizódszámok ({format})", + "StartupDirectory": "Indítási könyvtár", + "SubtitleLanguages": "Feliratnyelvek", + "SourceRelativePath": "Forrás relatív elérési útja", + "True": "Igaz", + "TvdbId": "TVDB ID", + "TorrentBlackholeSaveMagnetFilesExtension": "Magnet Files kiterjesztés mentése", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Csak olvasható", + "Trace": "Nyom", + "TagsSettingsSummary": "Tekintse meg az összes címkét és azok felhasználási módját. A fel nem használt címkék eltávolíthatók", + "TestAllIndexers": "Tesztelje az összes indexelőt", + "TaskUserAgentTooltip": "Az API-t hívó alkalmazás által biztosított felhasználói ügynök", + "ThemeHelpText": "Változtassa meg az alkalmazás felhasználói felület témáját, az \"Auto\" téma az operációs rendszer témáját használja a Világos vagy Sötét mód beállításához. A Theme.Park ihlette", + "TablePageSizeHelpText": "Az egyes oldalakon megjelenítendő elemek száma", + "ToggleMonitoredToUnmonitored": "Felügyelt, kattintson a figyelés megszüntetéséhez", + "Standard": "Alapértelmezett", + "StandardEpisodeFormat": "Alapértelmezett epizód formátum", + "StartProcessing": "Indítsa el a feldolgozást", + "SelectFolder": "Mappa kiválasztása", + "ShowUnknownSeriesItemsHelpText": "Sorozat nélküli elemek megjelenítése a sorban, idetartozhatnak az eltávolított sorozatok, filmek vagy bármi más a(z) {appName} kategóriájában" } diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index b35dd0ce5..b763b0c4c 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -7,5 +7,6 @@ "Add": "Legg til", "Absolute": "Absolutt", "Activity": "Aktivitet", - "About": "Om" + "About": "Om", + "CalendarOptions": "Kalenderinnstillinger" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 11c82bb52..221036b0c 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1484,7 +1484,7 @@ "QueueFilterHasNoItems": "O filtro de fila selecionado não possui itens", "BlackholeFolderHelpText": "Pasta na qual {appName} armazenará o arquivo {extension}", "Destination": "Destinação", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL json do deluge, consulte {url}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo aURL json do Deluge, consulte {url}", "DownloadClientDelugeValidationLabelPluginFailure": "Falha na configuração do rótulo", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} não conseguiu adicionar o rótulo ao {clientName}.", "DownloadClientDownloadStationProviderMessage": "{appName} não consegue se conectar ao Download Station se a autenticação de dois fatores estiver habilitada em sua conta DSM", @@ -1496,74 +1496,74 @@ "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração NzbGet KeepHistory deve ser menor que 25.000", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0. O que impede que {appName} veja os downloads concluídos.", "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {clientName}, como {url}", - "DownloadClientSettingsUseSslHelpText": "Use conexão segura ao conectar-se a {clientName}", + "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se a {clientName}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Transmission", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL rpc {clientName}, por exemplo, {url}, o padrão é '{defaultUrl}'", - "DownloadClientValidationAuthenticationFailureDetail": "Por favor, verifique seu nome de usuário e senha. Verifique também se o host que executa {appName} não está impedido de acessar {clientName} pelas limitações da WhiteList na configuração de {clientName}.", + "DownloadClientValidationAuthenticationFailureDetail": "Por favor verifique seu nome de usuário e senha. Verifique também se o host que executa {appName} não está impedido de acessar {clientName} pelas limitações da WhiteList na configuração de {clientName}.", "DownloadClientValidationSslConnectFailureDetail": "{appName} não consegue se conectar a {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar {appName} e {clientName} para não usar SSL.", "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status de descompactação: {unpackStatus} - Status de movimentação: {moveStatus} - Status do script: {scriptStatus} - Status de exclusão: {deleteStatus} - Status de marcação: {markStatus}", "PostImportCategory": "Categoria Pós-Importação", "SecretToken": "Token Secreto", "TorrentBlackhole": "Torrent Blackhole", - "TorrentBlackholeSaveMagnetFiles": "Salvar arquivos magnéticos", - "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnético se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", + "TorrentBlackholeSaveMagnetFiles": "Salvar Arquivos Magnéticos", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnético se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnético salvos em um arquivo)", "UnknownDownloadState": "Estado de download desconhecido: {state}", "UsenetBlackhole": "Usenet Blackhole", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de sementes", - "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desativado para este torrent ou 'Layout de conteúdo do torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", + "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desabilitado para este torrent ou 'Layout de conteúdo de torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "As categorias não são suportadas até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} não poderá realizar o tratamento de download concluído conforme configurado. Você pode corrigir isso no qBittorrent ('Ferramentas - Opções...' no menu) alterando 'Opções - BitTorrent - Limitação da proporção de compartilhamento' de 'Removê-los' para 'Pausá-los'", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} não poderá realizar o tratamento de download concluído conforme configurado. Você pode corrigir isso no qBittorrent ('Ferramentas -> Opções...' no menu) alterando 'Opções -> BitTorrent -> Limitação da proporção de compartilhamento' de 'Removê-los' para 'Pausá-los'", "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o endpoint XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes do download' afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda 'Abortar trabalhos que não podem ser concluídos', pois é mais eficaz.", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a ordenação de filmes para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a classificação de filmes para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefere que cada download tenha uma pasta separada. Com * anexado à pasta/caminho, o Sabnzbd não criará essas pastas de trabalho. Vá para Sabnzbd para consertar.", "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório [categoria] no diretório de saída.", "XmlRpcPath": "Caminho RPC XML", - "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as tags iniciais. Isso evita conflitos com downloads não relacionados.", - "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como tags. As dicas são exemplos.", - "DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta tags após a importação de um download.", - "BlackholeWatchFolder": "Pasta Monitorada", + "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta etiquetas após a importação de um download.", + "BlackholeWatchFolder": "Pasta de Monitoramento", "BlackholeWatchFolderHelpText": "Pasta da qual {appName} deve importar downloads concluídos", "Category": "Categoria", "Directory": "Diretório", "DownloadClientDelugeTorrentStateError": "Deluge está relatando um erro", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin Rotulo habilitado em {clientName} para usar categorias.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin rótulo habilitado no {clientName} para usar categorias.", "DownloadClientDownloadStationSettingsDirectory": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporta de {minVersion} a {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "A pasta não existe", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nenhum destino padrão", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do DownloadStation em BT/HTTP/FTP/NZB - Localização.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do DownloadStation em BT/HTTP/FTP/NZB -> Localização.", "DownloadClientDownloadStationValidationSharedFolderMissing": "A pasta compartilhada não existe", "DownloadClientFloodSettingsAdditionalTags": "Etiquetas Adicionais", "DownloadClientFloodSettingsPostImportTags": "Etiquetas Pós-Importação", - "DownloadClientFloodSettingsRemovalInfo": "{appName} cuidará da remoção automática de torrents com base nos critérios de propagação atuais em Configurações - Indexadores", - "DownloadClientFloodSettingsStartOnAdd": "Comece em Adicionar", + "DownloadClientFloodSettingsRemovalInfo": "{appName} cuidará da remoção automática de torrents com base nos critérios de propagação atuais em Configurações -> Indexadores", + "DownloadClientFloodSettingsStartOnAdd": "Comece ao Adicionar", "DownloadClientFloodSettingsUrlBaseHelpText": "Adiciona um prefixo à API Flood, como {url}", "DownloadClientFreeboxApiError": "A API Freebox retornou um erro: {errorDescription}", "DownloadClientFreeboxAuthenticationError": "A autenticação na API Freebox falhou. Motivo: {errorDescription}", "DownloadClientFreeboxNotLoggedIn": "Não logado", "DownloadClientFreeboxSettingsApiUrl": "URL da API", "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API Freebox com a versão da API, por exemplo, '{url}', o padrão é '{defaultApiUrl}'", - "DownloadClientFreeboxSettingsAppId": "ID do App", - "DownloadClientFreeboxSettingsAppToken": "Token do App", + "DownloadClientFreeboxSettingsAppId": "ID do Aplicativo", + "DownloadClientFreeboxSettingsAppToken": "Token do Aplicativo", "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API Freebox (ou seja, 'app_token')", "DownloadClientFreeboxSettingsHostHelpText": "Nome do host ou endereço IP do host do Freebox, o padrão é '{url}' (só funcionará se estiver na mesma rede)", "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API Freebox. Verifique as configurações de 'Host', 'Porta' ou 'Usar SSL'. (Erro: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "O download contém vários arquivos e não está em uma pasta de trabalho: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos NzbGet versão 16.0", - "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de tag não está ativado", + "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de rótulo não ativado", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "A configuração KeepHistory do NzbGet está muito alta.", "DownloadClientNzbgetValidationKeepHistoryZero": "A configuração KeepHistory do NzbGet deve ser maior que 0", "DownloadClientPneumaticSettingsNzbFolder": "Pasta Nzb", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisará estar acessível no XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeiro e último primeiro", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última peças primeiro (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem sequencial", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeiro e Último Primeiro", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última partes primeiro (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem Sequencial", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixe em ordem sequencial (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções - UI da Web - 'Usar HTTPS em vez de HTTP' no qBittorrent.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções -> UI da Web -> 'Usar HTTPS em vez de HTTP' em qBittorrent.", "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent não pode resolver o link magnético com DHT desativado", "DownloadClientQbittorrentTorrentStateError": "qBittorrent está relatando um erro", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está baixando metadados", @@ -1574,55 +1574,55 @@ "DownloadClientQbittorrentValidationCategoryRecommended": "Categoria é recomendada", "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} não tentará importar downloads concluídos sem uma categoria.", "DownloadClientQbittorrentValidationCategoryUnsupported": "A categoria não é suportada", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila não habilitada", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "O Filas de Torrent não está habilitado nas configurações do qBittorrent. Habilite-o no qBittorrent ou selecione ‘Último’ como prioridade.", - "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de seed. {appName} lidará com a remoção automática de torrents com base nos critérios de propagação atuais em Configurações - Indexadores somente quando a remoção concluída estiver ativada.", - "DownloadClientRTorrentSettingsAddStopped": "Adicionar parado", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "A ativação adicionará torrents e magnets ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila Não Habilitada", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "O Fila de Torrent não está habilitado nas configurações do qBittorrent. Habilite-o no qBittorrent ou selecione ‘Último’ como prioridade.", + "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de seed. {appName} lidará com a remoção automática de torrents com base nos critérios de propagação atuais em Configurações->Indexadores somente quando Remover Concluído estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização rTorrent, que pode ser usada em scripts rTorrent para personalizar o comportamento.", + "DownloadClientRTorrentSettingsAddStopped": "Adicionar Parado", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "A ativação adicionará torrents e ímãs ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", "DownloadClientRTorrentSettingsUrlPath": "Caminho da URL", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção ‘Verificar antes do download’ no Sabnbzd", "DownloadClientSabnzbdValidationDevelopVersion": "Versão de desenvolvimento do Sabnzbd, assumindo a versão 3.0.0 ou superior.", "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} pode não ser compatível com novos recursos adicionados ao SABnzbd ao executar versões de desenvolvimento.", - "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desabilitar ordenação por data", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a ordenação por data para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", - "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar Ordenação de Filmes", - "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar Ordenação para TV", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desativar a ordenação de TV para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desativar Classificação por Data", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a classificação por data para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar Classificação de Filmes", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar Classificação de TV", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desativar a classificação de TV para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", "DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar pastas de trabalho", - "DownloadClientSabnzbdValidationUnknownVersion": "Versão desconhecida: {rawVersion}", + "DownloadClientSabnzbdValidationUnknownVersion": "Versão Desconhecida: {rawVersion}", "DownloadClientSettingsAddPaused": "Adicionar Pausado", "DownloadClientSettingsCategoryHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado.", "DownloadClientSettingsDestinationHelpText": "Especifica manualmente o destino do download, deixe em branco para usar o padrão", "DownloadClientSettingsInitialState": "Estado Inicial", - "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados ao {clientName}", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados a {clientName}", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar há mais de 14 dias", "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para {appName} definir após importar o download. {appName} não removerá torrents nessa categoria mesmo que a propagação seja concluída. Deixe em branco para manter a mesma categoria.", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar nos últimos 14 dias", - "DownloadClientSettingsOlderPriority": "Prioridade para os mais antigos", - "DownloadClientSettingsRecentPriority": "Prioridade para os mais recentes", + "DownloadClientSettingsOlderPriority": "Priorizar Mais Antigos", + "DownloadClientSettingsRecentPriority": "Priorizar Recentes", "DownloadClientUTorrentTorrentStateError": "uTorrent está relatando um erro", "DownloadClientValidationApiKeyIncorrect": "Chave de API incorreta", "DownloadClientValidationApiKeyRequired": "Chave de API necessária", - "DownloadClientValidationAuthenticationFailure": "Falha de autenticação", + "DownloadClientValidationAuthenticationFailure": "Falha de Autenticação", "DownloadClientValidationCategoryMissing": "A categoria não existe", - "DownloadClientValidationCategoryMissingDetail": "A categoria inserida não existe em {clientName}. Crie-a primeiro em {clientName}.", + "DownloadClientValidationCategoryMissingDetail": "A categoria inserida não existe em {clientName}. Crie-o primeiro em {clientName}.", "DownloadClientValidationErrorVersion": "A versão de {clientName} deve ser pelo menos {requiredVersion}. A versão informada é {reportedVersion}", "DownloadClientValidationGroupMissing": "O grupo não existe", - "DownloadClientValidationGroupMissingDetail": "O grupo inserido não existe em {clientName}. Crie-a primeiro em {clientName}.", + "DownloadClientValidationGroupMissingDetail": "O grupo inserido não existe em {clientName}. Crie-o primeiro em {clientName}.", "DownloadClientValidationSslConnectFailure": "Não é possível conectar através de SSL", "DownloadClientValidationTestNzbs": "Falha ao obter a lista de NZBs: {exceptionMessage}", "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: {exception}", + "DownloadClientValidationUnknownException": "Exceção desconhecida: {exceção}", "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.", "DownloadStationStatusExtracting": "Extraindo: {progress}%", "TorrentBlackholeSaveMagnetFilesExtension": "Salvar extensão de arquivos magnéticos", "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensão a ser usada para links magnéticos, o padrão é '.magnet'", - "TorrentBlackholeSaveMagnetFilesReadOnly": "Somente para Leitura", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Só Leitura", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Em vez de mover arquivos, isso instruirá {appName} a copiar ou vincular (dependendo das configurações/configuração do sistema)", "TorrentBlackholeTorrentFolder": "Pasta do Torrent", "UseSsl": "Usar SSL", @@ -2029,5 +2029,14 @@ "BlocklistAndSearch": "Lista de Bloqueio e Pesquisa", "BlocklistAndSearchHint": "Inicie uma busca por um substituto após adicionar a lista de bloqueio", "BlocklistOnlyHint": "Adiciona a Lista de bloqueio sem procurar um substituto", - "IgnoreDownload": "Ignorar Download" + "IgnoreDownload": "Ignorar Download", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais", + "KeepAndTagSeries": "Manter e Etiquetar Séries", + "KeepAndUnmonitorSeries": "Manter e Desmonitorar Séries", + "ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se elas caírem ou não aparecerem na(s) sua(s) lista(s)", + "ListSyncTag": "Etiqueta de Sincronização de Lista", + "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", + "LogOnly": "Só Registro", + "UnableToLoadListOptions": "Não foi possível carregar as opções da lista", + "CleanLibraryLevel": "Limpar Nível da Biblioteca" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index cab8c997a..0f30d9911 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -21,5 +21,22 @@ "AddCustomFilter": "Додати власний фільтр", "AddCustomFormat": "Додати власний формат", "AddDelayProfile": "Додати Профіль Затримки", - "AddCustomFormatError": "Неможливо додати новий власний формат, спробуйте ще раз." + "AddCustomFormatError": "Неможливо додати новий власний формат, спробуйте ще раз.", + "AddListExclusion": "Додати виняток зі списку", + "AbsoluteEpisodeNumbers": "Загальні номери епізодів", + "AbsoluteEpisodeNumber": "Загальний номер епізоду", + "AddImportList": "Додати список імпорту", + "AddIndexer": "Додати Індексер", + "AddNew": "Додати", + "AddNewRestriction": "Додати нове обмеження", + "AddNewSeries": "Додати новий серіал", + "AddDownloadClientImplementation": "Додати клієнт завантаження - {implementationName}", + "AddImportListExclusionError": "Не вдалось додати виняток нового списку імпорту, спробуйте ще раз.", + "AddImportListImplementation": "Додати список імпорту - {implementationName}", + "AddIndexerError": "Неможливо додати новий індексер, спробуйте ще раз.", + "AddIndexerImplementation": "Додати індексер - {implementationName}", + "AddList": "Додати список", + "AddListError": "Неможливо додати новий список, спробуйте ще раз.", + "AddListExclusionError": "Неможливо додати новий виняток зі списку, спробуйте ще раз.", + "AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків" } From 19db75b36beaa5e549d903b136dbda300f1f8562 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 25 Jan 2024 17:07:41 -0800 Subject: [PATCH 077/762] Add max token length (including ellipsis) for some tokens New: Accept ':##' on renaming tokens to allow specifying a maximum length for series, episode titles and release group Closes #6416 --- .../StringExtensionTests/ReverseFixture.cs | 17 ++++ .../Extensions/StringExtensions.cs | 9 ++ .../TruncatedReleaseGroupFixture.cs | 94 +++++++++++++++++++ .../TruncatedSeriesTitleFixture.cs | 57 +++++++++++ .../Proxies/DownloadStationInfoProxy.cs | 2 +- .../Organizer/FileNameBuilder.cs | 68 ++++++++++---- 6 files changed, 229 insertions(+), 18 deletions(-) create mode 100644 src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ReverseFixture.cs create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedReleaseGroupFixture.cs create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedSeriesTitleFixture.cs diff --git a/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ReverseFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ReverseFixture.cs new file mode 100644 index 000000000..27a73cc4b --- /dev/null +++ b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ReverseFixture.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests +{ + [TestFixture] + public class ReverseFixture + { + [TestCase("input", "tupni")] + [TestCase("racecar", "racecar")] + public void should_reverse_string(string input, string expected) + { + input.Reverse().Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 75e5462f4..8a4d140f7 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -242,5 +242,14 @@ namespace NzbDrone.Common.Extensions { return input.Contains(':') ? $"[{input}]" : input; } + + public static string Reverse(this string text) + { + var array = text.ToCharArray(); + + Array.Reverse(array); + + return new string(array); + } } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedReleaseGroupFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedReleaseGroupFixture.cs new file mode 100644 index 000000000..266b39332 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedReleaseGroupFixture.cs @@ -0,0 +1,94 @@ +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 TruncatedReleaseGroupFixture : CoreTest<FileNameBuilder> + { + private Series _series; + private List<Episode> _episodes; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder<Series> + .CreateNew() + .With(s => s.Title = "Series Title") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.MultiEpisodeStyle = 0; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock<INamingConfigService>() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _episodes = new List<Episode> + { + Builder<Episode>.CreateNew() + .With(e => e.Title = "Episode Title 1") + .With(e => e.SeasonNumber = 1) + .With(e => e.EpisodeNumber = 1) + .Build() + }; + + _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" }; + + Mocker.GetMock<IQualityDefinitionService>() + .Setup(v => v.Get(Moq.It.IsAny<Quality>())) + .Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock<ICustomFormatService>() + .Setup(v => v.All()) + .Returns(new List<CustomFormat>()); + } + + private void GivenProper() + { + _episodeFile.Quality.Revision.Version = 2; + } + + [Test] + public void should_truncate_from_beginning() + { + _series.Title = "The Fantastic Life of Mr. Sisko"; + + _episodeFile.Quality.Quality = Quality.Bluray1080p; + _episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala"; + _episodes = _episodes.Take(1).ToList(); + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:12}"; + + var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv"); + result.Length.Should().BeLessOrEqualTo(255); + result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-IWishIWas....mkv"); + } + + [Test] + public void should_truncate_from_from_end() + { + _series.Title = "The Fantastic Life of Mr. Sisko"; + + _episodeFile.Quality.Quality = Quality.Bluray1080p; + _episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala"; + _episodes = _episodes.Take(1).ToList(); + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:-17}"; + + var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv"); + result.Length.Should().BeLessOrEqualTo(255); + result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-...ASixFourImpala.mkv"); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedSeriesTitleFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedSeriesTitleFixture.cs new file mode 100644 index 000000000..993af1ce1 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedSeriesTitleFixture.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +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 TruncatedSeriesTitleFixture : CoreTest<FileNameBuilder> + { + private Series _series; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder<Series> + .CreateNew() + .With(s => s.Title = "Series Title") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.MultiEpisodeStyle = 0; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock<INamingConfigService>() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock<IQualityDefinitionService>() + .Setup(v => v.Get(Moq.It.IsAny<Quality>())) + .Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock<ICustomFormatService>() + .Setup(v => v.All()) + .Returns(new List<CustomFormat>()); + } + + [TestCase("{Series Title:16}", "The Fantastic...")] + [TestCase("{Series TitleThe:17}", "Fantastic Life...")] + [TestCase("{Series CleanTitle:-13}", "...Mr. Sisko")] + public void should_truncate_series_title(string format, string expected) + { + _series.Title = "The Fantastic Life of Mr. Sisko"; + _namingConfig.SeriesFolderFormat = format; + + var result = Subject.GetSeriesFolder(_series, _namingConfig); + result.Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs index 6b9b24847..5fe44ddfd 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 791469fd6..57ebfc5ca 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -315,6 +315,7 @@ namespace NzbDrone.Core.Organizer folderName = CleanFolderName(folderName); folderName = ReplaceReservedDeviceNames(folderName); + folderName = folderName.Replace("{ellipsis}", "..."); return folderName; } @@ -337,6 +338,7 @@ namespace NzbDrone.Core.Organizer folderName = CleanFolderName(folderName); folderName = ReplaceReservedDeviceNames(folderName); + folderName = folderName.Replace("{ellipsis}", "..."); return folderName; } @@ -492,19 +494,19 @@ namespace NzbDrone.Core.Organizer private void AddSeriesTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series) { - tokenHandlers["{Series Title}"] = m => series.Title; - tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); - tokenHandlers["{Series TitleYear}"] = m => TitleYear(series.Title, series.Year); - tokenHandlers["{Series CleanTitleYear}"] = m => CleanTitle(TitleYear(series.Title, series.Year)); - tokenHandlers["{Series TitleWithoutYear}"] = m => TitleWithoutYear(series.Title); - tokenHandlers["{Series CleanTitleWithoutYear}"] = m => CleanTitle(TitleWithoutYear(series.Title)); - tokenHandlers["{Series TitleThe}"] = m => TitleThe(series.Title); - tokenHandlers["{Series CleanTitleThe}"] = m => CleanTitleThe(series.Title); - tokenHandlers["{Series TitleTheYear}"] = m => TitleYear(TitleThe(series.Title), series.Year); - tokenHandlers["{Series CleanTitleTheYear}"] = m => CleanTitleTheYear(series.Title, series.Year); - tokenHandlers["{Series TitleTheWithoutYear}"] = m => TitleWithoutYear(TitleThe(series.Title)); - tokenHandlers["{Series CleanTitleTheWithoutYear}"] = m => CleanTitleThe(TitleWithoutYear(series.Title)); - tokenHandlers["{Series TitleFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(series.Title)); + tokenHandlers["{Series Title}"] = m => Truncate(series.Title, m.CustomFormat); + tokenHandlers["{Series CleanTitle}"] = m => Truncate(CleanTitle(series.Title), m.CustomFormat); + tokenHandlers["{Series TitleYear}"] = m => Truncate(TitleYear(series.Title, series.Year), m.CustomFormat); + tokenHandlers["{Series CleanTitleYear}"] = m => Truncate(CleanTitle(TitleYear(series.Title, series.Year)), m.CustomFormat); + tokenHandlers["{Series TitleWithoutYear}"] = m => Truncate(TitleWithoutYear(series.Title), m.CustomFormat); + tokenHandlers["{Series CleanTitleWithoutYear}"] = m => Truncate(CleanTitle(TitleWithoutYear(series.Title)), m.CustomFormat); + tokenHandlers["{Series TitleThe}"] = m => Truncate(TitleThe(series.Title), m.CustomFormat); + tokenHandlers["{Series CleanTitleThe}"] = m => Truncate(CleanTitleThe(series.Title), m.CustomFormat); + tokenHandlers["{Series TitleTheYear}"] = m => Truncate(TitleYear(TitleThe(series.Title), series.Year), m.CustomFormat); + tokenHandlers["{Series CleanTitleTheYear}"] = m => Truncate(CleanTitleTheYear(series.Title, series.Year), m.CustomFormat); + tokenHandlers["{Series TitleTheWithoutYear}"] = m => Truncate(TitleWithoutYear(TitleThe(series.Title)), m.CustomFormat); + tokenHandlers["{Series CleanTitleTheWithoutYear}"] = m => Truncate(CleanTitleThe(TitleWithoutYear(series.Title)), m.CustomFormat); + tokenHandlers["{Series TitleFirstCharacter}"] = m => Truncate(TitleFirstCharacter(TitleThe(series.Title)), m.CustomFormat); tokenHandlers["{Series Year}"] = m => series.Year.ToString(); } @@ -659,15 +661,15 @@ namespace NzbDrone.Core.Organizer private void AddEpisodeTitleTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, List<Episode> episodes, int maxLength) { - tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(GetEpisodeTitles(episodes), "+", maxLength); - tokenHandlers["{Episode CleanTitle}"] = m => GetEpisodeTitle(GetEpisodeTitles(episodes).Select(CleanTitle).ToList(), "and", maxLength); + tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(GetEpisodeTitles(episodes), "+", maxLength, m.CustomFormat); + tokenHandlers["{Episode CleanTitle}"] = m => GetEpisodeTitle(GetEpisodeTitles(episodes).Select(CleanTitle).ToList(), "and", maxLength, m.CustomFormat); } private void AddEpisodeFileTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile, bool useCurrentFilenameAsFallback) { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + tokenHandlers["{Release Group}"] = m => Truncate(episodeFile.ReleaseGroup, m.CustomFormat) ?? m.DefaultValue("Sonarr"); } private void AddQualityTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series, EpisodeFile episodeFile) @@ -1046,8 +1048,15 @@ namespace NzbDrone.Core.Organizer return titles; } - private string GetEpisodeTitle(List<string> titles, string separator, int maxLength) + private string GetEpisodeTitle(List<string> titles, string separator, int maxLength, string formatter) { + var maxFormatterLength = GetMaxLengthFromFormatter(formatter); + + if (maxFormatterLength > 0) + { + maxLength = Math.Min(maxLength, maxFormatterLength); + } + separator = $" {separator.Trim()} "; var joined = string.Join(separator, titles); @@ -1144,6 +1153,7 @@ namespace NzbDrone.Core.Organizer var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance); tokenHandlers["{Episode Title}"] = m => string.Empty; tokenHandlers["{Episode CleanTitle}"] = m => string.Empty; + tokenHandlers["{ellipsis}"] = m => "..."; var result = ReplaceTokens(pattern, tokenHandlers, namingConfig); @@ -1202,6 +1212,30 @@ namespace NzbDrone.Core.Organizer return result.TrimStart(' ', '.').TrimEnd(' '); } + + private string Truncate(string input, string formatter) + { + var maxLength = GetMaxLengthFromFormatter(formatter); + + if (maxLength == 0 || input.Length <= Math.Abs(maxLength)) + { + return input; + } + + if (maxLength < 0) + { + return $"{{ellipsis}}{input.Reverse().Truncate(Math.Abs(maxLength) - 3).TrimEnd(' ', '.').Reverse()}"; + } + + return $"{input.Truncate(maxLength - 3).TrimEnd(' ', '.')}{{ellipsis}}"; + } + + private int GetMaxLengthFromFormatter(string formatter) + { + int.TryParse(formatter, out var maxCustomLength); + + return maxCustomLength; + } } internal sealed class TokenMatch From d7aea82e45a7c5fec9e72b534fc4c9fb8654c519 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:56:08 -0600 Subject: [PATCH 078/762] Improve Release Grabbing & Failure Logging --- .../Download/ProcessDownloadDecisions.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 640e67582..b59812d69 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -196,31 +196,30 @@ namespace NzbDrone.Core.Download private async Task<ProcessedDecisionResult> ProcessDecisionInternal(DownloadDecision decision, int? downloadClientId = null) { var remoteEpisode = decision.RemoteEpisode; + var remoteIndexer = remoteEpisode.Release.Indexer; try { - _logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteEpisode.Release.Indexer, remoteEpisode.Release.IndexerPriority); + _logger.Trace("Grabbing release '{0}' from Indexer {1} at priority {2}.", remoteEpisode, remoteIndexer, remoteEpisode.Release.IndexerPriority); await _downloadService.DownloadReport(remoteEpisode, downloadClientId); return ProcessedDecisionResult.Grabbed; } catch (ReleaseUnavailableException) { - _logger.Warn("Failed to download release from indexer, no longer available. " + remoteEpisode); + _logger.Warn("Failed to download release '{0}' from Indexer {1}. Release not available", remoteEpisode, remoteIndexer); return ProcessedDecisionResult.Rejected; } catch (Exception ex) { if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException) { - _logger.Debug(ex, - "Failed to send release to download client, storing until later. " + remoteEpisode); - + _logger.Debug(ex, "Failed to send release '{0}' from Indexer {1} to download client, storing until later.", remoteEpisode, remoteIndexer); return ProcessedDecisionResult.Failed; } else { - _logger.Warn(ex, "Couldn't add report to download queue. " + remoteEpisode); + _logger.Warn(ex, "Couldn't add release '{0}' from Indexer {1} to download queue.", remoteEpisode, remoteIndexer); return ProcessedDecisionResult.Skipped; } } From 8f0514a91d141ba8cd314205dda30eedd1444b1d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jan 2024 20:49:46 -0800 Subject: [PATCH 079/762] Fixed: Grouped calendar events not correctly showing as downloading Closes #6441 --- frontend/src/Calendar/Events/CalendarEventGroupConnector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js index dbd967784..dca227a85 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js +++ b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js @@ -10,7 +10,7 @@ function createIsDownloadingSelector() { (state) => state.queue.details, (episodeIds, details) => { return details.items.some((item) => { - return item.episode && episodeIds.includes(item.episode.id); + return !!(item.episodeId && episodeIds.includes(item.episodeId)); }); } ); From e66c6282416c9e99abc5e50b6d8b8219c6359df2 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 1 Feb 2024 04:34:17 +0100 Subject: [PATCH 080/762] Update some translation keys --- .../Settings/Profiles/Delay/EditDelayProfileModalContent.js | 2 +- .../Download/Clients/Transmission/TransmissionSettings.cs | 3 +-- src/NzbDrone.Core/Localization/Core/en.json | 5 +++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js index 38cc33559..a8ecb86f7 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -88,7 +88,7 @@ function EditDelayProfileModalContent(props) { { !isFetching && !!error ? <div> - {translate('AddQualityProfileError')} + {translate('AddDelayProfileError')} </div> : null } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 7140c0f4c..9f550084b 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -45,11 +45,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Transmission")] [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/rpc")] [FieldToken(TokenField.HelpText, "UrlBase", "defaultUrl", "/transmission/")] - public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 153a894cd..f35981026 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -18,6 +18,7 @@ "AddCustomFormat": "Add Custom Format", "AddCustomFormatError": "Unable to add a new custom format, please try again.", "AddDelayProfile": "Add Delay Profile", + "AddDelayProfileError": "Unable to add a new delay profile, please try again.", "AddDownloadClient": "Add Download Client", "AddDownloadClientError": "Unable to add a new download client, please try again.", "AddDownloadClientImplementation": "Add Download Client - {implementationName}", @@ -767,8 +768,8 @@ "IconForSpecials": "Icon for Specials", "IconForSpecialsHelpText": "Show icon for special episodes (season 0)", "IgnoreDownload": "Ignore Download", - "IgnoreDownloads": "Ignore Downloads", "IgnoreDownloadHint": "Stops {appName} from processing this download further", + "IgnoreDownloads": "Ignore Downloads", "IgnoreDownloadsHint": "Stops {appName} from processing these downloads further", "Ignored": "Ignored", "IgnoredAddresses": "Ignored Addresses", @@ -1624,9 +1625,9 @@ "RemoveFromQueue": "Remove from queue", "RemoveMultipleFromDownloadClientHint": "Removes downloads and files from download client", "RemoveQueueItem": "Remove - {sourceTitle}", - "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.", "RemoveQueueItemConfirmation": "Are you sure you want to remove '{sourceTitle}' from the queue?", "RemoveQueueItemRemovalMethod": "Removal Method", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.", "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.", "RemoveRootFolder": "Remove root folder", "RemoveSelected": "Remove Selected", From e17655c26ad5b6d77a4adffc3fe98435886ba1a9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:34:51 +0200 Subject: [PATCH 081/762] Fixed: Notifications with only On Series Add enabled being labeled as disabled --- .../src/Settings/Notifications/Notifications/Notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index 8fb4230e0..e7de0b308 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -191,7 +191,7 @@ class Notification extends Component { } { - !onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ? + !onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ? <Label kind={kinds.DISABLED} outline={true} From e1c6722aad1e35a1e2371bc47f399b57e4f96f27 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 1 Feb 2024 04:35:21 +0100 Subject: [PATCH 082/762] New: Ignore 'Other' subfolder when scanning disk Closes #6437 --- .../MediaFiles/DiskScanServiceTests/ScanFixture.cs | 1 + src/NzbDrone.Core/MediaFiles/DiskScanService.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index d3b8c000f..da3bcef4b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -186,6 +186,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Path.Combine(_series.Path, "Scenes", "file6.mkv").AsOsAgnostic(), Path.Combine(_series.Path, "Shorts", "file7.mkv").AsOsAgnostic(), Path.Combine(_series.Path, "Trailers", "file8.mkv").AsOsAgnostic(), + Path.Combine(_series.Path, "Other", "file9.mkv").AsOsAgnostic(), Path.Combine(_series.Path, "Series Title S01E01 (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(), }); diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 4c92b0d93..999d4d067 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -69,7 +69,7 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - private static readonly Regex ExcludedExtrasSubFolderRegex = new Regex(@"(?:\\|\/|^)(?:extras|extrafanart|behind the scenes|deleted scenes|featurettes|interviews|scenes|samples|shorts|trailers)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ExcludedExtrasSubFolderRegex = new Regex(@"(?:\\|\/|^)(?:extras|extrafanart|behind the scenes|deleted scenes|featurettes|interviews|other|scenes|samples|shorts|trailers)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:@eadir|\.@__thumb|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedExtraFilesRegex = new Regex(@"(-(trailer|other|behindthescenes|deleted|featurette|interview|scene|short)\.[^.]+$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$", RegexOptions.Compiled | RegexOptions.IgnoreCase); From ded7c3c6e2459f041297d479c788febc5d061854 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:36:21 +0200 Subject: [PATCH 083/762] Only bind shortcut for pending changes confirmation when it's shown --- frontend/src/Settings/PendingChangesModal.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js index 4cb83e8f6..213445c65 100644 --- a/frontend/src/Settings/PendingChangesModal.js +++ b/frontend/src/Settings/PendingChangesModal.js @@ -15,12 +15,17 @@ function PendingChangesModal(props) { isOpen, onConfirm, onCancel, - bindShortcut + bindShortcut, + unbindShortcut } = props; useEffect(() => { - bindShortcut('enter', onConfirm); - }, [bindShortcut, onConfirm]); + if (isOpen) { + bindShortcut('enter', onConfirm); + + return () => unbindShortcut('enter', onConfirm); + } + }, [bindShortcut, unbindShortcut, isOpen, onConfirm]); return ( <Modal @@ -61,7 +66,8 @@ PendingChangesModal.propTypes = { kind: PropTypes.oneOf(kinds.all), onConfirm: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, - bindShortcut: PropTypes.func.isRequired + bindShortcut: PropTypes.func.isRequired, + unbindShortcut: PropTypes.func.isRequired }; PendingChangesModal.defaultProps = { From e2210228b34a4d98ef64965e810689d39733734e Mon Sep 17 00:00:00 2001 From: Alex Herbig <aherbigs@gmail.com> Date: Wed, 31 Jan 2024 22:36:39 -0500 Subject: [PATCH 084/762] New: Add RZeroX to release group parsing exceptions --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index fc588106b..b2d13a262 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -83,6 +83,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")] [TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")] [TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")] + [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 99b26355b..836101e14 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -521,7 +521,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)(?=\]|\)))", 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); From 42b11528b4699b8343887185c93a02b139192d83 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 26 Jan 2024 21:03:05 -0800 Subject: [PATCH 085/762] New: Improve multi-language negate Custom Format Closes #6408 --- .../MultiLanguageFixture.cs | 71 ++++++++++++++++ .../OriginalLanguageFixture.cs | 80 +++++++++++++++++++ .../SingleLanguageFixture.cs | 70 ++++++++++++++++ .../CustomFormatSpecificationBase.cs | 2 +- .../Specifications/LanguageSpecification.cs | 19 +++++ 5 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs create mode 100644 src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs create mode 100644 src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs new file mode 100644 index 000000000..5527610ed --- /dev/null +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification +{ + [TestFixture] + public class MultiLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification> + { + private CustomFormatInput _input; + + [SetUp] + public void Setup() + { + _input = new CustomFormatInput + { + EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(), + Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(), + Size = 100.Megabytes(), + Languages = new List<Language> + { + Language.English, + Language.French + }, + Filename = "Series.Title.S01E01" + }; + } + + [Test] + public void should_match_one_language() + { + Subject.Value = Language.French.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + + [Test] + public void should_not_match_different_language() + { + Subject.Value = Language.Spanish.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_not_match_negated_when_one_language_matches() + { + Subject.Value = Language.French.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_not_match_negated_when_all_languages_do_not_match() + { + Subject.Value = Language.Spanish.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs new file mode 100644 index 000000000..33f2b65fc --- /dev/null +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification +{ + [TestFixture] + public class OriginalLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification> + { + private CustomFormatInput _input; + + [SetUp] + public void Setup() + { + _input = new CustomFormatInput + { + EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(), + Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(), + Size = 100.Megabytes(), + Languages = new List<Language> + { + Language.French + }, + Filename = "Series.Title.S01E01" + }; + } + + public void GivenLanguages(params Language[] languages) + { + _input.Languages = languages.ToList(); + } + + [Test] + public void should_match_same_single_language() + { + GivenLanguages(Language.English); + + Subject.Value = Language.Original.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + + [Test] + public void should_not_match_different_single_language() + { + Subject.Value = Language.Original.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_not_match_negated_same_single_language() + { + GivenLanguages(Language.English); + + Subject.Value = Language.Original.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_match_negated_different_single_language() + { + Subject.Value = Language.Original.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs new file mode 100644 index 000000000..6718057a2 --- /dev/null +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification +{ + [TestFixture] + public class SingleLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification> + { + private CustomFormatInput _input; + + [SetUp] + public void Setup() + { + _input = new CustomFormatInput + { + EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(), + Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(), + Size = 100.Megabytes(), + Languages = new List<Language> + { + Language.French + }, + Filename = "Series.Title.S01E01" + }; + } + + [Test] + public void should_match_same_language() + { + Subject.Value = Language.French.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + + [Test] + public void should_not_match_different_language() + { + Subject.Value = Language.Spanish.Id; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_not_match_negated_same_language() + { + Subject.Value = Language.French.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } + + [Test] + public void should_match_negated_different_language() + { + Subject.Value = Language.Spanish.Id; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs index 7b2e2c0a3..fa0dbb80e 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats public abstract NzbDroneValidationResult Validate(); - public bool IsSatisfiedBy(CustomFormatInput input) + public virtual bool IsSatisfiedBy(CustomFormatInput input) { var match = IsSatisfiedByWithoutNegate(input); diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs index fcd5f5374..d841b7053 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs @@ -30,6 +30,16 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] public int Value { get; set; } + public override bool IsSatisfiedBy(CustomFormatInput input) + { + if (Negate) + { + return IsSatisfiedByWithNegate(input); + } + + return IsSatisfiedByWithoutNegate(input); + } + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { var comparedLanguage = input.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown @@ -39,6 +49,15 @@ namespace NzbDrone.Core.CustomFormats return input.Languages?.Contains(comparedLanguage) ?? false; } + private bool IsSatisfiedByWithNegate(CustomFormatInput input) + { + var comparedLanguage = input.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown + ? input.Series.OriginalLanguage + : (Language)Value; + + return !input.Languages?.Contains(comparedLanguage) ?? false; + } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); From c5a724f14eec20acf565ac3a036944191b30cab0 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 1 Feb 2024 04:38:51 +0100 Subject: [PATCH 086/762] New: Send 'On Manual Interaction Required' notifications in more cases Closes #6448 --- .../Download/CompletedDownloadService.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 3fd5008cb..5f4f94938 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -96,6 +96,8 @@ namespace NzbDrone.Core.Download if (series == null) { trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes."); + SendManualInteractionRequiredNotification(trackedDownload); + return; } @@ -106,16 +108,7 @@ namespace NzbDrone.Core.Download if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch) { trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details."); - - if (!trackedDownload.HasNotifiedManualInteractionRequired) - { - trackedDownload.HasNotifiedManualInteractionRequired = true; - - var releaseInfo = new GrabbedReleaseInfo(grabbedHistories); - var manualInteractionEvent = new ManualInteractionRequiredEvent(trackedDownload, releaseInfo); - - _eventAggregator.PublishEvent(manualInteractionEvent); - } + SendManualInteractionRequiredNotification(trackedDownload); return; } @@ -136,6 +129,8 @@ namespace NzbDrone.Core.Download if (trackedDownload.RemoteEpisode == null) { trackedDownload.Warn("Unable to parse download, automatic import is not possible."); + SendManualInteractionRequiredNotification(trackedDownload); + return; } @@ -192,6 +187,7 @@ namespace NzbDrone.Core.Download if (statusMessages.Any()) { trackedDownload.Warn(statusMessages.ToArray()); + SendManualInteractionRequiredNotification(trackedDownload); } } @@ -258,6 +254,21 @@ namespace NzbDrone.Core.Download return false; } + private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload) + { + if (!trackedDownload.HasNotifiedManualInteractionRequired) + { + var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList(); + + trackedDownload.HasNotifiedManualInteractionRequired = true; + + var releaseInfo = grabbedHistories.Count > 0 ? new GrabbedReleaseInfo(grabbedHistories) : null; + var manualInteractionEvent = new ManualInteractionRequiredEvent(trackedDownload, releaseInfo); + + _eventAggregator.PublishEvent(manualInteractionEvent); + } + } + private void SetImportItem(TrackedDownload trackedDownload) { trackedDownload.ImportItem = _provideImportItemService.ProvideImportItem(trackedDownload.DownloadItem, trackedDownload.ImportItem); From 200396ef7a62af65dd7859006e65037efe302d3f Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 1 Feb 2024 03:34:22 +0000 Subject: [PATCH 087/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/pt_BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 221036b0c..241ee3cfb 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -605,7 +605,7 @@ "Importing": "Importando", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear", "IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}", - "IncludeHealthWarnings": "Incluir avisos de integridade", + "IncludeHealthWarnings": "Incluir Advertências 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", From 9eafdbd1aff66b2e2c760057630c9b7e937cc125 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 6 Feb 2024 10:58:32 +0000 Subject: [PATCH 088/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Ole Nørby <ole@olenoerby.dk> Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: aghus <aghus.m@outlook.com> Co-authored-by: gr0sz <joshuatg727@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 20 +++++++- src/NzbDrone.Core/Localization/Core/es.json | 11 ++++- .../Localization/Core/pt_BR.json | 47 ++++++++++--------- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 2c03a7dee..66f4c6531 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -1,3 +1,21 @@ { - "Absolute": "Absolut" + "Absolute": "Absolut", + "AbsoluteEpisodeNumber": "Absolut Episode-nummer", + "AddConditionError": "Kan ikke tilføje en ny betingelse, prøv igen.", + "AddAutoTagError": "Kan ikke tilføje en ny liste, prøv igen.", + "AddConnection": "Tilføj forbindelse", + "AddCustomFormat": "Tilføj tilpasset format", + "AddCustomFormatError": "Kunne ikke tilføje et nyt tilpasset format, prøv igen.", + "AddDelayProfile": "Tilføj forsinkelsesprofil", + "AddCondition": "Tilføj betingelse", + "AddAutoTag": "Tilføj automatisk Tag", + "AbsoluteEpisodeNumbers": "Absolutte Episode-numre", + "Add": "Tilføj", + "Activity": "Aktivitet", + "About": "Om", + "Actions": "Handlinger", + "AddANewPath": "Tilføj en ny sti", + "AddConditionImplementation": "Tilføj betingelse - {implementationName}", + "AddConnectionImplementation": "Tilføj forbindelse - {implementationName}", + "AddCustomFilter": "Tilføj tilpasset filter" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 9c26968fc..28db58326 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -989,7 +989,7 @@ "ImportListsValidationInvalidApiKey": "La clave API es inválida", "ImportListsValidationTestFailed": "El test fue abortado debido a un error: {exceptionMessage}", "ImportScriptPathHelpText": "La ruta al script a usar para importar", - "ImportUsingScriptHelpText": "Copia archivos a importar usando un script (p. ej. para transcodificación)", + "ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)", "Importing": "Importando", "IncludeUnmonitored": "Incluir sin monitorizar", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a fallos durante más de 6 horas", @@ -1059,5 +1059,12 @@ "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": "Número 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", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hay indexadores disponibles con la sincronización RSS activada, {appName} no capturará nuevos estrenos automáticamente", + "IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 241ee3cfb..5b65a8e75 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -392,8 +392,8 @@ "SelectFolder": "Selecionar Pasta", "Unavailable": "Indisponível", "UnmappedFolders": "Pastas não mapeadas", - "AutoTaggingNegateHelpText": "Se marcada, a regra de tagging automática não será aplicada se esta condição {implementationName} corresponder.", - "AutoTaggingRequiredHelpText": "Esta condição {implementationName} deve corresponder para que a regra de tagging automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} será suficiente.", + "AutoTaggingNegateHelpText": "se marcada, a regra de etiqueta automática não será aplicada se esta condição {implementationName} corresponder.", + "AutoTaggingRequiredHelpText": "Esta condição {implementationName} deve corresponder para que a regra de etiqueta automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} será suficiente.", "SomeResultsAreHiddenByTheAppliedFilter": "Alguns resultados estão ocultos pelo filtro aplicado", "UnableToLoadAutoTagging": "Não foi possível carregar a marcação automática", "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {indexerNames}.", @@ -417,7 +417,7 @@ "AddNotificationError": "Não foi possível adicionar uma nova notificação, tente novamente.", "AddQualityProfile": "Adicionar perfil de qualidade", "AddQualityProfileError": "Não foi possível adicionar uma nova notificação, tente novamente.", - "AddReleaseProfile": "Adicionar Perfil de Lançamento", + "AddReleaseProfile": "Adicionar um Perfil de Lançamento", "AddRemotePathMapping": "Adicionar Mapeamento de Caminho Remoto", "AddRemotePathMappingError": "Não foi possível adicionar um novo mapeamento de caminho remoto, tente novamente.", "AfterManualRefresh": "Depois da Atualização Manual", @@ -477,7 +477,7 @@ "ColonReplacementFormatHelpText": "Mude como o {appName} lida com a substituição do dois-pontos", "CompletedDownloadHandling": "Gerenciamento de Downloads Completos", "Condition": "Condição", - "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de expressões regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam ser escapados com um `\\`", + "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões Regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam escape com um `\\`", "ConnectSettings": "Configurações de Conexão", "ConnectSettingsSummary": "Notificações, conexões com servidores/reprodutores de mídia e scripts personalizados", "Connections": "Conexões", @@ -488,7 +488,7 @@ "CreateGroup": "Criar Grupo", "Custom": "Personalizar", "CustomFormat": "Formato personalizado", - "CustomFormatUnknownCondition": "Condição de formato personalizado desconhecido '{implementation}'", + "CustomFormatUnknownCondition": "Condição de Formato Personalizado desconhecida '{implementation}'", "CustomFormatUnknownConditionOption": "Opção desconhecida '{key}' para a condição '{implementation}'", "CustomFormatsLoadError": "Não foi possível carregar Formatos Personalizados", "CustomFormatsSettings": "Configurações de Formatos Personalizados", @@ -501,7 +501,7 @@ "DefaultDelayProfileSeries": "Este é o perfil padrão. Aplica-se a todas as séries que não possuem um perfil explícito.", "DelayMinutes": "{delay} Minutos", "DelayProfile": "Perfil de Atraso", - "DefaultCase": "Minúscula ou maiúscula", + "DefaultCase": "Padrão Maiúscula ou Minúscula", "DelayProfileSeriesTagsHelpText": "Aplica-se a séries com pelo menos uma tag correspondente", "DelayProfiles": "Perfis de Atraso", "DelayProfilesLoadError": "Não foi possível carregar perfis de atraso", @@ -544,7 +544,7 @@ "EditGroups": "Editar Grupos", "EditImportListExclusion": "Editar Exclusão de Lista de Importação", "EditListExclusion": "Editar Exclusão da Lista", - "EditMetadata": "Editar metadados {metadataType}", + "EditMetadata": "Editar {metadataType} Metadados", "EditQualityProfile": "Editar Perfil de Qualidade", "EditReleaseProfile": "Editar Perfil de Lançamento", "EditRemotePathMapping": "Editar Mapeamento do Caminho Remoto", @@ -574,7 +574,7 @@ "Extend": "Estender", "ExtraFileExtensionsHelpTextsExamples": "Exemplos: '.sub, .nfo' or 'sub,nfo'", "FileManagement": "Gerenciamento de Arquivo", - "FileNameTokens": "Tokens de Nome de Arquivo", + "FileNameTokens": "Tokens de nome de arquivo", "FileNames": "Nomes de Arquivo", "FirstDayOfWeek": "Primeiro Dia da Semana", "Folders": "Pastas", @@ -620,8 +620,8 @@ "LanguagesLoadError": "Não foi possível carregar os idiomas", "ListExclusionsLoadError": "Não foi possível carregar as exclusões de lista", "ListOptionsLoadError": "Não foi possível carregar as opções de lista", - "ListQualityProfileHelpText": "Os itens da lista de perfil de qualidade serão adicionados com", - "ListRootFolderHelpText": "Os itens da lista da pasta raiz serão adicionados a", + "ListQualityProfileHelpText": "Os itens da lista de Perfil de Qualidade que serão adicionados com", + "ListRootFolderHelpText": "Os itens da lista da pasta raiz que serão adicionados", "ListTagsHelpText": "Tags que serão adicionadas ao importar esta lista", "ListWillRefreshEveryInterval": "A lista será atualizada a cada {refreshInterval}", "ListsLoadError": "Não foi possível carregar as listas", @@ -631,7 +631,7 @@ "LogLevelTraceHelpTextWarning": "O registro em log deve ser habilitado apenas temporariamente", "Logging": "Registro em log", "LongDateFormat": "Formato longo de data", - "Lowercase": "Minúsculas", + "Lowercase": "Minúscula", "ManualImportItemsLoadError": "Não foi possível carregar itens de importação manual", "Max": "Máx.", "MaximumLimits": "Limites máximos", @@ -668,12 +668,12 @@ "MultiEpisodeStyle": "Estilo de multiepisódio", "MustContain": "Deve conter", "MustNotContain": "Não deve conter", - "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais destes termos (sem distinção entre maiúsculas e minúsculas)", + "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais termos (não diferenciar maiúsculas e minúsculas)", "NamingSettings": "Configurações de nomenclatura", "NamingSettingsLoadError": "Não foi possível carregar as configurações de nomenclatura", "Never": "Nunca", "NoChanges": "Sem alterações", - "NoDelay": "Sem atraso", + "NoDelay": "Sem Atraso", "NoLinks": "Sem links", "NoTagsHaveBeenAddedYet": "Nenhuma tag foi adicionada ainda", "None": "Nenhum", @@ -703,7 +703,7 @@ "Password": "Senha", "PendingChangesDiscardChanges": "Descartar mudanças e sair", "PendingChangesStayReview": "Ficar e revisar mudanças", - "Period": "Período", + "Period": "Ponto", "Permissions": "Permissões", "PortNumber": "Número da Porta", "PreferAndUpgrade": "Preferir e Atualizar", @@ -737,7 +737,7 @@ "RecyclingBinCleanupHelpTextWarning": "Os arquivos na lixeira mais antigos do que o número de dias selecionado serão limpos automaticamente", "RecyclingBinHelpText": "Os arquivos irão para cá quando excluídos, em vez de serem excluídos permanentemente", "AbsoluteEpisodeNumber": "Número Absoluto do Episódio", - "AddAutoTagError": "Não foi possível adicionar uma nova tag automática, por favor, tente novamente.", + "AddAutoTagError": "Não foi possível adicionar uma nova etiqueta automática, tente novamente.", "AnalyseVideoFilesHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação de endereços locais.", @@ -753,9 +753,9 @@ "ExtraFileExtensionsHelpText": "Lista separada por vírgulas de arquivos extras para importar (.nfo será importado como .nfo-orig)", "HistoryLoadError": "Não foi possível carregar o histórico", "IndexerTagSeriesHelpText": "Usar este indexador apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", - "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages oferece suporte a um sufixo \":EN+DE\", permitindo que você filtre os idiomas inclusos no nome do arquivo. Use \"-DE\" para excluir idiomas específicos. Usar \"+\" (p. ex.: \":EN+\") resultará em \"[EN]\"/\"[EN+--]\"/\"[--]\" dependendo dos idiomas excluídos. P. ex.: \"{MediaInfo Full:EN+DE}\".", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages suporta um sufixo `:EN+DE` permitindo filtrar os idiomas incluídos no nome do arquivo. Use `-DE` para excluir idiomas específicos. Anexar `+` (por exemplo, `:EN+`) resultará em `[EN]`/`[EN+--]`/`[--]` dependendo dos idiomas excluídos. Por exemplo, `{MediaInfo Full:EN+DE}`.", "MinimumFreeSpaceHelpText": "Impedir a importação se deixar menos do que esta quantidade de espaço em disco disponível", - "MustContainHelpText": "O lançamento deve conter pelo menos um desses termos (sem distinção entre maiúsculas e minúsculas)", + "MustContainHelpText": "O lançamento deve conter pelo menos um destes termos (não diferenciar maiúsculas e minúsculas)", "NegateHelpText": "Se marcado, o formato personalizado não será aplicado se esta condição {implementationName} corresponder.", "NoLimitForAnyRuntime": "Sem limite para qualquer duração", "NoMinimumForAnyRuntime": "Sem mínimo para qualquer duração", @@ -772,7 +772,7 @@ "RegularExpressionsTutorialLink": "Mais detalhes sobre expressões regulares podem ser encontrados [aqui](https://www.regular-expressions.info/tutorial.html).", "ReleaseProfile": "Perfil de Lançamento", "ReleaseProfileIndexerHelpText": "Especifique a qual indexador o perfil se aplica", - "ReleaseProfileIndexerHelpTextWarning": "O uso de um indexador específico com perfis de lançamento pode levar à obtenção de lançamentos duplicados", + "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", @@ -799,8 +799,8 @@ "RestartLater": "Vou reiniciar mais tarde", "RestartNow": "Reiniciar Agora", "RestartRequiredHelpTextWarning": "Requer reinicialização para entrar em vigor", - "RestartRequiredToApplyChanges": "O {appName} requer uma reinicialização para aplicar as alterações. Deseja reiniciar agora?", - "RestartRequiredWindowsService": "Dependendo de qual usuário está executando o serviço {appName}, pode ser necessário reiniciar o {appName} como administrador uma vez antes que o serviço seja iniciado automaticamente.", + "RestartRequiredToApplyChanges": "{appName} requer reinicialização para aplicar as alterações. Deseja reiniciar agora?", + "RestartRequiredWindowsService": "Dependendo de qual usuário está executando o serviço {appName}, pode ser necessário reiniciar {appName} como administrador uma vez antes que o serviço seja iniciado automaticamente.", "RestartSonarr": "Reiniciar {appName}", "RestrictionsLoadError": "Não foi possível carregar as Restrições", "Retention": "Retenção", @@ -901,7 +901,7 @@ "UpgradeUntilThisQualityIsMetOrExceeded": "Atualize até que essa qualidade seja atendida ou excedida", "UpgradesAllowed": "Atualizações Permitidas", "UpgradesAllowedHelpText": "se as qualidades desativadas não forem atualizadas", - "Uppercase": "Maiúsculo", + "Uppercase": "Maiuscula", "UrlBase": "URL base", "UrlBaseHelpText": "Para suporte a proxy reverso, o padrão é vazio", "UseProxy": "Usar Proxy", @@ -1198,7 +1198,7 @@ "InteractiveImportNoLanguage": "Defina um idioma para cada arquivo selecionado", "InteractiveImportNoQuality": "Defina a qualidade para cada arquivo selecionado", "InteractiveImportNoSeries": "A série deve ser escolhida para cada arquivo selecionado", - "KeyboardShortcuts": "Atalhos de teclado", + "KeyboardShortcuts": "Atalhos do Teclado", "KeyboardShortcutsCloseModal": "Fechar pop-up atual", "KeyboardShortcutsConfirmModal": "Aceitar o pop-up de confirmação", "KeyboardShortcutsOpenModal": "Abrir este pop-up", @@ -2038,5 +2038,6 @@ "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", "LogOnly": "Só Registro", "UnableToLoadListOptions": "Não foi possível carregar as opções da lista", - "CleanLibraryLevel": "Limpar Nível da Biblioteca" + "CleanLibraryLevel": "Limpar Nível da Biblioteca", + "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente." } From 745b92daf4bf4b9562ffe52dad84a12a5561add5 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 1 Feb 2024 20:19:26 -0800 Subject: [PATCH 089/762] Fixed: Redirecting after login Closes #6454 --- .../Authentication/AuthenticationController.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Http/Authentication/AuthenticationController.cs b/src/Sonarr.Http/Authentication/AuthenticationController.cs index 79edc7567..fbb9262b9 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationController.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -46,7 +47,17 @@ namespace Sonarr.Http.Authentication await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); - return Redirect(_configFileProvider.UrlBase + "/"); + if (returnUrl.IsNullOrWhiteSpace()) + { + return Redirect(_configFileProvider.UrlBase + "/"); + } + + if (_configFileProvider.UrlBase.IsNullOrWhiteSpace() || returnUrl.StartsWith(_configFileProvider.UrlBase)) + { + return Redirect(returnUrl); + } + + return Redirect(_configFileProvider.UrlBase + returnUrl); } [HttpGet("logout")] From 4cb110070422917ae87ab7c3293fb263977056f0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 1 Feb 2024 20:20:30 -0800 Subject: [PATCH 090/762] Fixed: Remove old naming config from v3 API responses Closes #6460 --- .../Organizer/BasicNamingConfig.cs | 12 ---- .../Organizer/FileNameBuilder.cs | 47 -------------- .../Config/NamingConfigController.cs | 6 -- .../Config/NamingConfigResource.cs | 6 -- .../Config/NamingExampleResource.cs | 17 ----- src/Sonarr.Api.V3/openapi.json | 64 +------------------ 6 files changed, 1 insertion(+), 151 deletions(-) delete mode 100644 src/NzbDrone.Core/Organizer/BasicNamingConfig.cs diff --git a/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs b/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs deleted file mode 100644 index b0dc16f6a..000000000 --- a/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NzbDrone.Core.Organizer -{ - public class BasicNamingConfig - { - public bool IncludeSeriesTitle { get; set; } - public bool IncludeEpisodeTitle { get; set; } - public bool IncludeQuality { get; set; } - public bool ReplaceSpaces { get; set; } - public string Separator { get; set; } - public string NumberStyle { get; set; } - } -} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 57ebfc5ca..115fa5d27 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -25,7 +25,6 @@ namespace NzbDrone.Core.Organizer string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension = "", NamingConfig namingConfig = null, List<CustomFormat> customFormats = null); string BuildFilePath(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension, NamingConfig namingConfig = null, List<CustomFormat> customFormats = null); string BuildSeasonPath(Series series, int seasonNumber); - BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); bool RequiresEpisodeTitle(Series series, List<Episode> episodes); @@ -253,52 +252,6 @@ namespace NzbDrone.Core.Organizer return path; } - public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) - { - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); - - if (episodeFormat == null) - { - return new BasicNamingConfig(); - } - - var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; - - var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); - - foreach (Match match in titleTokens) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - - if (!separator.Equals(" ")) - { - basicNamingConfig.ReplaceSpaces = true; - } - - if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeSeriesTitle = true; - } - - if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeEpisodeTitle = true; - } - - if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeQuality = true; - } - } - - return basicNamingConfig; - } - public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) { if (namingConfig == null) diff --git a/src/Sonarr.Api.V3/Config/NamingConfigController.cs b/src/Sonarr.Api.V3/Config/NamingConfigController.cs index 137f24769..f940519aa 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigController.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigController.cs @@ -49,12 +49,6 @@ namespace Sonarr.Api.V3.Config var nameSpec = _namingConfigService.GetConfig(); var resource = nameSpec.ToResource(); - if (resource.StandardEpisodeFormat.IsNotNullOrWhiteSpace()) - { - var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); - basicConfig.AddToResource(resource); - } - return resource; } diff --git a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs index 4e4116807..f1acc9fe3 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs @@ -14,11 +14,5 @@ namespace Sonarr.Api.V3.Config public string SeriesFolderFormat { get; set; } public string SeasonFolderFormat { get; set; } public string SpecialsFolderFormat { get; set; } - public bool IncludeSeriesTitle { get; set; } - public bool IncludeEpisodeTitle { get; set; } - public bool IncludeQuality { get; set; } - public bool ReplaceSpaces { get; set; } - public string Separator { get; set; } - public string NumberStyle { get; set; } } } diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs index 66e21aff8..6784e1a4b 100644 --- a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs +++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs @@ -32,26 +32,9 @@ namespace Sonarr.Api.V3.Config SeriesFolderFormat = model.SeriesFolderFormat, SeasonFolderFormat = model.SeasonFolderFormat, SpecialsFolderFormat = model.SpecialsFolderFormat - - // IncludeSeriesTitle - // IncludeEpisodeTitle - // IncludeQuality - // ReplaceSpaces - // Separator - // NumberStyle }; } - public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource) - { - resource.IncludeSeriesTitle = basicNamingConfig.IncludeSeriesTitle; - resource.IncludeEpisodeTitle = basicNamingConfig.IncludeEpisodeTitle; - resource.IncludeQuality = basicNamingConfig.IncludeQuality; - resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces; - resource.Separator = basicNamingConfig.Separator; - resource.NumberStyle = basicNamingConfig.NumberStyle; - } - public static NamingConfig ToModel(this NamingConfigResource resource) { return new NamingConfig diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index ee63aefa5..139d7b2dd 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -4737,48 +4737,6 @@ "type": "string" } }, - { - "name": "includeSeriesTitle", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "includeEpisodeTitle", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "includeQuality", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "replaceSpaces", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "separator", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "numberStyle", - "in": "query", - "schema": { - "type": "string" - } - }, { "name": "id", "in": "query", @@ -9825,26 +9783,6 @@ "specialsFolderFormat": { "type": "string", "nullable": true - }, - "includeSeriesTitle": { - "type": "boolean" - }, - "includeEpisodeTitle": { - "type": "boolean" - }, - "includeQuality": { - "type": "boolean" - }, - "replaceSpaces": { - "type": "boolean" - }, - "separator": { - "type": "string", - "nullable": true - }, - "numberStyle": { - "type": "string", - "nullable": true } }, "additionalProperties": false @@ -12082,4 +12020,4 @@ "apikey": [ ] } ] -} \ No newline at end of file +} From 1006ec6b522027595044deba491969c8076216b5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Fri, 2 Feb 2024 07:38:53 +0100 Subject: [PATCH 091/762] really fix translation key --- .../Download/Clients/Transmission/TransmissionSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 9f550084b..dae92b1b1 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsUrlBaseHelpText")] [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Transmission")] [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/rpc")] [FieldToken(TokenField.HelpText, "UrlBase", "defaultUrl", "/transmission/")] From 904285045bfb0d55c20aba9930d15cd94b534f2d Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 3 Feb 2024 01:29:59 +0200 Subject: [PATCH 092/762] Fixed: Naming validation when using max token length --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 115fa5d27..f527dd334 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Organizer public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title(The)?(Without)?(Year)?\})", + public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title(The)?(Without)?(Year)?(?::(?<customFormat>[0-9-]+))?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); From 80630bf97f5bb3b49d4824dc039d2edfc74e4797 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Wed, 7 Feb 2024 04:58:09 +0100 Subject: [PATCH 093/762] Fixed: Wrapping of naming tokens with alternate separators --- .../src/Settings/MediaManagement/Naming/NamingOption.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 824cf247e..a09c91ec8 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -17,7 +17,7 @@ } .small { - width: 480px; + width: 490px; } .large { @@ -26,7 +26,7 @@ .token { flex: 0 0 50%; - padding: 6px 16px; + padding: 6px 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 16px; + padding: 6px 6px; background-color: var(--popoverBodyBackgroundColor); .footNote { From 6ab1d8e16b29e98b4d2ebb68e0356f6f2d3a2c10 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 6 Feb 2024 19:58:49 -0800 Subject: [PATCH 094/762] New: Log database engine version on startup --- .../000_database_engine_version_check.cs | 69 +++++++++++++++++++ .../Framework/MigrationController.cs | 3 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs diff --git a/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs b/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs new file mode 100644 index 000000000..93bfc0afc --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs @@ -0,0 +1,69 @@ +using System.Data; +using System.Text.RegularExpressions; +using FluentMigrator; +using NLog; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)] + public class DatabaseEngineVersionCheck : FluentMigrator.Migration + { + protected readonly Logger _logger; + + public DatabaseEngineVersionCheck() + { + _logger = NzbDroneLogger.GetLogger(this); + } + + public override void Up() + { + IfDatabase("sqlite").Execute.WithConnection(LogSqliteVersion); + IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion); + } + + public override void Down() + { + // No-op + } + + private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran) + { + using (var versionCmd = conn.CreateCommand()) + { + versionCmd.Transaction = tran; + versionCmd.CommandText = "SELECT sqlite_version();"; + + using (var reader = versionCmd.ExecuteReader()) + { + while (reader.Read()) + { + var version = reader.GetString(0); + + _logger.Info("SQLite {0}", version); + } + } + } + } + + private void LogPostgresVersion(IDbConnection conn, IDbTransaction tran) + { + using (var versionCmd = conn.CreateCommand()) + { + versionCmd.Transaction = tran; + versionCmd.CommandText = "SHOW server_version"; + + using (var reader = versionCmd.ExecuteReader()) + { + while (reader.Read()) + { + var version = reader.GetString(0); + var cleanVersion = Regex.Replace(version, @"\(.*?\)", ""); + + _logger.Info("Postgres {0}", cleanVersion); + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index dea8365c1..8ef3b647a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -42,12 +42,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework serviceProvider = new ServiceCollection() .AddLogging(b => b.AddNLog()) .AddFluentMigratorCore() + .Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true) .ConfigureRunner( builder => builder .AddPostgres() .AddNzbDroneSQLite() .WithGlobalConnectionString(connectionString) - .WithMigrationsIn(Assembly.GetExecutingAssembly())) + .ScanIn(Assembly.GetExecutingAssembly()).For.All()) .Configure<TypeFilterOptions>(opt => opt.Namespace = "NzbDrone.Core.Datastore.Migration") .Configure<ProcessorOptions>(opt => { From 63e132d25716abe8b8b30a851b4574ce7245dab0 Mon Sep 17 00:00:00 2001 From: jab416171 <jab416171@gmail.com> Date: Sat, 27 Jan 2024 12:34:03 -0700 Subject: [PATCH 095/762] Wrapped fields on series details page in div This allows you to triple click to select the path for instance, similar to the details page in radarr. --- frontend/src/Series/Details/SeriesDetails.js | 124 +++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index d4a147e53..b4e6303cc 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -428,14 +428,16 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <Icon - name={icons.FOLDER} - size={17} - /> - <span className={styles.path}> - {path} - </span> + <div> + <Icon + name={icons.FOLDER} + size={17} + /> + <span className={styles.path}> + {path} + </span> + </div> </Label> <Tooltip @@ -444,16 +446,18 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <Icon - name={icons.DRIVE} - size={17} - /> - <span className={styles.sizeOnDisk}> - { - formatBytes(sizeOnDisk || 0) - } - </span> + <div> + <Icon + name={icons.DRIVE} + size={17} + /> + <span className={styles.sizeOnDisk}> + { + formatBytes(sizeOnDisk || 0) + } + </span> + </div> </Label> } tooltip={ @@ -470,32 +474,36 @@ class SeriesDetails extends Component { title={translate('QualityProfile')} size={sizes.LARGE} > - <Icon - name={icons.PROFILE} - size={17} - /> - <span className={styles.qualityProfileName}> - { - <QualityProfileNameConnector - qualityProfileId={qualityProfileId} - /> - } - </span> + <div> + <Icon + name={icons.PROFILE} + size={17} + /> + <span className={styles.qualityProfileName}> + { + <QualityProfileNameConnector + qualityProfileId={qualityProfileId} + /> + } + </span> + </div> </Label> <Label className={styles.detailsLabel} size={sizes.LARGE} > - <Icon - name={monitored ? icons.MONITORED : icons.UNMONITORED} - size={17} - /> - <span className={styles.qualityProfileName}> - {monitored ? translate('Monitored') : translate('Unmonitored')} - </span> + <div> + <Icon + name={monitored ? icons.MONITORED : icons.UNMONITORED} + size={17} + /> + <span className={styles.qualityProfileName}> + {monitored ? translate('Monitored') : translate('Unmonitored')} + </span> + </div> </Label> <Label @@ -503,14 +511,16 @@ class SeriesDetails extends Component { title={statusDetails.message} size={sizes.LARGE} > - <Icon - name={statusDetails.icon} - size={17} - /> - <span className={styles.qualityProfileName}> - {statusDetails.title} - </span> + <div> + <Icon + name={statusDetails.icon} + size={17} + /> + <span className={styles.qualityProfileName}> + {statusDetails.title} + </span> + </div> </Label> { @@ -520,14 +530,16 @@ class SeriesDetails extends Component { title={translate('Network')} size={sizes.LARGE} > - <Icon - name={icons.NETWORK} - size={17} - /> - <span className={styles.qualityProfileName}> - {network} - </span> + <div> + <Icon + name={icons.NETWORK} + size={17} + /> + <span className={styles.qualityProfileName}> + {network} + </span> + </div> </Label> } @@ -537,14 +549,16 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <Icon - name={icons.EXTERNAL_LINK} - size={17} - /> - <span className={styles.links}> - {translate('Links')} - </span> + <div> + <Icon + name={icons.EXTERNAL_LINK} + size={17} + /> + <span className={styles.links}> + {translate('Links')} + </span> + </div> </Label> } tooltip={ From cac97c057faa44c1656e02681cb9ba668faca488 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Tue, 6 Feb 2024 22:01:07 -0600 Subject: [PATCH 096/762] Improve Custom Format rejection messaging --- .../EpisodeImport/Specifications/UpgradeSpecification.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 01530e782..0a6d5a6be 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -72,7 +72,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications currentFormats != null ? currentFormats.ConcatToString() : "", currentFormatScore); - return Decision.Reject("Not a Custom Format upgrade for existing episode file(s)"); + return Decision.Reject("Not a Custom Format upgrade for existing episode file(s). New: [{0}] ({1}) do not improve on Existing: [{2}] ({3})", + newFormats != null ? newFormats.ConcatToString() : "", + newFormatScore, + currentFormats != null ? currentFormats.ConcatToString() : "", + currentFormatScore); } _logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting", From f722d49b3a9efefa65bef1b24d90be9332ca62ea Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 6 Feb 2024 16:47:20 -0800 Subject: [PATCH 097/762] Fixed: Don't use sub folder to check for free disk space for update Closes #6478 --- src/NzbDrone.Core/Update/InstallUpdateService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 569f36e47..eea51684a 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -105,11 +105,12 @@ namespace NzbDrone.Core.Update return false; } + var tempFolder = _appFolderInfo.TempFolder; var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder(); - if (_diskProvider.GetTotalSize(updateSandboxFolder) < 1.Gigabytes()) + if (_diskProvider.GetTotalSize(tempFolder) < 1.Gigabytes()) { - _logger.Warn("Temporary location '{0}' has less than 1 GB free space, Sonarr may not be able to update itself.", updateSandboxFolder); + _logger.Warn("Temporary location '{0}' has less than 1 GB free space, Sonarr may not be able to update itself.", tempFolder); } var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); From 895eccebc5ae61c6ae977daef20d00341f78b125 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 6 Feb 2024 20:02:03 -0800 Subject: [PATCH 098/762] New: Parse and reject split episode releases and files --- .../ParserTests/SingleEpisodeParserFixture.cs | 14 ++++++++ .../SplitEpisodeSpecification.cs | 30 +++++++++++++++++ .../SplitEpisodeSpecification.cs | 33 +++++++++++++++++++ .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 9 +++++ 5 files changed, 87 insertions(+) create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 245fee1a6..075d1758d 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -211,5 +211,19 @@ namespace NzbDrone.Core.Test.ParserTests result.FullSeason.Should().BeFalse(); result.Special.Should().BeTrue(); } + + [TestCase("Series.Title.S06E01b.Fade.Out.Fade.in.Part.2.1080p.DSNP.WEB-DL.AAC2.0.H.264-FLUX", "Series Title", 6, 1)] + public void should_parse_split_episode(string postTitle, string title, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.EpisodeNumbers.Should().HaveCount(1); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.First().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + result.IsSplitEpisode.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs new file mode 100644 index 000000000..fef3be741 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class SplitEpisodeSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public SplitEpisodeSpecification(Logger logger) + { + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Default; + public RejectionType Type => RejectionType.Permanent; + + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (subject.ParsedEpisodeInfo.IsSplitEpisode) + { + _logger.Debug("Split episode release {0} rejected. Not supported", subject.Release.Title); + return Decision.Reject("Split episode releases are not supported"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs new file mode 100644 index 000000000..8a84b0b86 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs @@ -0,0 +1,33 @@ +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class SplitEpisodeSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public SplitEpisodeSpecification(Logger logger) + { + _logger = logger; + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + if (localEpisode.FileEpisodeInfo == null) + { + return Decision.Accept(); + } + + if (localEpisode.FileEpisodeInfo.IsSplitEpisode) + { + _logger.Debug("Single episode split into multiple files"); + return Decision.Reject("Single episode split into multiple files"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 9423bd5ca..3833199bb 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model public bool IsPartialSeason { get; set; } public bool IsMultiSeason { get; set; } public bool IsSeasonExtra { get; set; } + public bool IsSplitEpisode { 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 836101e14..fc369f6e7 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -74,6 +74,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_]|[ex]){1,2}(?<episode>\d{2,3}(?!\d+))){2,})", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Split episodes (S01E05a, S01E05b, etc) + new Regex(@"^(?<title>.+?)(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))(?<splitepisode>[a-d])(?:[ _.])))", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes without a title, Single (S01E05, 1x05) new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -966,6 +970,11 @@ namespace NzbDrone.Core.Parser { result.Special = true; } + + if (matchGroup.Groups["splitepisode"].Success) + { + result.IsSplitEpisode = true; + } } if (absoluteEpisodeCaptures.Any()) From 34e74eecd77ee736db6df6e5c04d71d7ba626776 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 6 Feb 2024 20:02:26 -0800 Subject: [PATCH 099/762] Fixed: Don't attempt to import from list with title only (#6477) Closes #6474 --- .../ImportListTests/ImportListSyncServiceFixture.cs | 11 ----------- .../ImportLists/ImportListSyncService.cs | 13 ------------- 2 files changed, 24 deletions(-) diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 3cf62f649..d9702c24b 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -406,17 +406,6 @@ namespace NzbDrone.Core.Test.ImportListTests .Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 3), true), Times.Once()); } - [Test] - public void should_search_if_series_title_and_no_series_id() - { - _importListFetch.Series.ForEach(m => m.ImportListId = 1); - WithList(1, true); - Subject.Execute(_commandAll); - - Mocker.GetMock<ISearchForNewSeries>() - .Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once()); - } - [Test] public void should_not_search_if_series_title_and_series_id() { diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index f6d414562..1c4c9ea7a 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -190,19 +190,6 @@ namespace NzbDrone.Core.ImportLists item.Title = mappedSeries.Title; } - // Map TVDb if we only have a series name - if (item.TvdbId <= 0 && item.Title.IsNotNullOrWhiteSpace()) - { - var mappedSeries = _seriesSearchService.SearchForNewSeries(item.Title) - .FirstOrDefault(); - - if (mappedSeries != null) - { - item.TvdbId = mappedSeries.TvdbId; - item.Title = mappedSeries?.Title; - } - } - // Check to see if series excluded var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault(); From 6e81517d51de5f3ca439fd3a07a732ec1d46967e Mon Sep 17 00:00:00 2001 From: Stas Panasiuk <36239821+spanasiuk@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:03:36 -0500 Subject: [PATCH 100/762] New: Parsing titles with multiple translated titles --- .../ParserTests/MultiEpisodeParserFixture.cs | 1 + src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 1 + .../ParserTests/SingleEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index 6af9a1733..e3c098f4f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -76,6 +76,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series T Se.3 afl.3 en 4", "Series T", 3, new[] { 3, 4 })] [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("", "", , new [] { })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index db1a61d3f..a87010eb9 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -94,6 +94,7 @@ namespace NzbDrone.Core.Test.ParserTests } [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")] + [TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")] public void should_parse_multiple_series_titles(string postTitle, params string[] titles) { var seriesTitleInfo = Parser.Parser.ParseTitle(postTitle).SeriesTitleInfo; diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 075d1758d..9bcb8ca7b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -165,6 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title [HDTV][Cap.402](website.com).avi", "Series Title", 4, 2)] [TestCase("Series Title [HDTV 720p][Cap.101](website.com).mkv", "Series Title", 1, 1)] [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] + [TestCase("Босх: Спадок / Series: Legacy / S2E1 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index fc369f6e7..622ea72a7 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -182,6 +182,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9]).+?(?:s?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+)))(?:[ex](?<episode>(?<!\d+)(?:\d{1,3})(?!\d+)))", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Single or multi episode releases with multiple titles, then season and episode numbers after the last title. (Title1 / Title2 / ... / S1E1-2 of 6) + 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 (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), From 913b845faadc3c9fc005abfba815426743d01bdf Mon Sep 17 00:00:00 2001 From: Qstick <qstick@gmail.com> Date: Fri, 9 Feb 2024 20:10:09 -0600 Subject: [PATCH 101/762] Fixed: Prevent anime search with ep/season if not supported --- src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index b3e0090cd..ed09945cd 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -402,7 +402,7 @@ namespace NzbDrone.Core.Indexers.Newznab searchCriteria.SeasonNumber > 0 && searchCriteria.EpisodeNumber > 0; - if (includeAnimeStandardFormatSearch) + if (includeAnimeStandardFormatSearch && SupportsEpisodeSearch) { AddTvIdPageableRequests(pageableRequests, Settings.AnimeCategories, @@ -419,7 +419,7 @@ namespace NzbDrone.Core.Indexers.Newznab "search", $"&q={NewsnabifyTitle(queryTitle)}+{searchCriteria.AbsoluteEpisodeNumber:00}")); - if (includeAnimeStandardFormatSearch) + if (includeAnimeStandardFormatSearch && SupportsEpisodeSearch) { pageableRequests.Add(GetPagedRequests(MaxPages, Settings.AnimeCategories, From bd9d4b484c4fe44b985174dc82e9c51907ddcff1 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:08:45 +0200 Subject: [PATCH 102/762] Update Custom Format Deletion confirmation message Consistency with the rest of the Delete*MessageText --- .../src/Settings/CustomFormats/CustomFormats/CustomFormat.js | 2 +- src/NzbDrone.Core/Localization/Core/ca.json | 2 +- src/NzbDrone.Core/Localization/Core/cs.json | 2 +- src/NzbDrone.Core/Localization/Core/de.json | 2 +- src/NzbDrone.Core/Localization/Core/el.json | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 2 +- src/NzbDrone.Core/Localization/Core/fr.json | 2 +- src/NzbDrone.Core/Localization/Core/hu.json | 2 +- src/NzbDrone.Core/Localization/Core/it.json | 2 +- src/NzbDrone.Core/Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js index efe105b20..0f72228bb 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js @@ -152,7 +152,7 @@ class CustomFormat extends Component { isOpen={this.state.isDeleteCustomFormatModalOpen} kind={kinds.DANGER} title={translate('DeleteCustomFormat')} - message={translate('DeleteCustomFormatMessageText', { customFormatName: name })} + message={translate('DeleteCustomFormatMessageText', { name })} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteCustomFormat} diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 6ee9776a1..0bba6b2a8 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -322,7 +322,7 @@ "CustomFormatUnknownConditionOption": "Opció desconeguda '{key}' per a la condició '{implementation}'", "Cutoff": "Requisit", "DelayingDownloadUntil": "S'està retardant la baixada fins al {date} a les {time}", - "DeleteCustomFormatMessageText": "Esteu segur que voleu suprimir el format personalitzat '{customFormatName}'?", + "DeleteCustomFormatMessageText": "Esteu segur que voleu suprimir el format personalitzat '{name}'?", "DeleteQualityProfileMessageText": "Esteu segur que voleu suprimir el perfil de qualitat '{name}'?", "DeleteRemotePathMappingMessageText": "Esteu segur que voleu suprimir aquesta assignació de camins remots?", "DeleteRootFolderMessageText": "Esteu segur que voleu suprimir la carpeta arrel '{path}'?", diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 1a09bf4e1..352a2cfd9 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -245,7 +245,7 @@ "CustomFormatJson": "Vlastní JSON formát", "Debug": "Ladit", "Day": "Den", - "DeleteCustomFormatMessageText": "Opravdu chcete odstranit vlastní formát '{customFormatName}'?", + "DeleteCustomFormatMessageText": "Opravdu chcete odstranit vlastní formát '{name}'?", "DefaultNameCopiedProfile": "{name} - Kopírovat", "DefaultNameCopiedSpecification": "{name} - Kopírovat", "DefaultNotFoundMessage": "Asi jsi se ztratil, není tu nic k vidění.", diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 7ba6610dd..b3ccecd05 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -23,7 +23,7 @@ "CloneCondition": "Bedingung klonen", "DeleteCondition": "Bedingung löschen", "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?", - "DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{customFormatName}' löschen willst?", + "DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{name}' löschen willst?", "RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?", "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?", "DeleteSelectedDownloadClients": "Lösche Download Client(s)", diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index d7f64dbef..4be715996 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -11,7 +11,7 @@ "RemoveFailedDownloads": "Αφαίρεση Αποτυχημένων Λήψεων", "DeleteCondition": "Διαγραφή συνθήκης", "DeleteConditionMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη συνθήκη '{name}';", - "DeleteCustomFormatMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη προσαρμοσμένη μορφή '{customFormatName}';", + "DeleteCustomFormatMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη προσαρμοσμένη μορφή '{name}';", "RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {selectedCount} αντικείμενα από την ουρά;", "CloneCondition": "Κλωνοποίηση συνθήκης", "RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f35981026..ba55cb50c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -322,7 +322,7 @@ "DeleteCondition": "Delete Condition", "DeleteConditionMessageText": "Are you sure you want to delete the condition '{name}'?", "DeleteCustomFormat": "Delete Custom Format", - "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{customFormatName}'?", + "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{name}'?", "DeleteDelayProfile": "Delete Delay Profile", "DeleteDelayProfileMessageText": "Are you sure you want to delete this delay profile?", "DeleteDownloadClient": "Delete Download Client", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 28db58326..fa8bb6233 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -476,7 +476,7 @@ "DeleteDelayProfile": "Eliminar Perfil de Retardo", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)", "DelayProfiles": "Perfiles de retardo", - "DeleteCustomFormatMessageText": "¿Estás seguro de que quieres eliminar el formato personalizado '{customFormatName}'?", + "DeleteCustomFormatMessageText": "¿Estás seguro de que quieres eliminar el formato personalizado '{name}'?", "DeleteBackup": "Eliminar copia de seguridad", "CopyUsingHardlinksSeriesHelpText": "Los hardlinks permiten a {appName} a importar los torrents que se estén compartiendo a la carpeta de la serie sin usar espacio adicional en el disco o sin copiar el contenido completo del archivo. Los hardlinks solo funcionarán si el origen y el destino están en el mismo volumen", "DefaultDelayProfileSeries": "Este es el perfil por defecto. Aplica a todas las series que no tienen un perfil explícito.", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 2eb48efaf..1228f6adc 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1233,7 +1233,7 @@ "EnableInteractiveSearch": "Activer la recherche interactive", "CloneCondition": "État du clone", "DeleteCustomFormat": "Supprimer le format personnalisé", - "DeleteCustomFormatMessageText": "Êtes-vous sûr de vouloir supprimer le format personnalisé '{customFormatName}' ?", + "DeleteCustomFormatMessageText": "Êtes-vous sûr de vouloir supprimer le format personnalisé '{name}' ?", "Conditions": "Conditions", "CountImportListsSelected": "{count} liste(s) d'importation sélectionnée(s)", "DeleteSeriesFolderConfirmation": "Le dossier de la série `{path}` et tout son contenu seront supprimés.", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 727b9c278..7903fb8ac 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -6,7 +6,7 @@ "DeleteCondition": "Feltétel törlése", "DeleteConditionMessageText": "Biztosan törli a(z) „{name}” feltételt?", "DeleteCustomFormat": "Egyéni formátum törlése", - "DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{customFormatName}' egyéni formátumot?", + "DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{name}' egyéni formátumot?", "ExportCustomFormat": "Egyéni formátum exportálása", "IndexerJackettAllHealthCheckMessage": "A nem támogatott Jackett 'all' végpontot használó indexelők: {indexerNames}", "Remove": "Eltávolítás", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 2b0da009b..adae40438 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -5,7 +5,7 @@ "MoveAutomatically": "Sposta Automaticamente", "ApplyTagsHelpTextRemove": "Rimuovi: Rimuove le etichette inserite", "ApplyTagsHelpTextAdd": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette", - "DeleteCustomFormatMessageText": "Sei sicuro di voler eliminare il formato personalizzato '{customFormatName}'?", + "DeleteCustomFormatMessageText": "Sei sicuro di voler eliminare il formato personalizzato '{name}'?", "DeleteSelectedDownloadClients": "Cancella i Client di Download", "Added": "Aggiunto", "AutomaticAdd": "Aggiungi Automaticamente", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 5b65a8e75..37b9b2ee2 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -94,7 +94,7 @@ "DeleteCondition": "Excluir condição", "DeleteConditionMessageText": "Tem certeza de que deseja excluir a condição '{name}'?", "DeleteCustomFormat": "Excluir formato personalizado", - "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado '{customFormatName}'?", + "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado '{name}'?", "ExportCustomFormat": "Exportar formato personalizado", "Negated": "Negado", "Remove": "Remover", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 420acb9d9..96d774dda 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -2,7 +2,7 @@ "CloneCondition": "克隆条件", "DeleteCondition": "删除条件", "DeleteConditionMessageText": "你确定要删除条件 “{name}” 吗?", - "DeleteCustomFormatMessageText": "是否确实要删除条件“{customFormatName}”?", + "DeleteCustomFormatMessageText": "是否确实要删除条件“{name}”?", "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{length}个字符长。您可以通过设置或配置文件执行此操作", "RemoveSelectedItemQueueMessageText": "您确定要从队列中删除一个项目吗?", "RemoveSelectedItemsQueueMessageText": "您确定要从队列中删除{selectedCount}项吗?", From dd704579df43b0dd835f8bb618c4b4412561a888 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 4 Feb 2024 20:40:38 +0200 Subject: [PATCH 103/762] Improve add/loading error notices --- frontend/src/Organize/OrganizePreviewModalContent.js | 2 +- .../CustomFormats/CustomFormats/CustomFormats.js | 2 +- .../CustomFormats/EditCustomFormatModalContent.js | 5 +++-- .../CustomFormats/ExportCustomFormatModalContent.js | 5 +++-- .../CustomFormats/ImportCustomFormatModalContent.js | 7 ++++--- .../Specifications/AddSpecificationModalContent.js | 4 ++-- .../DownloadClients/AddDownloadClientModalContent.js | 4 ++-- .../DownloadClients/DownloadClients/DownloadClient.js | 2 +- .../DownloadClients/EditDownloadClientModalContent.js | 4 ++-- .../Manage/ManageDownloadClientsModalContent.tsx | 2 +- .../EditRemotePathMappingModalContent.js | 5 +++-- .../EditRemotePathMappingModalContentConnector.js | 3 +-- .../RemotePathMappings/RemotePathMappings.js | 2 +- .../EditImportListExclusionModalContent.js | 5 +++-- .../EditImportListExclusionModalContentConnector.js | 3 +-- .../ImportLists/AddImportListModalContent.js | 4 ++-- .../ImportLists/EditImportListModalContent.js | 4 ++-- .../src/Settings/ImportLists/ImportLists/ImportList.js | 2 +- .../Settings/ImportLists/ImportLists/ImportLists.js | 3 +-- .../ImportLists/ImportLists/ImportListsConnector.js | 10 ++++------ .../ImportLists/Manage/ManageImportListsModal.tsx | 3 ++- .../Settings/ImportLists/Options/ImportListOptions.tsx | 5 +++-- .../Indexers/Indexers/AddIndexerModalContent.js | 4 ++-- .../Indexers/Indexers/EditIndexerModalContent.js | 5 +++-- frontend/src/Settings/Indexers/Indexers/Indexer.js | 2 +- .../Notifications/AddNotificationModalContent.js | 6 ++++-- .../Notifications/EditNotificationModalContent.js | 4 ++-- .../Profiles/Delay/EditDelayProfileModalContent.js | 4 ++-- .../Delay/EditDelayProfileModalContentConnector.js | 3 +-- .../Profiles/Quality/EditQualityProfileModalContent.js | 5 +++-- .../src/Settings/Profiles/Quality/QualityProfiles.js | 2 +- .../Profiles/Release/EditReleaseProfileModalContent.js | 2 +- .../Release/EditReleaseProfileModalContentConnector.js | 4 +--- .../src/Settings/Profiles/Release/ReleaseProfile.js | 5 ++--- .../Tags/AutoTagging/EditAutoTaggingModalContent.js | 5 +++-- .../Specifications/AddSpecificationModalContent.js | 4 ++-- frontend/src/Settings/Tags/Tag.js | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 1 - 38 files changed, 73 insertions(+), 71 deletions(-) diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js index bb1ab3355..6bb35bc79 100644 --- a/frontend/src/Organize/OrganizePreviewModalContent.js +++ b/frontend/src/Organize/OrganizePreviewModalContent.js @@ -109,7 +109,7 @@ class OrganizePreviewModalContent extends Component { { !isFetching && error && - <div>{translate('OrganizeLoadError')}</div> + <Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert> } { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js index 188df61d9..8036a4a25 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js @@ -62,7 +62,7 @@ class CustomFormats extends Component { <FieldSet legend={translate('CustomFormats')}> <PageSectionContent errorMessage={translate('CustomFormatsLoadError')} - {...otherProps}c={true} + {...otherProps} > <div className={styles.customFormats}> { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js index a57a38a7e..33497ce44 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -112,9 +113,9 @@ class EditCustomFormatModalContent extends Component { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddCustomFormatError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js index c6f0a64c5..4527cf662 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import ClipboardButton from 'Components/Link/ClipboardButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -41,9 +42,9 @@ class ExportCustomFormatModalContent extends Component { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('CustomFormatsLoadError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js index 4dc7641e2..b9c0590d1 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } 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'; @@ -11,7 +12,7 @@ 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, sizes } from 'Helpers/Props'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ImportCustomFormatModalContent.css'; @@ -95,9 +96,9 @@ class ImportCustomFormatModalContent extends Component { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('CustomFormatsLoadError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js index 3b38e9666..f06764719 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js @@ -42,9 +42,9 @@ class AddSpecificationModalContent extends Component { { !isSchemaFetching && !!schemaError && - <div> + <Alert kind={kinds.DANGER}> {translate('AddConditionError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js index 40f78f35f..7578314d9 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js @@ -43,9 +43,9 @@ class AddDownloadClientModalContent extends Component { { !isSchemaFetching && !!schemaError && - <div> + <Alert kind={kinds.DANGER}> {translate('AddDownloadClientError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js index fceaeda65..4e5063382 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -41,7 +41,7 @@ class DownloadClient extends Component { }); }; - onDeleteDownloadClientModalClose= () => { + onDeleteDownloadClientModalClose = () => { this.setState({ isDeleteDownloadClientModalOpen: false }); }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 9fe0a2f25..f2509603f 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -69,9 +69,9 @@ class EditDownloadClientModalContent extends Component { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddDownloadClientError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 0db06b9eb..2722f02fa 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -277,7 +277,7 @@ function ManageDownloadClientsModalContent( isDisabled={!anySelected} onPress={onTagsPress} > - Set Tags + {translate('SetTags')} </SpinnerButton> </div> diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js index 5fc2ac757..fd8ba14ec 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js @@ -1,5 +1,6 @@ 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'; @@ -52,9 +53,9 @@ function EditRemotePathMappingModalContent(props) { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddRemotePathMappingError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js index 136a68f50..6848d8bad 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -55,7 +54,7 @@ function createRemotePathMappingSelector() { items } = remotePathMappings; - const mapping = id ? _.find(items, { id }) : newRemotePathMapping; + const mapping = id ? items.find((i) => i.id === id) : newRemotePathMapping; const settings = selectSettings(mapping, pendingChanges, saveError); return { diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js index 1a942a19e..8e3666ce9 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js @@ -47,7 +47,7 @@ class RemotePathMappings extends Component { } = this.props; return ( - <FieldSet legend={translate('RemotePathMappings')} > + <FieldSet legend={translate('RemotePathMappings')}> <PageSectionContent errorMessage={translate('RemotePathMappingsLoadError')} {...otherProps} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js index c48e24903..284d1100c 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js @@ -1,5 +1,6 @@ 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'; @@ -50,9 +51,9 @@ function EditImportListExclusionModalContent(props) { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddImportListExclusionError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js index 644753ec6..059223231 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -26,7 +25,7 @@ function createImportListExclusionSelector() { items } = importListExclusions; - const mapping = id ? _.find(items, { id }) : newImportListExclusion; + const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion; const settings = selectSettings(mapping, pendingChanges, saveError); return { diff --git a/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js index b5132db42..9ed22fad2 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js @@ -44,9 +44,9 @@ class AddImportListModalContent extends Component { { !isSchemaFetching && !!schemaError ? - <div> + <Alert kind={kinds.DANGER}> {translate('AddListError')} - </div> : + </Alert> : null } diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index c8020d975..2c1ab4bb0 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -74,9 +74,9 @@ function EditImportListModalContent(props) { { !isFetching && !!error ? - <div> + <Alert kind={kinds.DANGER}> {translate('AddListError')} - </div> : + </Alert> : null } diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js index df7e34b88..75792c9ae 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js @@ -41,7 +41,7 @@ class ImportList extends Component { }); }; - onDeleteImportListModalClose= () => { + onDeleteImportListModalClose = () => { this.setState({ isDeleteImportListModalOpen: false }); }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js index 346aae650..11fcceb54 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js @@ -5,7 +5,6 @@ import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { icons } from 'Helpers/Props'; -import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import AddImportListModal from './AddImportListModal'; import EditImportListModalConnector from './EditImportListModalConnector'; @@ -67,7 +66,7 @@ class ImportLists extends Component { > <div className={styles.lists}> { - items.sort(sortByName).map((item) => { + items.map((item) => { return ( <ImportList key={item.id} diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 9e6f9cee5..2b29f6eb1 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -4,16 +4,14 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import sortByName from 'Utilities/Array/sortByName'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( - (state) => state.settings.importLists, - (importLists) => { - return { - ...importLists - }; - } + createSortedSectionSelector('settings.importLists', sortByName), + (importLists) => importLists ); } diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModal.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModal.tsx index 67a029d85..ffe295ade 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModal.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; import ManageImportListsModalContent from './ManageImportListsModalContent'; interface ManageImportListsModalProps { @@ -11,7 +12,7 @@ function ManageImportListsModal(props: ManageImportListsModalProps) { const { isOpen, onModalClose } = props; return ( - <Modal isOpen={isOpen} onModalClose={onModalClose}> + <Modal isOpen={isOpen} size={sizes.EXTRA_LARGE} onModalClose={onModalClose}> <ManageImportListsModalContent onModalClose={onModalClose} /> </Modal> ); diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx index 28d06b1dc..e518e592e 100644 --- a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx +++ b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx @@ -2,13 +2,14 @@ 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 FieldSet from 'Components/FieldSet'; 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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import { inputTypes } from 'Helpers/Props'; +import { inputTypes, kinds } from 'Helpers/Props'; import { clearPendingChanges } from 'Store/Actions/baseActions'; import { fetchImportListOptions, @@ -110,7 +111,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) { {isFetching ? <LoadingIndicator /> : null} {!isFetching && error ? ( - <div>{translate('UnableToLoadListOptions')}</div> + <Alert kind={kinds.DANGER}>{translate('ListOptionsLoadError')}</Alert> ) : null} {hasSettings && !isFetching && !error ? ( diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js index 0eb46995e..00ebbdc55 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js @@ -43,9 +43,9 @@ class AddIndexerModalContent extends Component { { !isSchemaFetching && !!schemaError && - <div> + <Alert kind={kinds.DANGER}> {translate('AddIndexerError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index 4306aa2d9..928dcf5d7 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -1,5 +1,6 @@ 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'; @@ -67,9 +68,9 @@ function EditIndexerModalContent(props) { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddIndexerError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js index ab9560338..e6c24cee8 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.js +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -42,7 +42,7 @@ class Indexer extends Component { }); }; - onDeleteIndexerModalClose= () => { + onDeleteIndexerModalClose = () => { this.setState({ isDeleteIndexerModalOpen: false }); }; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js index b92cdc479..f254ecabf 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; 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 { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import AddNotificationItem from './AddNotificationItem'; import styles from './AddNotificationModalContent.css'; @@ -39,9 +41,9 @@ class AddNotificationModalContent extends Component { { !isSchemaFetching && !!schemaError && - <div> + <Alert kind={kinds.DANGER}> {translate('AddNotificationError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index 5bfc9b5ae..83f5d257d 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -59,9 +59,9 @@ function EditNotificationModalContent(props) { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddNotificationError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js index a8ecb86f7..e2799e581 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) { { !isFetching && !!error ? - <div> + <Alert kind={kinds.DANGER}> {translate('AddDelayProfileError')} - </div> : + </Alert> : null } diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js index a1e3d85a1..3643bb158 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -33,7 +32,7 @@ function createDelayProfileSelector() { items } = delayProfiles; - const profile = id ? _.find(items, { id }) : newDelayProfile; + const profile = id ? items.find((i) => i.id === id) : newDelayProfile; const settings = selectSettings(profile, pendingChanges, saveError); return { diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index 1c129a9b3..ece0e8728 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } 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'; @@ -152,9 +153,9 @@ class EditQualityProfileModalContent extends Component { { !isFetching && !!error && - <div> + <Alert kind={kinds.DANGER}> {translate('AddQualityProfileError')} - </div> + </Alert> } { diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js index 26740b468..6e40bedad 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js @@ -55,7 +55,7 @@ class QualityProfiles extends Component { <FieldSet legend={translate('QualityProfiles')}> <PageSectionContent errorMessage={translate('QualityProfilesLoadError')} - {...otherProps}c={true} + {...otherProps} > <div className={styles.qualityProfiles}> { diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js index 64d707b5f..99442839c 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js @@ -48,7 +48,7 @@ function EditReleaseProfileModalContent(props) { <Form {...otherProps}> <FormGroup> - <FormLabel>Name</FormLabel> + <FormLabel>{translate('Name')}</FormLabel> <FormInputGroup type={inputTypes.TEXT} diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js index b14d72269..0371a1a7a 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -11,7 +10,6 @@ const newReleaseProfile = { enabled: true, required: [], ignored: [], - includePreferredWhenRenaming: false, tags: [], indexerId: 0 }; @@ -30,7 +28,7 @@ function createMapStateToProps() { items } = releaseProfiles; - const profile = id ? _.find(items, { id }) : newReleaseProfile; + const profile = id ? items.find((i) => i.id === id) : newReleaseProfile; const settings = selectSettings(profile, pendingChanges, saveError); return { diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js index 8c277e8d9..7ec97bc80 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import MiddleTruncate from 'react-middle-truncate'; @@ -43,7 +42,7 @@ class ReleaseProfile extends Component { }); }; - onDeleteReleaseProfileModalClose= () => { + onDeleteReleaseProfileModalClose = () => { this.setState({ isDeleteReleaseProfileModalOpen: false }); }; @@ -72,7 +71,7 @@ class ReleaseProfile extends Component { isDeleteReleaseProfileModalOpen } = this.state; - const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId }); + const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId); return ( <Card diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js index 01a5e846b..811c98461 100644 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -122,9 +123,9 @@ export default function EditAutoTaggingModalContent(props) { { !isFetching && !!error ? - <div> + <Alert kind={kinds.DANGER}> {translate('AddAutoTagError')} - </div> : + </Alert> : null } diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js index 454a2591a..9e06e815b 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js @@ -55,9 +55,9 @@ export default function AddSpecificationModalContent(props) { { !isSchemaFetching && !!schemaError ? - <div> + <Alert kind={kinds.DANGER}> {translate('AddConditionError')} - </div> : + </Alert> : null } diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index e7715364c..b74043fcd 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -40,7 +40,7 @@ class Tag extends Component { }); }; - onDeleteTagModalClose= () => { + onDeleteTagModalClose = () => { this.setState({ isDeleteTagModalOpen: false }); }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ba55cb50c..9ca119b76 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1955,7 +1955,6 @@ "Umask777Description": "{octal} - Everyone write", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", - "UnableToLoadListOptions": "Unable to load list options", "UnableToUpdateSonarrDirectly": "Unable to update {appName} directly,", "Unavailable": "Unavailable", "Underscore": "Underscore", From d15c116f13c79153c1b1f0ee2e2a2c48eed904a2 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:52:44 +0200 Subject: [PATCH 104/762] Fix translation keys for indexer validation --- src/NzbDrone.Core/Indexers/HttpIndexerBase.cs | 4 ++-- src/NzbDrone.Core/Localization/Core/en.json | 4 ++-- src/NzbDrone.Core/Localization/Core/fi.json | 4 ++-- src/NzbDrone.Core/Localization/Core/fr.json | 4 ++-- src/NzbDrone.Core/Localization/Core/pt_BR.json | 4 ++-- src/NzbDrone.Core/Localization/Core/zh_CN.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 42d790bf5..b62b6786b 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -377,14 +377,14 @@ namespace NzbDrone.Core.Indexers if (firstRequest == null) { - return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationJackettNoRssFeedQueryAvailable")); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationNoRssFeedQueryAvailable")); } var releases = await FetchPage(firstRequest, parser); if (releases.Empty()) { - return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationJackettNoResultsInConfiguredCategories")); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationNoResultsInConfiguredCategories")); } } catch (ApiKeyException ex) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 9ca119b76..2d3e87b8a 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -976,8 +976,8 @@ "IndexerValidationInvalidApiKey": "Invalid API Key", "IndexerValidationJackettAllNotSupported": "Jackett's all endpoint is not supported, please add indexers individually", "IndexerValidationJackettAllNotSupportedHelpText": "Jackett's all endpoint is not supported, please add indexers individually", - "IndexerValidationJackettNoResultsInConfiguredCategories": "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings.", - "IndexerValidationJackettNoRssFeedQueryAvailable": "No RSS feed query available. This may be an issue with the indexer or your indexer category settings.", + "IndexerValidationNoResultsInConfiguredCategories": "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings.", + "IndexerValidationNoRssFeedQueryAvailable": "No RSS feed query available. This may be an issue with the indexer or your indexer category settings.", "IndexerValidationQuerySeasonEpisodesNotSupported": "Indexer does not support the current query. Check if the categories and or searching for seasons/episodes are supported. Check the log for more details.", "IndexerValidationRequestLimitReached": "Request limit reached: {exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "Indexer does not support required search parameters", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index acfca2a33..5f42f19ac 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1677,7 +1677,7 @@ "FilterLessThanOrEqual": "on pienempi kuin tai sama", "FilterNotInLast": "ei kuluneina", "FilterNotInNext": "ei seuraavina", - "IndexerValidationJackettNoResultsInConfiguredCategories": "Kysely onnistui, mutta tietolähteesi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "IndexerValidationNoResultsInConfiguredCategories": "Kysely onnistui, mutta tietolähteesi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", "TvdbId": "TheTVDB ID", "DownloadClientSettingsInitialState": "Virheellinen tila", "DownloadClientSettingsRecentPriority": "Uusien painotus", @@ -1718,7 +1718,7 @@ "MaximumSingleEpisodeAgeHelpText": "Täysiä tuotantokausia etsittäessä hyväksytään vain kausipaketit, joiden uusin jakso on tätä asetusta vanhempi. Koskee vain vakiosarjoja. Poista käytöstä asettamalla arvoksi \"0\" (nolla).", "FailedToLoadSystemStatusFromApi": "Järjestelmän tilan lataus rajapinnasta epäonnistui", "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Sinun on poistettava televisiojärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", - "IndexerValidationJackettNoRssFeedQueryAvailable": "RSS-syötekyselyä ei ole käytettävissä. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "IndexerValidationNoRssFeedQueryAvailable": "RSS-syötekyselyä ei ole käytettävissä. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sinun on poistettava päiväysjärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Sinun on poistettava elokuvien järjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", "DownloadClientSettingsCategoryHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa.", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 1228f6adc..c7415bb2e 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1567,7 +1567,7 @@ "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", - "IndexerValidationJackettNoRssFeedQueryAvailable": "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.", + "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", @@ -1637,7 +1637,7 @@ "IndexerSettingsSeasonPackSeedTime": "Temps de partage pour les packs de saison", "IndexerSettingsSeasonPackSeedTimeHelpText": "Le temps pendant lequel un torrent de pack de saison doit être partagé avant de s'arrêter, laisser vide utilise la valeur par défaut du client de téléchargement", "IndexerValidationJackettAllNotSupported": "L'endpoint 'all' de Jackett n'est pas pris en charge, veuillez ajouter les indexeurs individuellement", - "IndexerValidationJackettNoResultsInConfiguredCategories": "La requête a réussi, mais aucun résultat n'a été retourné dans les catégories configurées de votre indexeur. Cela peut être un problème avec l'indexeur ou vos paramètres de catégorie de l'indexeur.", + "IndexerValidationNoResultsInConfiguredCategories": "La requête a réussi, mais aucun résultat n'a été retourné dans les catégories configurées de votre indexeur. Cela peut être un problème avec l'indexeur ou vos paramètres de catégorie de l'indexeur.", "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}.", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 37b9b2ee2..ee51db1bd 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1653,8 +1653,8 @@ "IndexerValidationCloudFlareCaptchaExpired": "O token CloudFlare CAPTCHA expirou, atualize-o.", "IndexerValidationFeedNotSupported": "O feed do indexador não é compatível: {exceptionMessage}", "IndexerValidationJackettAllNotSupportedHelpText": "Todos os endpoints de Jackett não são suportados. Adicione indexadores individualmente", - "IndexerValidationJackettNoResultsInConfiguredCategories": "Consulta bem-sucedida, mas nenhum resultado nas categorias configuradas foi retornado do seu indexador. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", - "IndexerValidationJackettNoRssFeedQueryAvailable": "Nenhuma consulta de feed RSS disponível. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", + "IndexerValidationNoResultsInConfiguredCategories": "Consulta bem-sucedida, mas nenhum resultado nas categorias configuradas foi retornado do seu indexador. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", + "IndexerValidationNoRssFeedQueryAvailable": "Nenhuma consulta de feed RSS disponível. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", "IndexerValidationRequestLimitReached": "Limite de solicitações atingido: {exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "O indexador não oferece suporte aos parâmetros de pesquisa obrigatórios", "IndexerValidationUnableToConnectResolutionFailure": "Não é possível conectar-se à falha de conexão do indexador. Verifique sua conexão com o servidor e DNS do indexador. {exceptionMessage}.", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 96d774dda..31d37cbe5 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1650,7 +1650,7 @@ "IndexerSettingsWebsiteUrl": "网址", "IndexerValidationCloudFlareCaptchaRequired": "站点受 CloudFlare CAPTCHA 保护。 需要有效的 CAPTCHA 令牌。", "IndexerValidationInvalidApiKey": "API 密钥无效", - "IndexerValidationJackettNoResultsInConfiguredCategories": "查询成功,但索引器未返回配置分类中的结果。 这可能是索引器或索引器分类设置的问题。", + "IndexerValidationNoResultsInConfiguredCategories": "查询成功,但索引器未返回配置分类中的结果。 这可能是索引器或索引器分类设置的问题。", "IndexerValidationQuerySeasonEpisodesNotSupported": "索引器不支持当前查询。 检查是否为支持分类和/或搜索季节/剧集。 检查日志以获取更多详细信息。", "IndexerValidationUnableToConnectTimeout": "可能是由于超时,无法连接到索引器。 请重试或检查您的网络设置。 {exceptionMessage}。", "IndexerSettingsPasskey": "通行密钥", @@ -1679,7 +1679,7 @@ "BlackholeFolderHelpText": "{appName} 将在其中存储 {extension} 文件的文件夹", "DownloadClientFreeboxUnableToReachFreeboxApi": "无法访问 Freebox API。 请检查“API 地址”的基础地址和版本。", "IndexerSettingsAllowZeroSize": "允许零大小", - "IndexerValidationJackettNoRssFeedQueryAvailable": "没有查询到可用的 RSS 订阅源。 这可能是索引器或索引器分类设置的问题。", + "IndexerValidationNoRssFeedQueryAvailable": "没有查询到可用的 RSS 订阅源。 这可能是索引器或索引器分类设置的问题。", "DownloadClientSabnzbdValidationDevelopVersionDetail": "运行开发版本时,{appName} 可能无法支持 SABnzbd 添加的新功能。", "DownloadClientPneumaticSettingsStrmFolderHelpText": "该文件夹中的 .strm 文件将由 drone 导入", "IndexerSettingsApiUrlHelpText": "除非您知道自己在做什么,否则请勿更改此设置。 因为您的 API Key 将被发送到该主机。", From 7dc1e47504332657143e81605bf4bc0c52a5f8bf Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:04:20 +0200 Subject: [PATCH 105/762] Fix translation token for DL client directory help text --- .../Download/Clients/DownloadStation/DownloadStationSettings.cs | 2 +- src/NzbDrone.Core/Localization/Core/de.json | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 2 +- src/NzbDrone.Core/Localization/Core/fi.json | 2 +- src/NzbDrone.Core/Localization/Core/fr.json | 2 +- src/NzbDrone.Core/Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index 6bb2d5a5a..37a3b0148 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")] public string TvCategory { get; set; } - [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientDownloadStationSettingsDirectory")] + [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientDownloadStationSettingsDirectoryHelpText")] public string TvDirectory { get; set; } public DownloadStationSettings() diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index b3ccecd05..912418807 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -350,7 +350,7 @@ "DownloadClientDelugeValidationLabelPluginInactive": "Label-Plugin nicht aktiviert", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Um Kategorien verwenden zu können, muss das Label-Plugin in {clientName} aktiviert sein.", "DownloadClientDownloadStationProviderMessage": "{appName} kann keine Verbindung zur Download Station herstellen, wenn die 2-Faktor-Authentifizierung in Ihrem DSM-Konto aktiviert ist", - "DownloadClientDownloadStationSettingsDirectory": "Optionaler freigegebener Ordner zum Ablegen von Downloads. Lassen Sie das Feld leer, um den Standardspeicherort der Download Station zu verwenden", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Optionaler freigegebener Ordner zum Ablegen von Downloads. Lassen Sie das Feld leer, um den Standardspeicherort der Download Station zu verwenden", "DownloadClientDownloadStationValidationApiVersion": "Download Station API-Version wird nicht unterstützt, sollte mindestens {requiredVersion} sein. Es unterstützt von {minVersion} bis {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "Ordner existiert nicht", "DownloadClientDownloadStationValidationFolderMissingDetail": "Der Ordner „{downloadDir}“ existiert nicht, er muss manuell im freigegebenen Ordner „{sharedFolder}“ erstellt werden.", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 2d3e87b8a..74feb6723 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -410,7 +410,7 @@ "DownloadClientDelugeValidationLabelPluginInactive": "Label plugin not activated", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "You must have the Label plugin enabled in {clientName} to use categories.", "DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", - "DownloadClientDownloadStationSettingsDirectory": "Optional shared folder to put downloads into, leave blank to use the default Download Station location", + "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}", "DownloadClientDownloadStationValidationFolderMissing": "Folder does not exist", "DownloadClientDownloadStationValidationFolderMissingDetail": "The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'.", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index fa8bb6233..83aa11ce8 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -587,7 +587,7 @@ "UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", "DownloadClientDownloadStationProviderMessage": "{appName} no puede conectarse a la Estación de descarga si la Autenticación de 2 factores está habilitada en tu cuenta de DSM", - "DownloadClientDownloadStationSettingsDirectory": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Predeterminada: 1. Se usa round-robin para clientes con la misma prioridad.", "DownloadClientDelugeValidationLabelPluginInactive": "Extensión de etiqueta no activada", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de Ratio de Compartición", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 5f42f19ac..579da4a21 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1271,7 +1271,7 @@ "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", - "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", "Table": "Taulukko", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c7415bb2e..a89f0e92b 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1649,7 +1649,7 @@ "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.", - "DownloadClientDownloadStationSettingsDirectory": "Dossier partagé facultatif dans lequel placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de Download Station", + "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", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index ee51db1bd..49bd34a10 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1529,7 +1529,7 @@ "Directory": "Diretório", "DownloadClientDelugeTorrentStateError": "Deluge está relatando um erro", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin rótulo habilitado no {clientName} para usar categorias.", - "DownloadClientDownloadStationSettingsDirectory": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporta de {minVersion} a {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "A pasta não existe", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nenhum destino padrão", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 31d37cbe5..5c0aa8e06 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1490,7 +1490,7 @@ "DownloadClientDelugeValidationLabelPluginInactive": "标签插件未激活", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} 无法将标签添加到 {clientName}。", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "你需要在 {clientName} 启用标签插件才可以使用分类。", - "DownloadClientDownloadStationSettingsDirectory": "用于存放下载内容的可选共享文件夹,留空以使用默认的 Download Station 位置", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的可选共享文件夹,留空以使用默认的 Download Station 位置", "DownloadClientDownloadStationValidationApiVersion": "Download Station API 版本不受支持,至少应为 {requiredVersion}。 它支持从 {minVersion} 到 {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "文件夹不存在", "DownloadClientDownloadStationValidationSharedFolderMissing": "共享文件夹不存在", From 9f46fc923d6e620b4924405f975688deb406593b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 12 Feb 2024 22:56:28 +0200 Subject: [PATCH 106/762] Fix typo for Downloaded Episodes Scan command name --- frontend/src/Commands/commandNames.js | 2 +- .../Folder/InteractiveImportSelectFolderModalContent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js index c2edf05bd..13ac9d62c 100644 --- a/frontend/src/Commands/commandNames.js +++ b/frontend/src/Commands/commandNames.js @@ -6,7 +6,7 @@ export const CLEAR_LOGS = 'ClearLog'; export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch'; export const DELETE_LOG_FILES = 'DeleteLogFiles'; export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles'; -export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan'; +export const DOWNLOADED_EPISODES_SCAN = 'DownloadedEpisodesScan'; export const EPISODE_SEARCH = 'EpisodeSearch'; export const INTERACTIVE_IMPORT = 'ManualImport'; export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch'; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index 8013765f9..aefda32a6 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -77,7 +77,7 @@ function InteractiveImportSelectFolderModalContent( dispatch( executeCommand({ - name: commandNames.DOWNLOADED_EPSIODES_SCAN, + name: commandNames.DOWNLOADED_EPISODES_SCAN, path: folder, }) ); From 2957b405121993a41dbd41ed2ff26d3322481fbc Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 12 Feb 2024 00:13:15 +0000 Subject: [PATCH 107/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 139d7b2dd..e69dccae3 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10046,6 +10046,9 @@ "isSeasonExtra": { "type": "boolean" }, + "isSplitEpisode": { + "type": "boolean" + }, "special": { "type": "boolean" }, @@ -12020,4 +12023,4 @@ "apikey": [ ] } ] -} +} \ No newline at end of file From d5e19b8c3c0444158228e65f023b4054ac2e9abf Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:44:24 +0200 Subject: [PATCH 108/762] Prevent useless builds --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14f77ea55..af065bb4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,14 @@ on: branches: - develop - main + paths: + - '!src/Sonarr.Api.*/openapi.json' pull_request: branches: - develop + paths: + - '!src/NzbDrone.Core/Localization/Core/**' + - '!src/Sonarr.Api.*/openapi.json' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 9ee2fe6f5c203437581816cd484b4d341238f044 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:37:42 +0200 Subject: [PATCH 109/762] Fix typo --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af065bb4f..e608bed69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,7 +112,7 @@ jobs: - name: Volta uses: volta-cli/action@v4 - - name: Yarn Intsall + - name: Yarn Install run: yarn install - name: Lint From ed27bcf213bdbc5cede650f89eb65593dc9631b4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:20:25 +0200 Subject: [PATCH 110/762] Fixed: Refresh tags state to clear removed tags by housekeeping (cherry picked from commit 2510f44c25bee6fede27d9fa2b9614176d12cb55) --- frontend/src/Settings/Tags/TagsConnector.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 770dc4720..9f8bdee5b 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import { fetchTagDetails } from 'Store/Actions/tagActions'; +import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; import Tags from './Tags'; function createMapStateToProps() { @@ -25,6 +25,7 @@ function createMapStateToProps() { } const mapDispatchToProps = { + dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchImportLists: fetchImportLists, @@ -41,6 +42,7 @@ class MetadatasConnector extends Component { componentDidMount() { const { + dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchDelayProfiles, dispatchFetchImportLists, @@ -50,6 +52,7 @@ class MetadatasConnector extends Component { dispatchFetchDownloadClients } = this.props; + dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchDelayProfiles(); dispatchFetchImportLists(); @@ -72,6 +75,7 @@ class MetadatasConnector extends Component { } MetadatasConnector.propTypes = { + dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, From 84e657482d37eed35f09c6dab3c2b8b5ebd5bac4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:46:10 +0200 Subject: [PATCH 111/762] Improve messaging on indexer specified download client is not available --- src/NzbDrone.Core/Download/DownloadClientProvider.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 8e8bcf8be..769928f2f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -59,13 +59,18 @@ namespace NzbDrone.Core.Download { var indexer = _indexerFactory.Find(indexerId); - if (indexer != null && indexer.DownloadClientId > 0) + if (indexer is { DownloadClientId: > 0 }) { var client = availableProviders.SingleOrDefault(d => d.Definition.Id == indexer.DownloadClientId); - if (client == null || (filterBlockedClients && blockedProviders.Contains(client.Definition.Id))) + if (client == null) { - throw new DownloadClientUnavailableException($"Indexer specified download client is not available"); + throw new DownloadClientUnavailableException($"Indexer specified download client does not exist for {indexer.Name}"); + } + + if (filterBlockedClients && blockedProviders.Contains(client.Definition.Id)) + { + throw new DownloadClientUnavailableException($"Indexer specified download client is not available due to recent failures for {indexer.Name}"); } return client; From c0b17d9345367ab6500b7cca6bb70c1e3b930284 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:05:01 +0200 Subject: [PATCH 112/762] Show download client ID as hint in select options --- .../src/Components/Form/DownloadClientSelectInputConnector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index f0ebf534b..fb0430f19 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -26,7 +26,8 @@ function createMapStateToProps() { const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { return { key: downloadClient.id, - value: downloadClient.name + value: downloadClient.name, + hint: `(${downloadClient.id})` }; }); From 75535e61d9b9b0796412889181117b784a02e751 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:10:41 +0200 Subject: [PATCH 113/762] Fixed: Reprocessing custom formats for file in Manual Import --- .../MediaFiles/EpisodeImport/Manual/ManualImportService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 67c4b1d9f..19e7205e4 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -169,6 +169,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup; localEpisode.Languages = (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages; localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); + localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null); } From 965e7c22d948fe079d24a35cc30c17d995a162f1 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:25:20 +0200 Subject: [PATCH 114/762] Fixed: Reprocessing multi-language file in Manage Episodes --- .../Manual/ManualImportService.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 19e7205e4..bd5393d4a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -167,7 +167,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.ExistingFile = series.Path.IsParentPath(path); localEpisode.Size = _diskProvider.GetFileSize(path); localEpisode.ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup; - localEpisode.Languages = (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages; + localEpisode.Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages; localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; @@ -183,22 +183,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem; var localEpisode = new LocalEpisode - { - Series = series, - Episodes = new List<Episode>(), - FileEpisodeInfo = Parser.Parser.ParsePath(path), - DownloadClientEpisodeInfo = downloadClientItem == null - ? null - : Parser.Parser.ParseTitle(downloadClientItem.Title), - DownloadItem = downloadClientItem, - Path = path, - SceneSource = SceneSource(series, rootFolder), - ExistingFile = series.Path.IsParentPath(path), - Size = _diskProvider.GetFileSize(path), - ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, - Languages = (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, - Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality - }; + { + Series = series, + Episodes = new List<Episode>(), + FileEpisodeInfo = Parser.Parser.ParsePath(path), + DownloadClientEpisodeInfo = downloadClientItem == null + ? null + : Parser.Parser.ParseTitle(downloadClientItem.Title), + DownloadItem = downloadClientItem, + Path = path, + SceneSource = SceneSource(series, rootFolder), + ExistingFile = series.Path.IsParentPath(path), + Size = _diskProvider.GetFileSize(path), + 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 + }; return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); } From b0829d553704140b885c3a8013edb7884a5771cf Mon Sep 17 00:00:00 2001 From: Qstick <qstick@gmail.com> Date: Sun, 11 Feb 2024 20:53:59 -0600 Subject: [PATCH 115/762] Fixed: Correctly persist calendar custom filter selection ignore-downstream --- frontend/src/Store/Actions/calendarActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index 4fece9523..5ff9f7126 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -97,7 +97,7 @@ export const persistState = [ 'calendar.view', 'calendar.selectedFilterKey', 'calendar.options', - 'seriesIndex.customFilters' + 'calendar.customFilters' ]; // From f1d343218cdbd5a63abeb2eb97bba1105dc8035d Mon Sep 17 00:00:00 2001 From: abcasada <40399292+abcasada@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:07:41 -0600 Subject: [PATCH 116/762] Hints for week column and short dates in UI settings (cherry picked from commit 4558f552820b52bb1f9cd97fdabe03654ce9924a) --- frontend/src/Settings/UI/UISettings.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index beccce918..963967f15 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -31,19 +31,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25' }, - { key: 'ddd MM/DD', value: 'Tue 03/25' }, - { key: 'ddd D/M', value: 'Tue 25/3' }, - { key: 'ddd DD/MM', value: 'Tue 25/03' } + { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, + { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, + { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, + { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } ]; const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014' }, - { key: 'MM/D/YYYY', value: '03/25/2014' }, - { key: 'MM/DD/YYYY', value: '03/25/2014' }, - { key: 'DD/MM/YYYY', value: '25/03/2014' }, - { key: 'YYYY-MM-DD', value: '2014-03-25' } + { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, + { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, + { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, + { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, + { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } ]; const longDateFormatOptions = [ From 39575b1248d5de47038c5816ff54634ccd36542e Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 13 Feb 2024 17:28:02 +0000 Subject: [PATCH 117/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: David13467 <davidnow00@gmail.com> Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Hicabi Erdem <bilgi@hicabierdem.com> Co-authored-by: Lucas <sixagag973@fkcod.com> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Steve Hansen <steve@hansenconsultancy.be> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: aghus <aghus.m@outlook.com> Co-authored-by: bai0012 <baicongrui@gmail.com> Co-authored-by: bogdan-rgb <b.hmelniczky@yandex.ru> Co-authored-by: savin-msk <ns@a77.io> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 176 +++++++++++--- src/NzbDrone.Core/Localization/Core/fi.json | 195 +++++++++++----- src/NzbDrone.Core/Localization/Core/hu.json | 220 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/it.json | 5 +- src/NzbDrone.Core/Localization/Core/nl.json | 21 +- .../Localization/Core/pt_BR.json | 52 ++--- src/NzbDrone.Core/Localization/Core/ru.json | 18 +- src/NzbDrone.Core/Localization/Core/tr.json | 57 ++++- .../Localization/Core/zh_CN.json | 3 +- 9 files changed, 616 insertions(+), 131 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 83aa11ce8..e2091514a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -39,10 +39,10 @@ "ApiKey": "Clave de API", "AnimeEpisodeFormat": "Formato de Episodio de Anime", "ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base", - "ApplyTagsHelpTextReplace": "Reemplazar: Reemplazar las etiquetas con las etiquetas introducidas (no introducir etiquetas para eliminar todas las etiquetas)", + "ApplyTagsHelpTextReplace": "Reemplazar: Sustituye las etiquetas por las introducidas (introduce \"no tags\" para borrar todas las etiquetas)", "ApplicationURL": "URL de la aplicación", "Authentication": "Autenticación", - "AuthForm": "Formularios (página de inicio de sesión)", + "AuthForm": "Formularios (Página de inicio de sesión)", "AuthenticationMethodHelpText": "Requerir nombre de usuario y contraseña para acceder {appName}", "AuthenticationRequired": "Autenticación requerida", "AutoTaggingLoadError": "No se pudo cargar el etiquetado automático", @@ -111,7 +111,7 @@ "Yes": "Sí", "ApplyTagsHelpTextHowToApplyDownloadClients": "Cómo añadir etiquetas a los clientes de descargas seleccionados", "ApplyTagsHelpTextHowToApplyImportLists": "Cómo añadir etiquetas a las listas de importación seleccionadas", - "ApplyTagsHelpTextHowToApplyIndexers": "Cómo añadir etiquetas a los indexadores seleccionados", + "ApplyTagsHelpTextHowToApplyIndexers": "Cómo aplicar etiquetas a los indexadores seleccionados", "ApplyTagsHelpTextHowToApplySeries": "Cómo añadir etiquetas a las series seleccionadas", "Series": "Series", "Folder": "Carpeta", @@ -129,8 +129,8 @@ "Episode": "Episodio", "Activity": "Actividad", "AddNew": "Añadir Nuevo", - "ApplyTagsHelpTextAdd": "Añadir: Añadir las etiquetas la lista existente de etiquetas", - "ApplyTagsHelpTextRemove": "Eliminar: Eliminar las etiquetas introducidas", + "ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente", + "ApplyTagsHelpTextRemove": "Eliminar: Elimina las etiquetas introducidas", "Blocklist": "Lista de bloqueos", "Grabbed": "Añadido", "Genres": "Géneros", @@ -217,7 +217,7 @@ "Absolute": "Absoluto", "AddANewPath": "Añadir una nueva ruta", "AddConditionImplementation": "Añadir Condición - {implementationName}", - "AppUpdated": "{appName} Actualizada", + "AppUpdated": "{appName} Actualizado", "AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script", "AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.", "AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación esté habilitada. Opcionalmente puede desactivar la autenticación desde una dirección local.", @@ -228,7 +228,7 @@ "AddDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", "VideoDynamicRange": "Video de Rango Dinámico", "AuthenticationMethodHelpTextWarning": "Por favor selecciona un método válido de autenticación", - "AddCustomFilter": "Añadir filtro personalizado", + "AddCustomFilter": "Añadir Filtro Personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Puntuación mínima de formato personalizado", "CountIndexersSelected": "{count} indexador(es) seleccionado(s)", "CouldNotFindResults": "No se pudieron encontrar resultados para '{term}'", @@ -246,16 +246,16 @@ "DeleteQualityProfileMessageText": "¿Seguro que quieres eliminar el perfil de calidad {name}?", "DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{path}'?", "DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {count} cliente(s) de descarga seleccionado(s)?", - "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y necesitará ser recargada para restaurar su funcionalidad.", + "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.", "CalendarOptions": "Opciones de Calendario", "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", "Default": "Por defecto", "DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{name}'?", "DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?", "AddImportListImplementation": "Añadir lista de importación - {implementationName}", - "AddIndexerImplementation": "Añadir Indexador - {implementationName}", + "AddIndexerImplementation": "Agregar Indexador - {implementationName}", "AutoRedownloadFailed": "Descarga fallida", - "ConnectionLostReconnect": "Radarr intentará conectarse automáticamente, o haz clic en el botón de recarga abajo.", + "ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puede hacer clic en recargar abajo.", "CustomFormatJson": "Formato JSON personalizado", "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", "DeleteImportList": "Eliminar Lista(s) de Importación", @@ -266,7 +266,7 @@ "DeleteSelectedImportListsMessageText": "Seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", "DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada", "DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?", - "DisabledForLocalAddresses": "Desactivado para direcciones locales", + "DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales", "DeletedReasonManual": "El archivo fue borrado por vía UI", "ClearBlocklist": "Limpiar lista de bloqueos", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", @@ -370,7 +370,7 @@ "AllFiles": "Todos los archivos", "Any": "Cualquiera", "AirsTomorrowOn": "Mañana a las {hora} en {networkLabel}", - "AppUpdatedVersion": "{appName} ha sido actualizada a la version `{version}`, para ver los cambios necesitara recargar {appName} ", + "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", "CalendarLegendEpisodeDownloadingTooltip": "El episodio se esta descargando actualmente", @@ -402,10 +402,10 @@ "FormatDateTime": "{formattedDate} {formattedTime}", "DownloadIgnored": "Descarga ignorada", "EditImportListImplementation": "Añadir lista de importación - {implementationName}", - "EditIndexerImplementation": "Editar indexador - {implementationName}", + "EditIndexerImplementation": "Editar Indexador - {implementationName}", "EnableProfile": "Habilitar perfil", "False": "Falso", - "EditDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", + "EditDownloadClientImplementation": "Editar Cliente de Descarga - {implementationName}", "FormatAgeMinutes": "minutos", "UnknownEventTooltip": "Evento desconocído", "DownloadWarning": "Alerta de descarga: {warningMessage}", @@ -433,7 +433,7 @@ "CountSelectedFile": "{selectedCount} archivo seleccionado", "CountSeriesSelected": "{count} serie seleccionada", "CreateGroup": "Crea un grupo", - "CustomFormatsLoadError": "No se pueden cargar formatos personalizados", + "CustomFormatsLoadError": "No se pudo cargar Formatos Personalizados", "CustomFormatsSettings": "Configuración de formatos personalizados", "CustomFormatsSettingsSummary": "Formatos y configuraciones personalizados", "CutoffUnmet": "Umbrales no alcanzados", @@ -442,7 +442,7 @@ "DelayMinutes": "{delay} Minutos", "Continuing": "Continua", "CustomFormats": "Formatos personalizados", - "AddRootFolderError": "No se puede agregar la carpeta raíz", + "AddRootFolderError": "No se pudoagregar la carpeta raíz", "CollapseAll": "Desplegar todo", "DailyEpisodeTypeDescription": "Episodios publicados diariamente o con menos frecuencia que utilizan año, mes y día (2023-08-04)", "DefaultNotFoundMessage": "Debes estar perdido, no hay nada que ver aquí.", @@ -463,7 +463,7 @@ "InteractiveImportLoadError": "No se pueden cargar elementos de la importación manual", "InteractiveImportNoFilesFound": "No se han encontrado archivos de vídeo en la carpeta seleccionada", "InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado", - "InteractiveSearchModalHeader": "Búsqueda interactiva", + "InteractiveSearchModalHeader": "Búsqueda Interactiva", "InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración", "ChownGroup": "Cambiar grupo propietario", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", @@ -514,7 +514,7 @@ "DeleteSpecificationHelpText": "Esta seguro que desea borrar la especificacion '{name}'?", "DeleteSelectedIndexers": "Borrar indexer(s)", "DeleteIndexer": "Borrar Indexer", - "DeleteSelectedDownloadClients": "Borrar gestor de descarga(s)", + "DeleteSelectedDownloadClients": "Borrar Cliente de Descarga(s)", "DeleteSeriesFolderCountConfirmation": "Esta seguro que desea eliminar '{count}' series seleccionadas?", "DeleteSeriesFolders": "Eliminar directorios de series", "DeletedSeriesDescription": "Serie fue eliminada de TheTVDB", @@ -586,9 +586,9 @@ "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.", "UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", - "DownloadClientDownloadStationProviderMessage": "{appName} no puede conectarse a la Estación de descarga si la Autenticación de 2 factores está habilitada en tu cuenta de DSM", + "DownloadClientDownloadStationProviderMessage": "{appName} no pudo conectarse a la Estación de Descarga si la Autenticación de 2 factores está habilitada en su cuenta de DSM", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", - "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Predeterminada: 1. Se usa round-robin para clientes con la misma prioridad.", + "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Por defecto: 1. Se usa Round-Robin para clientes con la misma prioridad.", "DownloadClientDelugeValidationLabelPluginInactive": "Extensión de etiqueta no activada", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de Ratio de Compartición", "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Una vez alcanzada esta puntuación de formato personalizada {appName} no capturará más lanzamientos de episodios", @@ -636,7 +636,7 @@ "DownloadClientValidationUnableToConnect": "No es posible conectarse a {clientName}", "DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para actualizaciones automáticas a propers/repacks", "DownloadStationStatusExtracting": "Extrayendo: {progress}%", - "EditConnectionImplementation": "Editar conexión - {implementationName}", + "EditConnectionImplementation": "Editar Conexión - {implementationName}", "EnableInteractiveSearch": "Habilitar Búsqueda Interactiva", "EnableInteractiveSearchHelpText": "Se usará cuando se utilice la búsqueda interactiva", "DoneEditingGroups": "Terminado de editar grupos", @@ -657,7 +657,7 @@ "DownloadClientQbittorrentValidationCategoryRecommended": "Se recomienda una categoría", "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 Manejo de descargas completadas como configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'", + "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", @@ -740,7 +740,7 @@ "EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas", "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador", "EnableRss": "Habilitar RSS", - "Ended": "Finalizado", + "Ended": "Terminado", "EpisodeFileRenamed": "Archivo de episodio renombrado", "EpisodeFileRenamedTooltip": "Archivo de episodio renombrado", "EpisodeFileMissingTooltip": "Archivo de episodio faltante", @@ -954,7 +954,7 @@ "ImportListsSonarrSettingsFullUrl": "URL completa", "ImportListsSonarrSettingsQualityProfilesHelpText": "Perfiles de calidad de la instancia de la fuente de la que importar", "ImportListsSonarrSettingsRootFoldersHelpText": "Carpetas raíz de la instancia de la fuente de la que importar", - "ImportListsTraktSettingsAdditionalParameters": "Parámetros adicionales", + "ImportListsTraktSettingsAdditionalParameters": "Parámetros Adicionales", "ImportListsTraktSettingsAdditionalParametersHelpText": "Parámetros adicionales de la API de Trakt", "ImportListsTraktSettingsAuthenticateWithTrakt": "Autenticar con Trakt", "ImportListsTraktSettingsGenresHelpText": "Filtrar series por género de Trakt (separados por coma) solo para listas populares", @@ -1051,7 +1051,7 @@ "ImportListsAniListSettingsImportWatchingHelpText": "Lista: Viendo actualmente", "ImportListsAniListSettingsImportNotYetReleased": "Importar Aún sin lanzar", "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} de la que importar", - "IndexerPriorityHelpText": "Prioridad del indexador desde 1 (la más alta) hasta 50 (la más baja). Predeterminado: 25. Usada para desempatar lanzamientos iguales cuando se capturan, {appName} seguirá usando todos los indexadores habilitados para Sincronización de RSS y Búsqueda.", + "IndexerPriorityHelpText": "Prioridad del Indexador de 1 (la más alta) a 50 (la más baja). Por defecto: 25. Usada para desempatar lanzamientos iguales cuando se capturan, {appName} seguirá usando todos los indexadores habilitados para Sincronización de RSS y Búsqueda", "IncludeHealthWarnings": "Incluir avisos de salud", "IndexerJackettAllHealthCheckMessage": "Indexadores usan el endpoint de Jackett no soportado 'todo': {indexerNames}", "HourShorthand": "h", @@ -1066,5 +1066,129 @@ "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hay indexadores disponibles con la sincronización RSS activada, {appName} no capturará nuevos estrenos automáticamente", "IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas", "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" + "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", + "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", + "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}", + "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", + "IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores", + "IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.", + "NotificationsDiscordSettingsAuthor": "Autor", + "IndexerSettingsApiUrl": "URL de la API", + "NoIndexersFound": "No se han encontrado indexadores", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Ten en cuenta que si cambias la categoría tendrás que añadir reglas requeridas/restringidas sobre los subgrupos para evitar ediciones en idiomas extranjeros.", + "IndexerSettingsApiUrlHelpText": "No cambie esto a menos que sepa lo que está haciendo. Ya que tu clave API será enviada a ese host.", + "NoDownloadClientsFound": "No se han encontrado clientes de descarga", + "IndexerSettingsAnimeCategories": "Categorías de Anime", + "IndexerSettingsMinimumSeeders": "Semillas mínimas", + "IndexerSettingsRssUrl": "URL de RSS", + "IndexerSettingsAnimeCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar anime", + "IndexerSettingsApiPath": "Ruta de la 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", + "IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.", + "NotificationsEmailSettingsUseEncryption": "Usar Cifrado", + "LastDuration": "Última Duración", + "LastExecution": "Última Ejecución", + "IndexerSettingsCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar los programas estándar/diarios", + "IndexerSettingsWebsiteUrl": "URL del Sitio Web", + "OnHealthRestored": "Al resolver las incidencias", + "KeyboardShortcutsConfirmModal": "Aceptar Confirmación de esta Ventana Modal", + "IndexerValidationUnableToConnectHttpError": "No se puede conectar al indexador, por favor compruebe su configuración DNS y asegúrese de que IPv6 está funcionando o que esté desactivado. {exceptionMessage}.", + "InteractiveImportNoSeason": "Se debe elegir la temporada para cada archivo seleccionado", + "Interval": "Intervalo", + "KeyboardShortcuts": "Atajos de Teclado", + "Level": "Nivel", + "LastWriteTime": "Última Fecha de Escritura", + "InteractiveSearchModalHeaderSeason": "Búsqueda Interactiva - {season}", + "InteractiveSearchSeason": "Búsqueda interactiva de todos los episodios de esta temporada", + "Language": "Idioma", + "SaveSettings": "Guardar ajustes", + "InstanceName": "Nombre de la Instancia", + "KeyboardShortcutsCloseModal": "Cerrar esta Ventana Modal", + "IndexerValidationJackettAllNotSupportedHelpText": "Todos los endpoint the Jackett no están soportados, por favor agregue los indexadores individualmente", + "IndexerValidationUnableToConnect": "No se pudo conectar con el indexador: {exceptionMessage}. Compruebe el registro que rodea a este error para obtener más detalles", + "InteractiveImportNoSeries": "Se debe elegir una serie para cada archivo seleccionado", + "InteractiveSearch": "Búsqueda Interactiva", + "LiberaWebchat": "Libera Webchat", + "IndexerValidationFeedNotSupported": "La fuente del indexador no es compatible: {exceptionMessage}", + "IndexerValidationInvalidApiKey": "Clave API Invalida", + "IndexerValidationJackettAllNotSupported": "Todos los endpoint the Jackett no están soportados, por favor agregue los indexadores individualmente", + "IndexerValidationSearchParametersNotSupported": "El indexador no admite los parámetros de búsqueda requeridos", + "IndexerValidationQuerySeasonEpisodesNotSupported": "El indexador no admite la consulta actual. Comprueba si las categorías y/o la búsqueda de temporadas/episodios son compatibles. Comprueba el registro para obtener más detalles.", + "IndexersLoadError": "No se pueden cargar los indexadores", + "IndexersSettingsSummary": "Indexadores y opciones de indexación", + "InvalidFormat": "Formato Inválido", + "IndexerValidationUnableToConnectTimeout": "No se ha podido conectar con el indexador, posiblemente debido a un tiempo de espera excedido. Inténtalo de nuevo o comprueba tu configuración de red. {exceptionMessage}.", + "InteractiveSearchResultsSeriesFailedErrorMessage": "La búsqueda ha fallado porque es {message}. Intente actualizar la información de la serie y compruebe que la información necesaria está presente antes de volver a buscar.", + "IndexerValidationJackettNoResultsInConfiguredCategories": "La solicitud se ha realizado correctamente, pero no se han devuelto resultados en las categorías configuradas en su indexador. Esto puede ser un problema con el indexador o con la configuración de categorías de tu indexador.", + "IndexerValidationUnableToConnectResolutionFailure": "No se puede conectar con el indexador, fallo de conexión. Compruebe su conexión con el servidor del indexador y DNS. {exceptionMessage}.", + "InstallLatest": "Instala el último", + "InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog", + "LastUsed": "Usado por última vez", + "InteractiveImportNoEpisode": "Hay que elegir uno o varios episodios para cada archiveo seleccionado", + "Indexers": "Indexadores", + "InteractiveImportNoLanguage": "Se debe elegir el idioma para cada archivo seleccionado", + "KeepAndTagSeries": "Conservar y Etiquetar Series", + "KeepAndUnmonitorSeries": "Conservar y Desmonitorizar Series", + "KeyboardShortcutsFocusSearchBox": "Enfocar Campo de Búsqueda", + "KeyboardShortcutsOpenModal": "Abrir esta Ventana Modal", + "Languages": "Idiomas", + "LibraryImportSeriesHeader": "Importe las series que ya posee", + "LibraryImportTips": "Algunos consejos para que la importación vaya sobre ruedas:", + "IndexerValidationTestAbortedDueToError": "El test fue abortado debido a un error: {exceptionMessage}", + "Large": "Grande", + "IndexerValidationJackettNoRssFeedQueryAvailable": "No hay consulta de fuente RSS disponible. Puede tratarse de un problema con el indexador o con la configuración de la categoría del indexador.", + "IndexerValidationUnableToConnectInvalidCredentials": "No se puede conectar al indexador, credenciales no válidas. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "No se puede conectar con el indexador, el servidor del indexador no está disponible. Inténtelo de nuevo más tarde. {exceptionMessage}.", + "InteractiveImport": "Importación Interactiva", + "KeyboardShortcutsSaveSettings": "Guardar ajustes", + "LatestSeason": "Última Temporada", + "ListTagsHelpText": "Etiquetas que se añadirán en la importación a partir de esta lista", + "ManageEpisodes": "Gestionar Episodios", + "Lowercase": "Minúscula", + "ListQualityProfileHelpText": "Los elementos de la lista del Perfil de Calidad se añadirán con", + "ListExclusionsLoadError": "No se puede cargar Lista de Exclusiones", + "ListRootFolderHelpText": "Los elementos de la lista de carpetas raíz se añadirán a", + "Local": "Local", + "LocalStorageIsNotSupported": "El Almacenamiento Local no es compatible o está deshabilitado. Es posible que un plugin o la navegación privada lo hayan deshabilitado.", + "ListOptionsLoadError": "No se pueden cargar opciones de lista", + "ListsLoadError": "No se pueden cargar las Listas", + "Logout": "Cerrar Sesión", + "Links": "Enlaces", + "Logging": "Registro de eventos", + "ListWillRefreshEveryInterval": "La lista se actualizará cada {refreshInterval}", + "LocalPath": "Ruta Local", + "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'.", + "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", + "ManageEpisodesSeason": "Gestionar los archivos de Episodios de esta temporada", + "LibraryImportTipsDontUseDownloadsFolder": "No lo utilice para importar descargas desde su cliente de descargas, esto es sólo para bibliotecas organizadas existentes, no para archivos sin clasificar.", + "LocalAirDate": "Fecha de emisión local", + "NotificationsEmailSettingsUseEncryptionHelpText": "Si prefiere utilizar el cifrado si está configurado en el servidor, utilizar siempre el cifrado mediante SSL (sólo puerto 465) o StartTLS (cualquier otro puerto) o no utilizar nunca el cifrado", + "LibraryImportTipsSeriesUseRootFolder": "Dirija {appName} a la carpeta que contiene todas sus series de TV, no a una en concreto. Por ejemplo, \"`{goodFolderExample}`\" y no \"`{badFolderExample}`\". Además, cada serie debe estar en su propia carpeta dentro de la carpeta raíz/biblioteca.", + "ListSyncTag": "Etiqueta de Sincronización de Lista", + "ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)", + "UnableToLoadListOptions": "No se pueden cargar opciones de lista" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 579da4a21..1766747ba 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -33,7 +33,7 @@ "Languages": "Kielet", "MultiLanguages": "Monikielinen", "Permissions": "Käyttöoikeudet", - "RestartRequiredWindowsService": "Jotta palvelu käynnistyisi automaattisesti, voi suorittavasta käyttäjästä riippuen olla tarpeellista suorittaa sovellus kerran järjestelmänvalvojan oikeuksilla.", + "RestartRequiredWindowsService": "Jotta palvelu käynnistyisi automaattisesti, voi suorittavasta käyttäjästä riippuen olla tarpeellista suorittaa {appName} kerran järjestelmänvalvojan oikeuksilla.", "SelectLanguageModalTitle": "{modalTitle} - Valitse kieli", "SelectLanguage": "Valitse kieli", "SelectLanguages": "Valitse kielet", @@ -44,14 +44,14 @@ "TimeFormat": "Kellonajan esitys", "UiLanguage": "Käyttöliittymän kieli", "UiLanguageHelpText": "{appName}in käyttöliittymän kieli.", - "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 skriptillä.", + "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.", "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. ", - "ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasua. \"Automaattinen\" vaihtaa vaalean ja tumman tilan välillä järjestelmän teeman mukaan. Innoittanut Theme.Park.", + "ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasua. \"Automaattinen\" vaihtaa vaalean ja tumman tilan välillä käyttöjärjestelmän teeman mukaan. Innoittanut Theme.Park.", "AnalyticsEnabledHelpText": "Lähetä nimettömiä käyttö- ja virhetietoja {appName}in palvelimille. Tämä sisältää tietoja selaimestasi, käyttöliittymän sivujen käytöstä, virheraportoinnista, käyttöjärjestelmästä ja suoritusalustasta. Käytämme näitä tietoja ominaisuuksien ja vikakorjausten painotukseen.", "EnableColorImpairedMode": "Heikentyneen värinäön tila", "EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihauille, jotka suoritetaan käyttöliittymästä tai {appName}in toimesta.", @@ -117,7 +117,7 @@ "IncludeUnmonitored": "Sisällytä valvomattomat", "QualityDefinitions": "Laatumääritykset", "QueueIsEmpty": "Jono on tyhjä", - "QueueLoadError": "Virhe ladattaessa jonoa", + "QueueLoadError": "Jonon lataus epäonnistui", "RemoveQueueItemConfirmation": "Haluatko varmasti poistaa kohteen \"{sourceTitle}\" jonosta?", "SourcePath": "Lähdesijainti", "Imported": "Tuotu", @@ -155,7 +155,7 @@ "DeleteTagMessageText": "Haluatko varmasti poistaa tunnisteen \"{label}\"?", "DownloadClientRootFolderHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset juurikansioon \"{rootFolderPath}\", mutta ne tulisi tallentaa muualle.", "EditImportListExclusion": "Muokkaa tuontilistapoikkeusta", - "EnableMediaInfoHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvataa levyn ja/tai verkon kuormitusta tarkistusten aikana.", + "EnableMediaInfoHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn ja/tai verkon kuormitusta tarkistusten aikana.", "HistoryLoadError": "Virhe ladattaessa historiaa", "Import": "Tuo", "DownloadClientQbittorrentSettingsContentLayout": "Sisällön rakenne", @@ -171,7 +171,7 @@ "EpisodeProgress": "Jaksotilanne", "FilterSeriesPlaceholder": "Suodata sarjoja", "GeneralSettingsSummary": "Portti, SSL-salaus, käyttäjätunnus ja salasana, välityspalvelin, analytiikka ja päivitykset.", - "GeneralSettingsLoadError": "Virhe ladattaessa yleisasetuksia", + "GeneralSettingsLoadError": "Virhe ladattaessa yleisiä asetuksia", "ImportCountSeries": "Tuo {selectedCount} sarjaa", "ICalTagsSeriesHelpText": "Vain vähintään yhdellä täsmäävällä tunnisteella merkityt sarjat sisällytetään syötteeseen.", "IndexerHDBitsSettingsMediumsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", @@ -180,7 +180,7 @@ "IndexerSettingsAllowZeroSize": "Salli nollakoko", "IndexerSettingsAdditionalParametersNyaa": "Muut parametrit", "IndexerSettingsAnimeCategories": "Animekategoriat", - "IndexerSettingsApiUrl": "API:n URL-osoite", + "IndexerSettingsApiUrl": "Rajapinnan URL-osoite", "IndexerSettingsCookie": "Eväste", "IndexerSettingsPasskey": "Suojausavain", "IndexerTagSeriesHelpText": "Tietolähdettä käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", @@ -199,7 +199,7 @@ "IndexerSettingsCategories": "Kategoriat", "IndexerSettingsSeedRatio": "Jakosuhde", "IndexerSettingsWebsiteUrl": "Verkkosivuston URL-osoite", - "IndexerValidationInvalidApiKey": "Virheellinen API-avain", + "IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", "IndexersLoadError": "Virhe ladattaessa tietolähteitä", "IndexersSettingsSummary": "Tietolähteet ja niiden asetukset.", "Indexers": "Tietolähteet", @@ -211,7 +211,7 @@ "MediaManagementSettingsLoadError": "Virhe ladattaessa mediatiedostojen hallinta-asetuksia", "MaximumSize": "Enimmäiskoko", "MaximumSizeHelpText": "Kaapattavien julkaisujen enimmäiskoko megatavuina. Arvo \"0\" (nolla) poistaa rajoituksen.", - "MinimumFreeSpaceHelpText": "Estä tuonti, jos sen jälkeinen vapaa levytila olisi tässä määritettyä arvoa pienempi.", + "MinimumFreeSpaceHelpText": "Estä tuonti, jos sen jälkeinen vapaa levytila olisi tässä määritettyä pienempi.", "MinutesSixty": "60 minuuttia: {sixty}", "MediaInfo": "Median tiedot", "MonitorAllEpisodesDescription": "Valvo erikoisjaksoja lukuunottamatta kaikkia jaksoja.", @@ -253,7 +253,7 @@ "SceneInformation": "Kohtaustiedot", "SelectFolderModalTitle": "{modalTitle} - Valitse kansio(t)", "SelectQuality": "Valitse laatu", - "SeriesFolderFormatHelpText": "Käytetään lisättäessä uutta sarjaa tai siirrettäessä sarjaa sarjaeditorin välityksellä.", + "SeriesFolderFormatHelpText": "Käytetään kun lisätään uusi sarja tai siirretään sarjoja sarjaeditorin avulla.", "SeriesIndexFooterMissingMonitored": "Jaksoja puuttuu (sarjaa valvotaan)", "ShowPreviousAiring": "Näytä edellinen esitys", "ShowBannersHelpText": "Korvaa nimet bannerikuvilla.", @@ -276,7 +276,7 @@ "QualityProfiles": "Laatuprofiilit", "QualityProfileInUseSeriesListCollection": "Sarjaan, listaan tai kokoelmaan liitettyä laatuprofiilia ei ole mahdollista poistaa.", "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", - "RecentChanges": "Viimeisimmät muutokset", + "RecentChanges": "Uusimmat muutokset", "ReleaseProfileTagSeriesHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", "ReleaseTitle": "Julkaisun nimike", "Reload": "Lataa uudelleen", @@ -337,7 +337,7 @@ "Type": "Tyyppi", "TypeOfList": "{typeOfList}-lista", "Twitter": "X (Twitter)", - "UiSettingsSummary": "Kalenterin, päiväyksen ja värirajoitteisten asetukset.", + "UiSettingsSummary": "Kalenterin, päiväyksen ja kellonajan, sekä kielen ja heikentyneelle värinäölle sopivan tilan asetukset.", "UnmappedFolders": "Kohdistamattomat kansiot", "UnmonitorSpecialEpisodes": "Älä valvo erikoisjaksoja", "UnmonitorSpecialsEpisodesDescription": "Lopeta kaikkien erikoisjaksojen valvonta muuttamatta muiden jakosojen tilaa.", @@ -408,14 +408,14 @@ "Discord": "Discord", "DownloadClientFloodSettingsPostImportTags": "Tuonnin jälkeiset tunnisteet", "DownloadClientDownloadStationValidationSharedFolderMissing": "Jaettua kansiota ei ole olemassa", - "DownloadClientFreeboxSettingsAppId": "Sovelluksen ID", - "DownloadClientFreeboxSettingsApiUrl": "API:n URL-osoite", - "DownloadClientFreeboxSettingsAppToken": "Sovellustunniste", - "DownloadClientFreeboxUnableToReachFreebox": "Freebox API:a ei tavoiteta. Tarkista \"Osoite\", \"Portti\" ja \"Käytä SSL-salausta\" -asetukset. (Virhe: {exceptionMessage})", + "DownloadClientFreeboxSettingsAppId": "Sovellustunniste", + "DownloadClientFreeboxSettingsApiUrl": "Rajapinnan URL-osoite", + "DownloadClientFreeboxSettingsAppToken": "Sovellustietue", + "DownloadClientFreeboxUnableToReachFreebox": "Freebox-rajapintaa ei tavoiteta. Tarkista \"Osoite\"-, \"Portti\"- ja \"Käytä SSL-salausta\"-asetukset. Virhe: {exceptionMessage}.", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Lataa tiedostot järjestyksessä (qBittorrent 4.1.0+).", - "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd develop -versio, oletettavasti versio 3.0.0 tai korkeampi.", + "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd:n develop-versio, oletettavasti vähintään versio 3.0.0.", "Daily": "Päivittäinen", - "CutoffUnmetNoItems": "Katkaisutasoa saavuttamattomia kohteita ei ole", + "CutoffUnmetNoItems": "Katkaisutasoa saavuttamattomia kohteita ei ole.", "DailyEpisodeFormat": "Päivittäisjaksojen kaava", "DelayProfileProtocol": "Protokolla: {preferredProtocol}", "Day": "Päivä", @@ -492,7 +492,7 @@ "DeletedSeriesDescription": "Sarja poistettiin TheTVDB:stä.", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", "NotificationStatusSingleClientHealthCheckMessage": "Ilmoitukset eivät ole ongelmien vuoksi käytettävissä: {notificationNames}", - "NotificationsLoadError": "Kytkösten lataus epäonnistui.", + "NotificationsLoadError": "Virhe ladattaessa kytköksiä", "Options": "Asetukset", "OptionalName": "Valinnainen nimi", "OverviewOptions": "Yleiskatsauksen asetukset", @@ -520,7 +520,7 @@ "RescanAfterRefreshHelpTextWarning": "{appName} ei tunnista tiedostomuutoksia automaattisesti, jos asetuksena ei ole \"Aina\".", "RequiredHelpText": "Tämän \"{implementationName}\" -ehdon on täsmättävä mukautetun muodon käyttämiseksi. Muutoin riittää yksi \"{implementationName}\" -vastaavuus.", "RescanSeriesFolderAfterRefresh": "Tarkista sarjan kansio päivityksen jälkeen", - "ResetAPIKeyMessageText": "Haluatko varmasti uudistaa API-avaimesi?", + "ResetAPIKeyMessageText": "Haluatko varmasti korvata rajapinnan avaimen uudella?", "ResetDefinitionTitlesHelpText": "Palauta määritysten nimet ja arvot.", "ResetQualityDefinitionsMessageText": "Haluatko varmasti palauttaa laatumääritykset?", "ResetTitles": "Palauta nimet", @@ -560,7 +560,7 @@ "Tags": "Tunnisteet", "ToggleUnmonitoredToMonitored": "Ei valvota (aloita painamalla)", "TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).", - "UiSettingsLoadError": "Virhe ladattaessa käyttöliittymäasetuksia", + "UiSettingsLoadError": "Virhe ladattaesssa käyttöliittymän asetuksia", "UnableToUpdateSonarrDirectly": "{appName}ia ei voida päivittää suoraan,", "UnmonitoredOnly": "Vain valvomattomat", "UnmonitorDeletedEpisodes": "Lopeta poistettujen jaksojen valvonta", @@ -594,7 +594,7 @@ "ClickToChangeQuality": "Vaihda laatua klikkaamalla", "EpisodeDownloaded": "Jakso on ladattu", "InteractiveImportNoQuality": "Jokaisen valitun tiedoston laatu on määritettävä.", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä edellyttää vähintään NzbGet-versiota 16.0", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.", "NotificationStatusAllClientHealthCheckMessage": "Mikään ilmoituspavelu ei ole ongelmien vuoksi käytettävissä.", "DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent lataa metatietoja", @@ -656,7 +656,7 @@ "ImportFailed": "Tuonti epäonnistui: {sourceTitle}", "DeleteRootFolder": "Poista juurikansio", "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Useita tuontilistojen juurikansioita puuttuu: {rootFolderInfo}", - "ImportListExclusionsLoadError": "Virhe ladattaessa tuontilistapokkeuksia", + "ImportListExclusionsLoadError": "Tuontilistapoikkeusten lataus epäonnistui", "TablePageSizeMinimum": "Sivukohtaisen kohdemäärän on oltava vähintään {minimumValue}.", "YesCancel": "Kyllä, peru", "ImportListSearchForMissingEpisodesHelpText": "{appName} aloittaa automaattisesti puuttuvien jaksojen etsinnän kun sarja lisätään.", @@ -698,11 +698,11 @@ "DownloadIgnored": "Lataus ohitettiin", "EditSelectedIndexers": "Muokkaa valittuja sisältölähteitä", "ErrorRestoringBackup": "Virhe palautettaessa varmuuskopiota", - "ErrorLoadingItem": "Virhe ladattaessa tätä kohdetta", + "ErrorLoadingItem": "Virhe ladattaessa kohdetta", "FileBrowserPlaceholderText": "Kirjoita sijainti tai selaa se alta", "FeatureRequests": "Kehitysehdotukset", "IndexerPriority": "Tietolähteiden painotus", - "IndexerOptionsLoadError": "Virhe ladattaessa tietolähdeasetuksia", + "IndexerOptionsLoadError": "Tietolähdeasetusten lataus ei onnistu", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", "RemotePathMappings": "Etäsijaintien kohdistukset", @@ -728,7 +728,7 @@ "AuthenticationRequiredUsernameHelpTextWarning": "Syötä uusi käyttäjätunnus", "BlocklistLoadError": "Virhe ladattaessa estolistaa", "Database": "Tietokanta", - "LastWriteTime": "Viimeksi tallennettu", + "LastWriteTime": "Edellinen tallennus", "ChownGroupHelpTextWarning": "Toimii vain, jos {appName}in suorittava käyttäjä on tiedoston omistaja. On parempi varmistaa, että lataustyökalu käyttää samaa ryhmää kuin {appName}.", "IndexerSettingsSeasonPackSeedTimeHelpText": "Aika, joka tuotantokausipaketin sisältävää torrentia tulee jakaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi.", "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", @@ -736,7 +736,7 @@ "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe)", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakorajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakosuhderajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", "Yesterday": "Eilen", "SeriesMonitoring": "Sarjan valvonta", @@ -750,7 +750,7 @@ "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.", "DownloadClientOptionsLoadError": "Virhe ladattaessa lataustyökaluasetuksia", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", - "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirtämisen sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjesteästä riippuen).", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).", "IndexerValidationUnableToConnectHttpError": "Tietolähdettä ei tavoiteta. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", "BypassDelayIfHighestQualityHelpText": "Ohitusviive kun julkaisun laatu vastaa laatuprofiilin korkeinta käytössä olevaa laatua halutulla protokollalla.", "IndexerHDBitsSettingsCategoriesHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", @@ -784,8 +784,8 @@ "AddNewSeriesRootFolderHelpText": "\"{folder}\" -alikansio luodaan automaattisesti.", "AddNewSeriesSearchForMissingEpisodes": "Käynnistä puuttuvien jaksojen etsintä", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Käynnistä katkaisutasoa saavuttamattomien jaksojen etsintä", - "AddNewSeriesError": "Virhe ladattaessa hakutuloksia. Yritä uudelleen.", - "AnalyseVideoFilesHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvataa levyn ja/tai verkon kuormitusta tarkistusten aikana.", + "AddNewSeriesError": "Hakutulosten lataus epäonnistui. Yritä uudelleen.", + "AnalyseVideoFilesHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn ja/tai verkon kuormitusta tarkistusten aikana.", "ShowUnknownSeriesItemsHelpText": "Näytä jonossa kohteet, joiden sarja ei ole tiedossa. Tämä voi sisältää poistettuja sarjoja, elokuvia tai mitä tahansa muuta {appName}in kategoriasta.", "ShowSearchHelpText": "Näytä hakupainike osoitettaessa.", "Size": "Koko", @@ -869,14 +869,14 @@ "Donate": "Lahjoita", "DiskSpace": "Levytila", "DownloadClientDelugeTorrentStateError": "Deluge ilmoittaa virhettä", - "DownloadClientFreeboxApiError": "Freebox API palautti virheen: {errorDescription}", - "DownloadClientFreeboxAuthenticationError": "Freebox API -todennus epäonnistui. Syy: {errorDescription}", + "DownloadClientFreeboxApiError": "Freebox-rajapinta palautti virheen: {errorDescription}", + "DownloadClientFreeboxAuthenticationError": "Freebox API -todennus epäonnistui. Syy: {errorDescription}.", "Download": "Lataa", "DownloadClientQbittorrentSettingsUseSslHelpText": "Käytä suojattua yhteyttä. Katso qBittorentin asetusten \"Selainkäyttö\"-osion \"Käytä HTTPS:ää HTTP:n sijaan\" -asetus.", "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Aloita lataamalla ensimmäinen ja viimeinen osa (qBittorrent 4.1.0+).", "DownloadClientRTorrentSettingsAddStopped": "Lisää pysäytettynä", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" on määritetty poistamaan valmistuneet lataukset, jonka seuraksena ne saatetaan poistaa ennen kuin {appName} ehtii tuoda niitä.", - "DownloadClientQbittorrentTorrentStatePathError": "Tuonti ei onnistu. Tiedostosijainti vastaa lataustyökalun perussijaintia. Ehkä \"Säilytä ylätason kansio\" ei ole käytössä tälle torrentille tai \"Torrent Content Layout\" -asetuksena ei ole \"Alkuperäinen\" tai \"Luo alikansio\"?", + "DownloadClientQbittorrentTorrentStatePathError": "Tuonti ei onnistu. Tiedostosijainti vastaa lataustyökalun perussijaintia. Ehkä \"Säilytä ylätason kansio\" ei ole käytössä tälle torrentille tai \"Torrentin sisällön asettelu\" -asetuksena EI OLE \"Alkuperäinen\" tai \"Luo alikansio\"?", "DownloadClientSeriesTagHelpText": "Lataustyökalua käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", "DownloadClientValidationGroupMissing": "Ryhmää ei ole olemassa", "DownloadClients": "Lataustyökalut", @@ -903,7 +903,7 @@ "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnista mihin sarjalle ja jaksolle julkaisu kuuluu, eikä sen automaattinen tuonti onnistu. Haluatko kaapata julkaisun \"{0}\"?", "FailedToUpdateSettings": "Asetusten päivitys epäonnistui", "Forums": "Keskustelualue", - "ErrorLoadingPage": "Virhe ladattaessa tätä sivua", + "ErrorLoadingPage": "Virhe ladattaessa sivua", "FormatRuntimeHours": "{hours} t", "FormatRuntimeMinutes": "{minutes} m", "ICalLink": "iCal-linkki", @@ -931,7 +931,7 @@ "MustContainHelpText": "Julkaisun on sisällettävä ainakin yksi näistä termeistä (kirjainkoolla ei ole merkitystä).", "NoEpisodesFoundForSelectedSeason": "Valitulle tuotantokaudelle ei löytynyt jaksoja.", "MonitorFutureEpisodesDescription": "Valvo jaksoja, joita ei ole vielä esitetty.", - "MissingNoItems": "Ei puuttuvia kohteita", + "MissingNoItems": "Puuttuvia kohteita ei ole.", "Mode": "Tila", "NextExecution": "Seuraava suoritus", "PreviewRename": "Nimeämisen esikatselu", @@ -943,7 +943,7 @@ "PreferredSize": "Toivottu koko", "Progress": "Tilanne", "OrganizeLoadError": "Virhe ladattaessa esikatseluita", - "QualityCutoffNotMet": "Laadu katkaisutasoa ei ole saavutettu", + "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.", @@ -961,7 +961,7 @@ "Downloaded": "Ladattu", "Global": "Yleiset", "GeneralSettings": "Yleiset asetukset", - "ImportListsLoadError": "Virhe ladattaessa tuontilistoja", + "ImportListsLoadError": "Tuontilistojen lataus epäonnistui", "Importing": "Tuonti", "IndexerDownloadClientHealthCheckMessage": "Tietolähteet virheellisillä lataustyökaluilla: {indexerNames}.", "Indexer": "Tietolähde", @@ -993,7 +993,7 @@ "TablePageSizeHelpText": "Sivukohtainen kohdemäärä.", "TablePageSize": "Sivun kohdemäärä", "TablePageSizeMaximum": "Sivukohtainen kohdemäärä ei voi olla suurempi kuin {maximumValue}.", - "WhatsNew": "Mitä uutta?", + "WhatsNew": "Mikä on uutta?", "WeekColumnHeaderHelpText": "Näkyy jokaisen sarakkeen yläpuolella käytettäessä viikkonäkymää.", "MonitorAllSeasonsDescription": "Valvo kaikkia uusia kausia automaattisesti.", "MonitorLastSeason": "Viimeinen kausi", @@ -1004,13 +1004,13 @@ "MonitorNoEpisodes": "Ei mitään", "MonitorNoEpisodesDescription": "Mitään jaksoja ei valvota.", "RemotePathMappingWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", - "RemoveFailedDownloadsHelpText": "Poista epäonnistuneet lataukset lataustyökalun historiasta", + "RemoveFailedDownloadsHelpText": "Poista epäonnistuneet lataukset lataustyökalun historiasta.", "RemoveSelected": "Poista valitut", "RemoveSelectedBlocklistMessageText": "Haluatko varmasti poistaa valitut kohteet estolistalta?", "RemoveSelectedItemsQueueMessageText": "Haluatko varmasti poistaa jonosta {selectedCount} kohdetta?", "ReplaceIllegalCharactersHelpText": "Korvaa laittomat merkit vaihtoehtoisella merkinnällä. Jos ei valittu, ne poistetaan.", "ResetQualityDefinitions": "Palauta laatumääritykset", - "RestartRequiredToApplyChanges": "{appName} on käynnistettävä muutosten käyttöönottamiseksi uudelleen. Haluatko tehdä sen nyt?", + "RestartRequiredToApplyChanges": "{appName} on käynnistettävä uudelleen muutosten käyttöönottamiseksi. Haluatko tehdä sen nyt?", "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", "Seasons": "Kaudet", "SearchAll": "Etsi kaikkia", @@ -1043,7 +1043,7 @@ "CloneCondition": "Monista ehto", "DeleteCondition": "Poista ehto", "Delete": "Poista", - "ApiKey": "API-avain", + "ApiKey": "Rajapinnan avain", "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muta, jos et ymmärrä riskejä.", "Certification": "Varmennus", "ChangeFileDate": "Muuta tiedoston päiväys", @@ -1062,7 +1062,7 @@ "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Mukautetun muodon vähimmäispisteytys", "CertificateValidation": "Varmenteen vahvistus", "ChmodFolder": "chmod-kansio", - "ChangeFileDateHelpText": "Muuta tiedoston päiväystä tuonnin/kirjastotarkistusten yhteydessä.", + "ChangeFileDateHelpText": "Muuta tiedoston päiväys tuonnin/kirjaston uudelleentarkistuksen yhteydessä.", "ChownGroupHelpText": "Ryhmän nimi tai GID. Käytä GID:tä etätiedostojärjestelmille.", "ClientPriority": "Lataustyökalun painotus", "CloneProfile": "Monista profiili", @@ -1084,7 +1084,7 @@ "UtcAirDate": "UTC-esitysaika", "FileManagement": "Tiedostojen hallinta", "InteractiveImportNoEpisode": "Jokaiselle valitulle tiedostolle on valittava ainakin yksi jakso.", - "ApiKeyValidationHealthCheckMessage": "Muuta API-avaimesi 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", @@ -1120,7 +1120,7 @@ "Posters": "Julisteet", "RecyclingBin": "Roskakori", "RecyclingBinCleanup": "Roskakorin tyhjennys", - "RecyclingBinCleanupHelpText": "Arvo \"0\" (nolla) poistaa automaattityhjennyksen käytöstä.", + "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ä.", "CreateGroup": "Luo ryhmä", @@ -1155,7 +1155,7 @@ "Destination": "Kohde", "Directory": "Kansio", "PendingChangesDiscardChanges": "Hylkää muutokset ja poistu", - "RecyclingBinHelpText": "Poistetut tiedostot siirretään tänne pysyvän poiston sijaan.", + "RecyclingBinHelpText": "Pysyvän poiston sijaan tiedostot siirretään tähän kansioon.", "ReleaseSceneIndicatorAssumingTvdb": "Oletuksena TheTVDB-numerointi.", "ReleaseSceneIndicatorMappedNotRequested": "Valittu jakso ei sisältynyt tähän hakuun.", "ReplaceWithSpaceDash": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki\"", @@ -1182,7 +1182,7 @@ "InteractiveImportNoSeason": "Jokaiselle valitulle tiedostolle on määritettävä tuotantokausi.", "InteractiveSearchSeason": "Etsi kauden kaikkia jaksoja manuaalihaulla", "Space": "Välilyönti", - "DownloadPropersAndRepacksHelpTextCustomFormat": "Käytä \"Älä suosi\" -valintaa suosiaksesi mukautettujen muotojen pisteytystä Proper- ja Repack-merkintöjä enemmän.", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Käytä 'Älä suosi' -valintaa suosiaksesi mukautettujen muotojen pisteytystä Proper- ja Repack-merkintöjä enemmän.", "AuthenticationRequiredPasswordHelpTextWarning": "Syötä uusi salasana", "AuthenticationMethod": "Tunnistautumistapa", "AuthenticationMethodHelpTextWarning": "Valitse sopiva tunnistautumistapa", @@ -1209,7 +1209,7 @@ "Authentication": "Tunnistautuminen", "AutomaticAdd": "Automaattinen lisäys", "AutoAdd": "Automaattilisäys", - "AutoRedownloadFailedHelpText": "Hae ja pyri laaamaan eri julkaisu automaattisesti.", + "AutoRedownloadFailedHelpText": "Etsi ja pyri lataamaan eri julkaisu automaattisesti.", "Clone": "Monista", "CloneCustomFormat": "Monista mukautettu muoto", "ConnectSettings": "Kytkösasetukset", @@ -1234,7 +1234,7 @@ "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää etuliitteen Delugen JSON-URL-osoitteeseen (ks. {url}).", "DownloadClientDownloadStationValidationApiVersion": "Download Stationin API-versiota ei tueta. Sen tulee olla vähintään {requiredVersion} (versioita {minVersion}–{maxVersion} tuetaan).", "DownloadClientFloodSettingsRemovalInfo": "{appName} suorittaa torrenttien automaattisen poiston sen tietolähdeastuksissa määritettyjen jakoasetusten perusteella.", - "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää etuliitteeksi Flood API:n (esim. {url}).", + "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää etuliitteen Flood-rajapintaan (esim. {url}).", "DownloadClientRTorrentSettingsUrlPath": "URL-sijainti", "ExtraFileExtensionsHelpText": "Pilkuin eroteltu listaus tuotavista oheistiedostoista (.nfo-tiedostot tuodaan \".nfo-orig\"-nimellä).", "MultiEpisode": "Useita jaksoja", @@ -1242,7 +1242,7 @@ "MultiEpisodeInvalidFormat": "Useita jaksoja: virheellinen kaava", "AutoRedownloadFailedFromInteractiveSearch": "Uudelleenlataus manuaalihaun tuloksista epäonnistui", "Blocklist": "Estolista", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi automaattisesti ja pyri lataamaan eri julkaisu vaikka epäonnistunut julkaisu oli kaapattu manuaalihausta.", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi ja pyri lataamaan eri julkaisu automaattisesti vaikka epäonnistunut julkaisu oli kaapattu manuaalihaun tuloksista.", "StandardEpisodeFormat": "Tavallisten jaksojen kaava", "SceneNumberNotVerified": "Kohtausnumeroa ei ole vielä vahvistettu", "Scene": "Kohtaus", @@ -1271,6 +1271,7 @@ "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", + "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Download Stationin oletussijaintia jättämällä tyhjäksi.", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", @@ -1424,7 +1425,7 @@ "VideoCodec": "Videokoodekki", "MinimumAge": "Vähimmäisikä", "ReleaseGroups": "Julkaisuryhmät", - "DownloadClientQbittorrentTorrentStateStalled": "Lataus on jäätynyt yhteyksien puuttumksen vuoksi.", + "DownloadClientQbittorrentTorrentStateStalled": "Lataus on jäätynyt, koska yhdistettyjä lähteitä ei ole.", "EnableProfileHelpText": "Käytä julkaisuprofiilia merkitsemällä tämä.", "EnableRss": "Käytä RSS-syötettä", "EpisodeFileDeletedTooltip": "Jaksotiedosto poistettiin", @@ -1487,10 +1488,10 @@ "NotificationsKodiSettingsDisplayTimeHelpText": "Määrittää ilmoituksen näyttöajan sekunteina.", "NotificationsMailgunSettingsUseEuEndpointHelpText": "Käytä MailGunin EU-päätepistettä.", "NotificationsNtfySettingsClickUrl": "Painalluksen URL-osoite", - "NotificationsNotifiarrSettingsApiKeyHelpText": "Käyttäjätililtäsi löytyvä API-avain.", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Käyttäjätililtäsi löytyvä rajapinnan (API) avain.", "NotificationsNtfySettingsClickUrlHelpText": "Valinnainen URL-osoite, joka ilmoitusta painettaessa avataan.", "NotificationsNtfySettingsTopics": "Topikit", - "NotificationsPushcutSettingsApiKeyHelpText": "API-avaimia voidaan hallita Puscut-sovelluksen tiliosiossa.", + "NotificationsPushcutSettingsApiKeyHelpText": "Rajapinnan (API) avaimia voidaan hallita Puscut-sovelluksen tiliosiossa.", "NotificationsPushoverSettingsSoundHelpText": "Ilmoituksen ääni. Käytä oletusta jättämällä tyhjäksi.", "NotificationsSettingsWebhookMethodHelpText": "Lähetyksessä käytettävä HTTP-menetelmä.", "NotificationsSimplepushSettingsEvent": "Tapahtuma", @@ -1521,10 +1522,10 @@ "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Sisällytä sarjan juliste ilmoitukseen.", "NotificationsGotifySettingsPriorityHelpText": "Ilmoituksen painotus.", "NotificationsJoinSettingsDeviceIds": "Laite-ID:t", - "NotificationsJoinSettingsApiKeyHelpText": "Join-tilisi asetuksista löytyvä API-avain (paina Join API -painiketta).", + "NotificationsJoinSettingsApiKeyHelpText": "Join-tilisi asetuksista löytyvä rajapinnan (API) avain (paina Join API -painiketta).", "NotificationsJoinSettingsDeviceNames": "Laitenimet", "NotificationsKodiSettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö Kodin kirjasto tuonnin tai uudelleennimeämisen yhteydessä.", - "NotificationsMailgunSettingsApiKeyHelpText": "MailGunissa luotu API-avain.", + "NotificationsMailgunSettingsApiKeyHelpText": "MailGunissa luotu rajapinnan (API) avain.", "NotificationsMailgunSettingsUseEuEndpoint": "Käytä EU-päätepistettä", "NotificationsNtfySettingsAccessToken": "Käyttötunniste", "NotificationsNtfySettingsServerUrl": "Palvelimen URL-osoite", @@ -1545,15 +1546,15 @@ "NotificationsTwitterSettingsConsumerSecretHelpText": "Kuluttajan salaisuus (consumer secret) X (Twitter) -sovelluksesta.", "NotificationsTwitterSettingsDirectMessageHelpText": "Lähetä julkisen viestin sijaan suora viesti.", "NotificationsTwitterSettingsMention": "Maininta", - "NotificationsValidationInvalidApiKeyExceptionMessage": "API-avain on virheellinen: {exceptionMessage}", - "NotificationsValidationInvalidApiKey": "API-avain on virheellinen", + "NotificationsValidationInvalidApiKeyExceptionMessage": "Rajapinnan avain ei kelpaa: {exceptionMessage}", + "NotificationsValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", "NotificationsValidationUnableToConnectToService": "Palvelua {serviceName} ei tavoiteta.", "NotificationsValidationUnableToConnectToApi": "Palvelun {service} rajapintaa ei tavoiteta. Palvelinyhteys epäonnistui: ({responseCode}) {exceptionMessage}.", "ReleaseHash": "Julkaisun hajatusarvo", "False": "Epätosi", "CustomFormatsSpecificationRegularExpressionHelpText": "Mukautetun muodon säännöllisen lausekkeen kirjainkokoa ei huomioida.", "CustomFormatsSpecificationRegularExpression": "Säännöllinen lauseke", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API -määritystä tehtäessä saatu sovellustunniste (i.e. \"app_token\").", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä saatu app_token-tietue.", "ImportListsPlexSettingsAuthenticateWithPlex": "Plex.tv-tunnistautuminen", "ImportListsSettingsAccessToken": "Käyttötunniste", "ManageClients": "Hallitse työkaluja", @@ -1574,7 +1575,7 @@ "NotificationsEmailSettingsBccAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään piilokopioina.", "NotificationsGotifySettingsServerHelpText": "Gotify-palvelimen URL-osoite. Sisällytä http(s):// ja portti (tarvittaessa).", "NotificationsGotifySettingsServer": "Gotify-palvelin", - "NotificationsGotifySettingsAppToken": "Sovellustunniste", + "NotificationsGotifySettingsAppToken": "Sovellustietue", "NotificationsEmailSettingsRecipientAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään.", "NotificationsJoinSettingsNotificationPriority": "Ilmoituksen painotus", "NotificationsJoinValidationInvalidDeviceId": "Laite-ID:issä näyttäisi olevan virheitä.", @@ -1592,7 +1593,7 @@ "NotificationsPushoverSettingsSound": "Ääni", "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", "NotificationsPushoverSettingsExpire": "Erääntyminen", - "NotificationsSendGridSettingsApiKeyHelpText": "SendGridin luoma API-avain.", + "NotificationsSendGridSettingsApiKeyHelpText": "SendGridin luoma rajapinnan (API) avain.", "NotificationsSimplepushSettingsEventHelpText": "Mukauta push-ilmoitusten toimintaa.", "NotificationsSignalSettingsSenderNumber": "Lähettäjän numero", "NotificationsSettingsWebhookMethod": "HTTP-menetelmä", @@ -1629,7 +1630,7 @@ "NotificationsValidationUnableToSendTestMessageApiResponse": "Testiviestin lähetys ei onnistu. API vastasi: {error}", "NotificationsEmailSettingsUseEncryption": "Käytä salausta", "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimikkeen ja näyttämään sen tiedot.", - "ImportScriptPathHelpText": "Tuonnissa suoirtettavan komentosarjan sijainti.", + "ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.", "NotificationsTwitterSettingsConsumerKeyHelpText": "Kuluttajan avain (consumer key) X (Twitter) -sovelluksesta.", "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", "RemoveMultipleFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", @@ -1687,7 +1688,7 @@ "ImportListsImdbSettingsListId": "Listan ID", "ImportListsImdbSettingsListIdHelpText": "IMDb-listan ID (esim. \"ls12345678\").", "ImportListsSonarrSettingsRootFoldersHelpText": "Lähdeinstanssin juurikansiot, joista tuodaan.", - "ImportListsTraktSettingsRatingHelpText": "Suodata sarjat arviovälin perusteella (0–100)", + "ImportListsTraktSettingsRatingHelpText": "Suodata sarjoja arvioden perusteella (alue 0–100).", "ImportListsTraktSettingsWatchedListFilter": "Katselulistan suodatin", "ImportListsTraktSettingsYearsHelpText": "Suodata sarjat vuoden tai vuosivälin perusteella", "MetadataSettingsEpisodeImages": "Jaksojen kuvat", @@ -1725,7 +1726,7 @@ "DownloadClientSettingsDestinationHelpText": "Määrittää manuaalisen tallennuskohteen. Käytä oletusta jättämällä tyhjäksi.", "DownloadClientSettingsOlderPriority": "Vanhojen painotus", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Yli 14 päivää sitten julkaistujen jaksojen kaappauksille käytettävä painotus.", - "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Traktin lajityyppien Slug-määrityksen perusteella (pilkuin eroteltuna) vain suosituille listoille.", + "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Trakt-lajityyppien slug-arvoilla (pilkuin eroteltuna). Koskee vain suosituimpia listoja.", "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse sarjatyyppi, jonka haluat tuoda.", "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja [UKK:sta](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server).", "MetadataSettingsSeriesMetadata": "Sarjojen metatiedot", @@ -1736,5 +1737,73 @@ "ChangeCategoryMultipleHint": "Vaihtaa latausten kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", "DownloadClientAriaSettingsDirectoryHelpText": "Valinnainen latuasten tallennussijainti. Käytä Aria2-oletusta jättämällä tyhjäksi.", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrentien jonotus ei ole käytössä qBittorent-asetuksissasi. Ota se käyttöön tai valitse painotukseksi \"Viimeiseksi\".", - "DownloadClientSettingsCategorySubFolderHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa. Tämä luo latauskansioon [kategoria]-alikansion." + "DownloadClientSettingsCategorySubFolderHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa. Tämä luo latauskansioon [kategoria]-alikansion.", + "ListQualityProfileHelpText": "Laatuprofiili, joka listalta lisätyille kohteille asetetaan.", + "RemovingTag": "Tunniste poistetaan", + "Ui": "Käyttöliittymä", + "Tba": "Selviää myöhemmin", + "UpgradesAllowedHelpText": "Jos käytöstä poistettuja laatuja ei päivitetä.", + "Repack": "Uudelleenpaketoitu", + "SupportedAutoTaggingProperties": "{appName} tukee automaattimerkinnän säännöissä seuraavia arvoja", + "RegularExpressionsCanBeTested": "Säännöllisiä lausekkeita voidaan testata [täällä](http://regexstorm.net/tester).", + "RssSyncIntervalHelpTextWarning": "Tämä koskee kaikkia tietolähteitä. Noudata niiden asettamia sääntöjä.", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla. Esimerkiksi \"{url}\". Oletus on \"{defaultApiUrl}\".", + "DownloadClientFreeboxSettingsHostHelpText": "Freeboxin isäntänimi tai IP-osoite. Oletus on \"{url}\" (toimii vain samassa verkossa).", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on \"{port}\".", + "DownloadClientNzbVortexMultipleFilesMessage": "Lataus sisältää useita tiedostoja, eikä se ole työkansiossa: {outputPath}.", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Freebox-rajapintaa ei tavoiteta. Tarkista \"Rajapinnan URL-osoite\" -asetuksen perus-URL ja versio.", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGetin \"KeepHistory\"-asetus on 0 ja tämä estää {appName}ia näkemästä valmistuneita latauksia.", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGetin \"KeepHistory\"-asetus on liian korkea.", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGetin \"KeepHistory\"-asetuksen tulee olla pienempi kuin 25000.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Älä järjestele elokuvia", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Tämän kansion on oltava tavoitettavissa XBMC:stä.", + "DownloadClientValidationTestTorrents": "Torrent-listausten nouto epäonnistui: {exceptionMessage}.", + "ExportCustomFormat": "Vie mukautettu muoto", + "ImportListsValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Tila, jossa torrentit lisätään qBittorrentiin. Huomioi, että pakotetut torrentit eivät noudata nopeusrajoituksia.", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent ei voi selvittää magnet-linkkejä, jos DHT ei ole käytössä.", + "MetadataPlexSettingsSeriesPlexMatchFile": "Luo Plex Match -tiedostot", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Luo sarjojen kansioihin .plexmatch-tiedostot.", + "NoResultsFound": "Tuloksia ei löytynyt.", + "RegularExpressionsTutorialLink": "Lisätietoja säännöllisistä lausekkeista löytyy [täältä](https://www.regular-expressions.info/tutorial.html).", + "ResetAPIKey": "Korvaa rajapinnan avain", + "Reset": "Uudista", + "ResetDefinitions": "Palauta määritykset", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent on määritetty poistamaan torrentit niiden saavuttaessa niitä koskevan jakosuhderajoituksen.", + "SecretToken": "Salainen tunniste", + "ShownClickToHide": "Näkyvissä, piilota painamalla", + "DownloadClientValidationAuthenticationFailure": "Tunnistautuminen epäonnistui", + "DownloadClientValidationApiKeyRequired": "Rajapinnan avain on pakollinen", + "DownloadClientValidationVerifySsl": "Vahvista SSL-asetukset", + "ImportListRootFolderMissingRootHealthCheckMessage": "Tuontilistalta tai -listoilta puuttuu juurikansio: {rootFolderInfo}.", + "Release": "Julkaisu", + "OrganizeRelativePaths": "Kaikki tiedostosijainnit on suhtetuttu sijaintiin: \"{path}\".", + "TorrentDelayTime": "Torrent-viive: {torrentDelay}", + "TorrentBlackholeTorrentFolder": "Torrent-kansio", + "UsenetBlackholeNzbFolder": "NZB-kansio", + "UsenetBlackhole": "Usenet Blackhole", + "XmlRpcPath": "XML RPC -sijainti", + "FailedToLoadSonarr": "{appName}in lataus epäonnistui", + "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä käytettävä App ID -sovellustunniste.", + "ListRootFolderHelpText": "Juurikansio, johon listan kohteet lisätään.", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Älä järjestele sarjoja", + "EnableAutomaticAddSeriesHelpText": "Lisää tämän listan sarjat {appName}iin kun synkronointi suoritetaan manuaalisesti käyttöliittymästä tai {appName}in toimesta.", + "DownloadClientValidationTestNzbs": "NZB-listausten nouto epäonnistui: {exceptionMessage}.", + "DownloadClientValidationUnableToConnect": "Lataustyökalua {clientName} ei tavoitettu", + "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latauskansio. Käytä Transmissionin oletusta jättämällä tyhjäksi.", + "AddDelayProfileError": "Virhe lisättäessä viiveporofiilia. Yritä uudelleen.", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää etuliite lataustyökalun {clientName} RPC-URL-osoitteeseen. Esimerkiksi {url}. Oletus on \"{defaultUrl}\".", + "DownloadClientValidationApiKeyIncorrect": "Rajapinnan avain ei kelpaa", + "HiddenClickToShow": "Piilotettu, näytä painalla", + "ImportListsCustomListValidationAuthenticationFailure": "Tunnistautuminen epäonnistui", + "ImportListsSonarrSettingsApiKeyHelpText": "{appName}-instanssin, josta tuodaan, rajapinan (API) avain.", + "IndexerSettingsApiUrlHelpText": "Älä muuta tätä, jos et tiedä mitä teet, koska rajapinta-avaimesi lähetetään kyseiselle palvelimelle.", + "ImportListsTraktSettingsRating": "Arvio", + "Required": "Pakollinen", + "TaskUserAgentTooltip": "User-Agent-tiedon ilmoitti rajapinnan kanssa viestinyt sovellus.", + "TorrentBlackhole": "Torrent Blackhole", + "WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats)." } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 7903fb8ac..10c0881c0 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -233,7 +233,7 @@ "Backup": "Biztonsági mentés", "AddNew": "Új hozzáadása", "AddANewPath": "Új útvonal hozzáadása", - "AddConditionImplementation": "Feltétel hozzáadása –{megvalósítás név}", + "AddConditionImplementation": "Feltétel hozzáadása –{implementationName}", "AddConnectionImplementation": "Csatlakozás hozzáadása - {implementationName}", "AddCustomFilter": "Egyéni szűrő hozzáadása", "AddDownloadClientImplementation": "Letöltési kliens hozzáadása – {implementationName}", @@ -258,8 +258,8 @@ "AddNewSeriesSearchForCutoffUnmetEpisodes": "Indítsa el a levágott, még nem talált epizódok keresését", "AddListExclusion": "Listakizárás hozzáadása", "AddNewSeriesSearchForMissingEpisodes": "Kezdje el a hiányzó epizódok keresését", - "AddSeriesWithTitle": "Cím {hozzáadása}", - "AddedDate": "Hozzáadva: {dátum}", + "AddSeriesWithTitle": "Cím {title}", + "AddedDate": "Hozzáadva: {date}", "AllFiles": "Minden fájl", "AllSeriesAreHiddenByTheAppliedFilter": "Az összes eredményt elrejti az alkalmazott szűrő", "AlternateTitles": "Alternatív címek", @@ -285,7 +285,7 @@ "AddNewSeries": "Új sorozat hozzáadása", "AddNewSeriesError": "Nem sikerült betölteni a keresési eredményeket, próbálkozzon újra.", "AddNewSeriesHelpText": "Könnyen hozzáadhat új sorozatot, csak kezdje el beírni a hozzáadni kívánt sorozat nevét.", - "AddNewSeriesRootFolderHelpText": "'{Mappa}' almappa automatikusan létrejön", + "AddNewSeriesRootFolderHelpText": "'{folder}' almappa automatikusan létrejön", "AuthenticationMethod": "Hitelesítési Módszer", "BlackholeWatchFolder": "Mappa figyelése", "CalendarFeed": "{appName} Naptár Feed", @@ -304,7 +304,7 @@ "Fixed": "Rögzített", "Events": "Események", "Exception": "Kivétel", - "AddListExclusionSeriesHelpText": "Akadályozza meg, hogy listák alapján sorozatokat adjanak hozzá a(z) {App név} alkalmazáshoz", + "AddListExclusionSeriesHelpText": "Akadályozza meg, hogy listák alapján sorozatokat adjanak hozzá a(z) {appName} alkalmazáshoz", "Languages": "Nyelvek", "Logs": "Naplók", "AnimeEpisodeTypeDescription": "Az abszolút epizódszámmal kiadott epizódok", @@ -875,7 +875,7 @@ "ChmodFolderHelpTextWarning": "Ez csak akkor működik, ha a(z) {appName} alkalmazást futtató felhasználó a fájl tulajdonosa. Jobb, ha megbizonyosodik arról, hogy a letöltési kliens megfelelően állítja be az engedélyeket.", "ChownGroupHelpTextWarning": "Ez csak akkor működik, ha a(z) {appName} alkalmazást futtató felhasználó a fájl tulajdonosa. Jobb, ha a letöltési kliens ugyanazt a csoportot használja, mint a {appName}.", "CollectionsLoadError": "Nem sikerült betölteni a gyűjteményeket", - "Conditions": "Körülmények", + "Conditions": "Állapot", "ContinuingSeriesDescription": "További epizódok/újabb évad várható", "ConnectionLostToBackend": "A(z) {appName} megszakadt a kapcsolat a háttérrendszerrel, ezért újra kell tölteni a működés visszaállításához.", "CountIndexersSelected": "{count} indexelő(k) kiválasztva", @@ -958,7 +958,7 @@ "SelectSeries": "Sorozat kiválasztása", "BlocklistAndSearchMultipleHint": "Indítsa el a helyettesítők keresését a tiltólistázás után", "BlocklistMultipleOnlyHint": "Blokklista helyettesítők keresése nélkül", - "BlocklistOnly": "Csak blokkolólista", + "BlocklistOnly": "Csak blokklistára", "BlocklistOnlyHint": "Blokklista csere keresése nélkül", "ChangeCategory": "Kategória módosítása", "ChangeCategoryMultipleHint": "Módosítja a letöltéseket az „Importálás utáni kategóriára” a Download Clientből", @@ -1028,5 +1028,209 @@ "StandardEpisodeFormat": "Alapértelmezett epizód formátum", "StartProcessing": "Indítsa el a feldolgozást", "SelectFolder": "Mappa kiválasztása", - "ShowUnknownSeriesItemsHelpText": "Sorozat nélküli elemek megjelenítése a sorban, idetartozhatnak az eltávolított sorozatok, filmek vagy bármi más a(z) {appName} kategóriájában" + "ShowUnknownSeriesItemsHelpText": "Sorozat nélküli elemek megjelenítése a sorban, idetartozhatnak az eltávolított sorozatok, filmek vagy bármi más a(z) {appName} kategóriájában", + "DestinationRelativePath": "A cél relatív útvonala", + "ImportFailed": "Sikertelen importálás: {sourceTitle}", + "ImportList": "Importálási lista", + "DeleteImportListExclusion": "Importálási lista kizárásának törlése", + "DeleteIndexer": "Indexelő törlése", + "DeleteNotification": "Értesítés törlése", + "DockerUpdater": "Frissítse a docker-tárolót a frissítés fogadásához", + "DeleteTag": "Címke törlése", + "CustomFormatScore": "Egyéni formátum pontszáma", + "ImportLists": "Listák importálása", + "CountSelectedFiles": "{selectedCount} kiválasztott fájl", + "AddDelayProfileError": "Nem sikerült új késleltetési profilt hozzáadni, próbálkozzon újra.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "A kategóriák használatához engedélyeznie kell a Label beépülő modult a {clientName} szolgáltatásban.", + "BindAddressHelpText": "Érvényes IP-cím, localhost vagy '*' minden interfészhez", + "CustomFormatUnknownConditionOption": "Ismeretlen „{key}” opció a „{implementation}” feltételhez", + "ImportListSearchForMissingEpisodes": "Keresse meg a hiányzó epizódokat", + "ImportListSearchForMissingEpisodesHelpText": "Miután a sorozatot hozzáadta a {appName} alkalmazáshoz, automatikusan megkeresi a hiányzó epizódokat", + "AutoTaggingRequiredHelpText": "Ennek az {implementationName} feltételnek meg kell egyeznie az automatikus címkézési szabály érvényesítéséhez. Ellenkező esetben egyetlen {implementationName} egyezés elegendő.", + "DeleteReleaseProfile": "Release profil törlése", + "DetailedProgressBarHelpText": "Szöveg megjelenítése a folyamatjelző sávon", + "BranchUpdate": "A(z) {appName} frissítéséhez használt fiók", + "BackupFolderHelpText": "A relatív elérési utak a(z) {appName} AppData könyvtárában találhatók", + "CustomFormatsSpecificationRegularExpressionHelpText": "Az egyéni formátumú reguláris kifejezés nem különbözteti meg a kis- és nagybetűket", + "Destination": "Rendeltetési hely", + "DestinationPath": "Cél útvonal", + "DetailedProgressBar": "Részletes folyamatjelző sáv", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Megosztott mappa nem létezik", + "EditListExclusion": "Listakizárás szerkesztése", + "ImportExistingSeries": "Meglévő sorozat importálása", + "ImportListSettings": "Listabeállítások importálása", + "ImportListsAniListSettingsAuthenticateWithAniList": "Hitelesítés az AniList segítségével", + "ImportListsAniListSettingsImportCancelled": "Importálás megszakítva", + "ImportListsAniListSettingsImportFinished": "Az importálás befejeződött", + "ImportListsAniListSettingsImportFinishedHelpText": "Média: Minden epizódot leadtak", + "ImportListsAniListSettingsImportHiatus": "Importálás szünet", + "ImportListsAniListSettingsImportNotYetReleased": "Az importálás még nem jelent meg", + "CleanLibraryLevel": "Tiszta könyvtári szint", + "CustomFormats": "Egyéni formátumok", + "DeleteEpisodeFromDisk": "Epizód törlése a lemezről", + "DeleteEpisodeFileMessage": "Biztosan törli a következőt: „{path}”?", + "DoNotPrefer": "Ne preferáld", + "DeleteSpecificationHelpText": "Biztosan törli a(z) „{name}” specifikációt?", + "DownloadClientDelugeTorrentStateError": "A Deluge hibát jelez", + "DownloadClientDownloadStationValidationFolderMissing": "A mappa nem létezik", + "DownloadClientDownloadStationValidationNoDefaultDestination": "A mappa nem létezik", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Hozzáadja a média tulajdonságait címkékként. A tippek példák.", + "DownloadClientFloodSettingsPostImportTags": "Importálás utáni címkék", + "DownloadClientFloodSettingsPostImportTagsHelpText": "A letöltés importálása után címkéket fűz hozzá.", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: Megtekintést tervez", + "BlackholeFolderHelpText": "Mappa, amelyben az {appName} tárolja az {extension} fájlt", + "BlackholeWatchFolderHelpText": "Mappa, amelyből a(z) {appName} a befejezett letöltéseket importálja", + "DeleteSeriesFoldersHelpText": "Törölje a sorozat mappáit és azok teljes tartalmát", + "DeleteSeriesModalHeader": "Törlés – {title}", + "DeleteSpecification": "Specifikáció törlése", + "DeleteSeriesFolders": "Sorozat mappák törlése", + "DownloadClientDelugeValidationLabelPluginFailure": "A címke konfigurálása nem sikerült", + "DownloadClientDelugeValidationLabelPluginInactive": "A címke beépülő modul nincs aktiválva", + "DeleteEpisodeFile": "Epizódfájl törlése", + "DeleteQualityProfile": "Törölje a minőségi profilt", + "DeleteReleaseProfileMessageText": "Biztosan törli ezt a kiadási profilt: „{name}”?", + "DeleteEpisodesFilesHelpText": "Törölje az epizódfájlokat és a sorozat mappáját", + "DeleteSelectedEpisodeFiles": "Törölje a kiválasztott epizódfájlokat", + "DeleteSelectedEpisodeFilesHelpText": "Biztosan törli a kiválasztott epizódfájlokat?", + "DeleteSeriesFolderHelpText": "Törölje a sorozat mappát és annak tartalmát", + "DailyEpisodeTypeDescription": "Naponta vagy ritkábban megjelenő, év-hónap-napot használó epizódok (2023-08-04)", + "CustomFilters": "Egyedi Szűrők", + "CurrentlyInstalled": "Jelenleg telepítve", + "DeleteEmptySeriesFoldersHelpText": "Törölje az üres sorozat- és évadmappákat a lemezellenőrzés és az epizódfájlok törlésekor", + "DeleteEpisodesFiles": "Törölje a(z) {episodeFileCount} epizódfájlt", + "DeleteSeriesFolderCountWithFilesConfirmation": "Biztos benne, hogy törölni szeretne {count} kiválasztott sorozatot és az összes tartalmat?", + "CountSelectedFile": "{selectedCount} kiválasztott fájl", + "CutoffUnmetLoadError": "Hiba a nem teljesített elemek betöltésekor", + "CutoffUnmetNoItems": "Nincsenek teljesítetlen elemek levágása", + "DownloadClientDownloadStationSettingsDirectory": "Opcionális megosztott mappa a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Download Station hely használatához", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Előtagot ad a deluge json URL-hez, lásd: {url}", + "DownloadClientFloodSettingsAdditionalTags": "További címkék", + "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", + "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", + "DatabaseMigration": "Adatbázis-migráció", + "DeleteSelectedSeries": "A kiválasztott sorozat törlése", + "DeleteSeriesFolder": "Sorozatmappa törlése", + "DeleteSeriesFolderCountConfirmation": "Biztosan törölni szeretne {count} kiválasztott sorozatot?", + "DeletedSeriesDescription": "A sorozatot törölték a TheTVDB-ből", + "DoNotBlocklist": "Ne tiltsa le", + "DoNotBlocklistHint": "Eltávolítás tiltólista nélkül", + "ImportListExclusionsLoadError": "Nem sikerült betölteni az importálási lista kizárásait", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Minden lista kézi beavatkozást igényel az esetleges részleges lehívások miatt", + "ImportListsAniListSettingsImportCancelledHelpText": "Média: A sorozatot törölték", + "ImportListsAniListSettingsImportCompleted": "Az importálás befejeződött", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: Befejezett megtekintés", + "ImportExtraFilesEpisodeHelpText": "Importálja a megfelelő extra fájlokat (feliratok, nfo stb.) az epizódfájl importálása után", + "ImportListsAniListSettingsImportDropped": "Az importálás megszakadt", + "ImportListsAniListSettingsImportPaused": "Az importálás szünetel", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: Várakozás", + "ErrorLoadingItem": "Hiba történt az elem betöltésekor", + "ErrorLoadingContents": "Hiba a tartalom betöltésekor", + "ErrorRestoringBackup": "Hiba a biztonsági mentés visszaállításakor", + "ICalLink": "iCal Link(ek)", + "RemoveQueueItemConfirmation": "Biztosan eltávolítja a következőt a sorból: \"{sourceTitle}\"?", + "EnableColorImpairedMode": "Engedélyezze a színtévesztő módot", + "EnableCompletedDownloadHandlingHelpText": "A befejezett letöltések automatikus importálása a letöltési kliensből", + "EpisodeNumbers": "Epizódszám(ok)", + "EpisodeProgress": "Az epizód előrehaladása", + "EpisodeNaming": "Epizód elnevezése", + "EpisodeSearchResultsLoadError": "Nem sikerült betölteni az epizódkeresés eredményeit. Próbáld újra később", + "EnableAutomaticAdd": "Automatikus hozzáadás engedélyezése", + "EnableMetadataHelpText": "Metaadatfájl létrehozásának engedélyezése ehhez a metaadattípushoz", + "EnableProfileHelpText": "Jelölje be a kiadási profil engedélyezéséhez", + "EpisodeTitleRequired": "Az epizód címe kötelező", + "HistoryLoadError": "Nem sikerült betölteni az előzményeket", + "RemoveQueueItem": "Eltávolítás – {sourceTitle}", + "ResetQualityDefinitionsMessageText": "Biztosan vissza szeretné állítani a minőségi definíciókat?", + "DownloadClientSettingsInitialState": "Kezdeti állapot", + "DownloadClientSettingsOlderPriority": "Régebbi prioritás", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Elsőbbség az elmúlt 14 napban sugárzott epizódok rögzítésekor", + "DownloadClientValidationSslConnectFailure": "Nem lehet SSL-n keresztül csatlakozni", + "DownloadClientValidationUnableToConnect": "Nem sikerült csatlakozni a következőhöz: {clientName}", + "EpisodeFileDeletedTooltip": "Az epizódfájl törölve", + "EpisodesLoadError": "Nem sikerült betölteni az epizódokat", + "ErrorLoadingContent": "Hiba történt a tartalom betöltésekor", + "GeneralSettingsSummary": "Port, SSL, felhasználónév/jelszó, proxy, elemzések és frissítések", + "HideEpisodes": "Epizódok elrejtése", + "ICalSeasonPremieresOnlyHelpText": "Egy évadban csak az első epizód lesz a hírfolyamban", + "HistorySeason": "Az idei szezon előzményeinek megtekintése", + "ICalTagsSeriesHelpText": "A hírcsatorna csak legalább egy egyező címkével rendelkező sorozatokat tartalmaz", + "IconForFinales": "Ikon a fináléhoz", + "DownloadClientUTorrentTorrentStateError": "Az uTorrent hibát jelez", + "General": "Általános", + "GeneralSettings": "Általános Beállítások", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Elsőbbség a 14 nappal ezelőtt sugárzott epizódok megragadásánál", + "DownloadClientSettingsRecentPriority": "Legutóbbi prioritás", + "DownloadClientSettingsUseSslHelpText": "Biztonságos kapcsolat használata, amikor a(z) {clientName} szolgáltatással csatlakozik", + "DownloadClientValidationUnableToConnectDetail": "Kérjük, ellenőrizze a gazdagépnevet és a portot.", + "EnableRss": "RSS Aktiválás", + "EditSelectedSeries": "Kiválasztott sorozat szerkesztése", + "EnableInteractiveSearchHelpTextWarning": "Ez az indexelő nem támogatja a keresést", + "EpisodeHistoryLoadError": "Nem sikerült betölteni az epizódelőzményeket", + "EpisodeImported": "Epizód importálva", + "EpisodeInfo": "Epizód infó", + "EpisodeIsNotMonitored": "Az epizódot nem figyelik", + "EpisodeMissingFromDisk": "Az epizód hiányzik a lemezről", + "EpisodeMissingAbsoluteNumber": "Az epizódnak nincs abszolút epizódszáma", + "EpisodeTitle": "Epizód címe", + "FinaleTooltip": "Sorozat vagy évadzáró", + "EpisodeFilesLoadError": "Nem sikerült betölteni az epizódfájlokat", + "EpisodeHasNotAired": "Az epizód nem került adásba", + "DownloadClientSettingsInitialStateHelpText": "A torrentek kezdeti állapota hozzáadva a következőhöz {clientName}", + "EnableHelpText": "Metaadatfájl létrehozásának engedélyezése ehhez a metaadattípushoz", + "EpisodeFileRenamedTooltip": "Az epizódfájl átnevezve", + "EpisodeGrabbedTooltip": "Az epizódot letöltötte a(z) {indexer} és elküldte a(z) {downloadClient} számára", + "EpisodeImportedTooltip": "Az epizód letöltése sikeresen megtörtént, és a letöltés kliensből lett letöltve", + "EpisodeIsDownloading": "Az epizód letöltése folyamatban van", + "FilterNotInNext": "nem a következőben", + "FilterSeriesPlaceholder": "Sorozat Szűrő", + "FirstDayOfWeek": "A hét első napja", + "FreeSpace": "Szabad hely", + "Global": "Globális", + "HomePage": "Kezdőlap", + "ICalShowAsAllDayEventsHelpText": "Az események egész napos eseményekként jelennek meg a naptárban", + "IconForFinalesHelpText": "A sorozat/évadzáró ikon megjelenítése a rendelkezésre álló epizódinformációk alapján", + "WouldYouLikeToRestoreBackup": "Szeretné visszaállítani a(z) „{name}” biztonsági másolatot?", + "EnableAutomaticSearchHelpTextWarning": "Ha interaktív keresést használ, akkor lesz használva", + "EnableInteractiveSearchHelpText": "Interaktív keresés esetén lesz használatos", + "EventType": "Esemény típus", + "FilterDoesNotStartWith": "nem azzal kezdődik", + "FullColorEvents": "Színes események", + "FullSeason": "Teljes évad", + "Genres": "Műfajok", + "GeneralSettingsLoadError": "Nem sikerült betölteni az Általános beállításokat", + "HasMissingSeason": "Hiányzik az évad", + "IgnoreDownload": "Letöltés figyelmen kívül hagyása", + "IgnoreDownloadHint": "Leállítja a(z) {appName} alkalmazásnak a letöltés további feldolgozását", + "Forecast": "Előrejelzés", + "DownloadClientValidationApiKeyRequired": "API-kulcs szükséges", + "DownloadClientValidationAuthenticationFailure": "Hitelesítési hiba", + "Grab": "Megfog", + "DownloadClientValidationGroupMissing": "Csoport nem létezik", + "DownloadClientValidationTestTorrents": "Nem sikerült lekérni a torrentek listáját: {exceptionMessage}", + "ICalFeed": "iCal-Feed", + "ICalIncludeUnmonitoredEpisodesHelpText": "Vegyen fel nem figyelt epizódokat az iCal-Feed-be", + "EnableSslHelpText": "Az érvénybe léptetéshez rendszergazdaként való újraindítás szükséges", + "EndedSeriesDescription": "Az érvénybe léptetéshez rendszergazdaként való újraindítás szükséges", + "HealthMessagesInfoBox": "Az állapotfelmérés okáról további információkat találhat, ha a sor végén található wikilinkre (könyv ikonra) kattint, vagy megnézi [logs] ({link}). Ha nehézségei vannak ezen üzenetek értelmezése során, forduljon ügyfélszolgálatunkhoz az alábbi linkeken.", + "EnableSsl": "SSL engedélyezése", + "RemoveQueueItemRemovalMethod": "Eltávolítási módszer", + "RemoveQueueItemRemovalMethodHelpTextWarning": "Az „Eltávolítás a letöltési kliensből” eltávolítja a letöltést és a fájl(oka)t a letöltési kliensből.", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "Az „Eltávolítás a letöltési kliensből” eltávolítja a letöltéseket és a fájlokat a letöltési kliensből.", + "DownloadClientValidationApiKeyIncorrect": "Az API kulcs helytelen", + "DownloadClientValidationCategoryMissing": "A kategória nem létezik", + "DownloadClientValidationCategoryMissingDetail": "A megadott kategória nem létezik a következőben: {clientName}. Először hozza létre a {clientName} szolgáltatásban.", + "DownloadClientValidationVerifySsl": "Ellenőrizze az SSL-beállításokat", + "DownloadClientVuzeValidationErrorVersion": "A protokoll verziója nem támogatott, használja a Vuze 5.0.0.0 vagy újabb verzióját a Vuze Web Remote bővítménnyel.", + "EditSeriesModalHeader": "Szerkesztés – {title}", + "IgnoreDownloads": "Letöltések figyelmen kívül hagyása", + "IgnoreDownloadsHint": "Leállítja a(z) {appName} alkalmazást, hogy feldolgozza ezeket a letöltéseket", + "EnableAutomaticSearchHelpText": "Akkor lesz használatos, ha automatikus keresést hajt végre a felhasználói felületen vagy a(z) {appName} alkalmazáson keresztül", + "ErrorLoadingPage": "Hiba történt az oldal betöltésekor", + "RemoveFromDownloadClientHint": "Távolítsa el a letöltést és a fájlokat) a letöltési kliensből", + "RemoveMultipleFromDownloadClientHint": "Eltávolítja a letöltéseket és fájlokat a letöltési kliensből" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index adae40438..d1e93e9d3 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -107,7 +107,7 @@ "AbsoluteEpisodeNumber": "Numero Episodio Assoluto", "AddConditionError": "Non è stato possibile aggiungere una nuova condizione. Riprova.", "AddConnection": "Aggiungi Connessione", - "AddAutoTagError": "Non è stato possibile aggiungere una nuova etichetta automatica. Riprova.", + "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.", @@ -248,5 +248,6 @@ "AddListExclusionSeriesHelpText": "Impedisce che le serie vengano aggiunte a{appName} tramite liste", "AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto", "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", - "AutoRedownloadFailed": "Download fallito" + "AutoRedownloadFailed": "Download fallito", + "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova." } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index ff5e3a7f4..ff993454a 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -151,5 +151,24 @@ "AutoRedownloadFailed": "Opnieuw downloaden mislukt", "AutoRedownloadFailedFromInteractiveSearch": "Opnieuw downloaden mislukt vanuit interactief zoeken", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Zoek en download automatisch een andere release als een release vanuit interactief zoeken mislukt is", - "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Bevestig het nieuwe wachtwoord" + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Bevestig het nieuwe wachtwoord", + "AutomaticSearch": "Automatisch Zoeken", + "Automatic": "Automatisch", + "BackupRetentionHelpText": "Automatische veiligheidskopieën ouder dan de retentie periode zullen worden opgeruimd", + "BackupsLoadError": "Kon geen veiligheidskopieën laden", + "Backup": "Veiligheidskopie", + "Backups": "Veiligheidskopieën", + "AutoTaggingSpecificationMaximumYear": "Maximum Jaar", + "AutoTaggingSpecificationMinimumYear": "Minimum Jaar", + "AutoTaggingSpecificationOriginalLanguage": "Taal", + "AutoTaggingSpecificationQualityProfile": "Kwaliteitsprofiel", + "AutoTaggingSpecificationRootFolder": "Hoofdmap", + "AutoTaggingSpecificationSeriesType": "Series Type", + "AutoTaggingSpecificationStatus": "Status", + "BackupIntervalHelpText": "Tussentijd voor automatische back-up", + "AddRootFolderError": "De hoofd map kon niet toegevoegd worden", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 49bd34a10..5a34836b9 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -479,7 +479,7 @@ "Condition": "Condição", "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões Regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam escape com um `\\`", "ConnectSettings": "Configurações de Conexão", - "ConnectSettingsSummary": "Notificações, conexões com servidores/reprodutores de mídia e scripts personalizados", + "ConnectSettingsSummary": "Notificações, conexões com servidores/players de mídia e scripts personalizados", "Connections": "Conexões", "CopyToClipboard": "Copiar para Área de Transferência", "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo propagados. Você pode desativar temporariamente a propagação e usar a função de renomeação do {appName} como solução alternativa.", @@ -597,7 +597,7 @@ "ImportListExclusionsLoadError": "Não foi possível carregar as exclusões da lista de importação", "ImportListSettings": "Configurações de Importar listas", "ImportListsLoadError": "Não foi possível carregar Importar listas", - "ImportListsSettingsSummary": "Importar de outra instância do {appName} ou de listas do Trakt, e gerenciar as exclusões de lista", + "ImportListsSettingsSummary": "Importe de outra instância do {appName} ou listas do Trakt e gerencie exclusões de listas", "ImportScriptPath": "Caminho para importar script", "ImportScriptPathHelpText": "O caminho para o script a ser usado para importar", "ImportUsingScript": "Importar usando script", @@ -729,7 +729,7 @@ "QualityProfiles": "Perfis de Qualidade", "QualityProfilesLoadError": "Não é possível carregar perfis de qualidade", "QualitySettings": "Configurações de Qualidade", - "QualitySettingsSummary": "Tamanhos de qualidade e nomenclatura", + "QualitySettingsSummary": "Tamanhos e nomenclatura de qualidade", "Range": "Faixa", "RecyclingBin": "Lixeira", "RecyclingBinCleanup": "Esvaziar Lixeira", @@ -865,7 +865,7 @@ "TagDetails": "Detalhes da Tag - {label}", "TagIsNotUsedAndCanBeDeleted": "A tag não é usada e pode ser excluída", "TagsLoadError": "Não foi possível carregar as tags", - "TagsSettingsSummary": "Veja todas as tags e como elas são usadas. Tags não utilizadas podem ser removidas", + "TagsSettingsSummary": "Veja todas as etiquetas e como elas são usadas. Etiquetas não utilizadas podem ser removidas", "TestAllClients": "Testar todos os clientes", "TestAllIndexers": "Testar todos os indexadores", "TestAllLists": "Testar todas as listas", @@ -885,7 +885,7 @@ "UiLanguage": "Idioma da UI", "UiLanguageHelpText": "Idioma que o {appName} usará para interface do usuário", "UiSettings": "Configurações da UI", - "UiSettingsSummary": "Opções de calendário, data e cores para deficientes visuais", + "UiSettingsSummary": "Opções de calendário, data e cores para daltônicos", "Underscore": "Sublinhar", "Ungroup": "Desagrupar", "Unlimited": "Ilimitado", @@ -1231,7 +1231,7 @@ "RootFolderSelectFreeSpace": "{freeSpace} Livre", "Search": "Pesquisar", "SelectDownloadClientModalTitle": "{modalTitle} - Selecione o Cliente de Download", - "SelectReleaseGroup": "Selecionar Grupo do Lançamento", + "SelectReleaseGroup": "Selecionar um Grupo de Lançamento", "SelectSeason": "Selecionar Temporada", "SelectSeasonModalTitle": "{modalTitle} - Selecione a Temporada", "SetReleaseGroup": "Definir Grupo do Lançamento", @@ -1279,7 +1279,7 @@ "SelectLanguage": "Selecione o Idioma", "SelectLanguageModalTitle": "{modalTitle} - Selecione o Idioma", "SelectLanguages": "Selecione os Idiomas", - "SelectQuality": "Selecionar Qualidade", + "SelectQuality": "Selecionar uma Qualidade", "SelectSeries": "Selecionar a Série", "SetReleaseGroupModalTitle": "{modalTitle} - Definir Grupo de Lançamento", "Shutdown": "Desligar", @@ -1454,7 +1454,7 @@ "SearchForAllMissingEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", "SearchSelected": "Pesquisar Selecionado", "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atendido", - "MassSearchCancelWarning": "Após começar, não é possível cancelar sem reiniciar o {appName} ou desabilitar todos os seus indexadores.", + "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar {appName} ou desabilitar todos os seus indexadores.", "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", "SearchForCutoffUnmetEpisodes": "Pesquise todos os episódios que o corte não foi atingido", "SearchForCutoffUnmetEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios de {totalRecords} corte não atingido?", @@ -1469,7 +1469,7 @@ "FormatAgeMinutes": "minutos", "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "FormatRuntimeHours": "{hours}h", - "FormatRuntimeMinutes": "{minutes} m", + "FormatRuntimeMinutes": "{minutes}m", "FormatShortTimeSpanHours": "{hours} hora(s)", "FormatShortTimeSpanMinutes": "{minutes} minuto(s)", "FormatShortTimeSpanSeconds": "{seconds} segundo(s)", @@ -1484,7 +1484,7 @@ "QueueFilterHasNoItems": "O filtro de fila selecionado não possui itens", "BlackholeFolderHelpText": "Pasta na qual {appName} armazenará o arquivo {extension}", "Destination": "Destinação", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo aURL json do Deluge, consulte {url}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL json do deluge, consulte {url}", "DownloadClientDelugeValidationLabelPluginFailure": "Falha na configuração do rótulo", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} não conseguiu adicionar o rótulo ao {clientName}.", "DownloadClientDownloadStationProviderMessage": "{appName} não consegue se conectar ao Download Station se a autenticação de dois fatores estiver habilitada em sua conta DSM", @@ -1496,8 +1496,8 @@ "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração NzbGet KeepHistory deve ser menor que 25.000", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0. O que impede que {appName} veja os downloads concluídos.", "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {clientName}, como {url}", - "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se a {clientName}", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Transmission", + "DownloadClientSettingsUseSslHelpText": "Use conexão segura ao conectar-se a {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local de transmissão padrão", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL rpc {clientName}, por exemplo, {url}, o padrão é '{defaultUrl}'", "DownloadClientValidationAuthenticationFailureDetail": "Por favor verifique seu nome de usuário e senha. Verifique também se o host que executa {appName} não está impedido de acessar {clientName} pelas limitações da WhiteList na configuração de {clientName}.", "DownloadClientValidationSslConnectFailureDetail": "{appName} não consegue se conectar a {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar {appName} e {clientName} para não usar SSL.", @@ -1505,11 +1505,11 @@ "PostImportCategory": "Categoria Pós-Importação", "SecretToken": "Token Secreto", "TorrentBlackhole": "Torrent Blackhole", - "TorrentBlackholeSaveMagnetFiles": "Salvar Arquivos Magnéticos", - "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnético se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnético salvos em um arquivo)", + "TorrentBlackholeSaveMagnetFiles": "Salvar Arquivos Magnets", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnet se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", "UnknownDownloadState": "Estado de download desconhecido: {state}", "UsenetBlackhole": "Usenet Blackhole", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de sementes", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de semeação", "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desabilitado para este torrent ou 'Layout de conteúdo de torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "As categorias não são suportadas até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", @@ -1545,8 +1545,8 @@ "DownloadClientFreeboxNotLoggedIn": "Não logado", "DownloadClientFreeboxSettingsApiUrl": "URL da API", "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API Freebox com a versão da API, por exemplo, '{url}', o padrão é '{defaultApiUrl}'", - "DownloadClientFreeboxSettingsAppId": "ID do Aplicativo", - "DownloadClientFreeboxSettingsAppToken": "Token do Aplicativo", + "DownloadClientFreeboxSettingsAppId": "ID do App", + "DownloadClientFreeboxSettingsAppToken": "Token do App", "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API Freebox (ou seja, 'app_token')", "DownloadClientFreeboxSettingsHostHelpText": "Nome do host ou endereço IP do host do Freebox, o padrão é '{url}' (só funcionará se estiver na mesma rede)", "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API Freebox. Verifique as configurações de 'Host', 'Porta' ou 'Usar SSL'. (Erro: {exceptionMessage})", @@ -1560,10 +1560,10 @@ "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeiro e Último Primeiro", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última partes primeiro (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última peças primeiro (qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem Sequencial", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixe em ordem sequencial (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções -> UI da Web -> 'Usar HTTPS em vez de HTTP' em qBittorrent.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções - UI da Web - 'Usar HTTPS em vez de HTTP' em qBittorrent.", "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent não pode resolver o link magnético com DHT desativado", "DownloadClientQbittorrentTorrentStateError": "qBittorrent está relatando um erro", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está baixando metadados", @@ -1578,7 +1578,7 @@ "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "O Fila de Torrent não está habilitado nas configurações do qBittorrent. Habilite-o no qBittorrent ou selecione ‘Último’ como prioridade.", "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de seed. {appName} lidará com a remoção automática de torrents com base nos critérios de propagação atuais em Configurações->Indexadores somente quando Remover Concluído estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização rTorrent, que pode ser usada em scripts rTorrent para personalizar o comportamento.", "DownloadClientRTorrentSettingsAddStopped": "Adicionar Parado", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "A ativação adicionará torrents e ímãs ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitando, irá adicionar torrents e magnets ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", "DownloadClientRTorrentSettingsUrlPath": "Caminho da URL", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção ‘Verificar antes do download’ no Sabnbzd", @@ -1620,8 +1620,8 @@ "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.", "DownloadStationStatusExtracting": "Extraindo: {progress}%", - "TorrentBlackholeSaveMagnetFilesExtension": "Salvar extensão de arquivos magnéticos", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensão a ser usada para links magnéticos, o padrão é '.magnet'", + "TorrentBlackholeSaveMagnetFilesExtension": "Salvar Arquivos Magnet com Extensão", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensão a ser usada para links magnet, o padrão é '.magnet'", "TorrentBlackholeSaveMagnetFilesReadOnly": "Só Leitura", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Em vez de mover arquivos, isso instruirá {appName} a copiar ou vincular (dependendo das configurações/configuração do sistema)", "TorrentBlackholeTorrentFolder": "Pasta do Torrent", @@ -1646,9 +1646,9 @@ "IndexerSettingsPasskey": "Passkey", "IndexerSettingsRssUrl": "URL do RSS", "IndexerSettingsSeasonPackSeedTime": "Tempo de Seed de Pack de Temporada", - "IndexerSettingsSeedRatio": "Taxa de Semeação", + "IndexerSettingsSeedRatio": "Proporção de Semeação", "IndexerSettingsSeedTime": "Tempo de Semeação", - "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser propagado antes de parar, vazio usa o padrão do cliente de download", + "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser semeado antes de parar, vazio usa o padrão do cliente de download", "IndexerSettingsWebsiteUrl": "URL do Website", "IndexerValidationCloudFlareCaptchaExpired": "O token CloudFlare CAPTCHA expirou, atualize-o.", "IndexerValidationFeedNotSupported": "O feed do indexador não é compatível: {exceptionMessage}", @@ -1679,8 +1679,8 @@ "IndexerHDBitsSettingsCategories": "Categorias", "IndexerHDBitsSettingsCategoriesHelpText": "se não for especificado, todas as opções serão usadas.", "IndexerHDBitsSettingsCodecs": "Codecs", - "IndexerHDBitsSettingsCodecsHelpText": "Se não for especificado, todas as opções serão usadas.", - "IndexerHDBitsSettingsMediums": "Meio", + "IndexerHDBitsSettingsCodecsHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsMediums": "Meios", "IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.", "ClearBlocklist": "Limpar lista de bloqueio", "MonitorRecentEpisodesDescription": "Monitore episódios exibidos nos últimos 90 dias e episódios futuros", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 3dd8540ee..f5271efdd 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -195,5 +195,21 @@ "AllFiles": "Все файлы", "AllSeriesAreHiddenByTheAppliedFilter": "Все результаты скрыты фильтром", "AlreadyInYourLibrary": "Уже в вашей библиотеке", - "Always": "Всегда" + "Always": "Всегда", + "Conditions": "Условия", + "AddAutoTag": "", + "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", + "CustomFormatsSettings": "Настройки пользовательских форматов", + "Daily": "Ежедневно", + "AnalyticsEnabledHelpText": "Отправлять в {appName} анонимную информацию об использовании и ошибках. Анонимная статистика включает в себя информацию о браузере, какие страницы веб-интерфейса {appName} загружены, сообщения об ошибках, а также операционной системе. Мы используем эту информацию для выявления ошибок, а также для разработки нового функционала.", + "AppDataDirectory": "Директория AppData", + "AddANewPath": "Добавить новый путь", + "CustomFormatsLoadError": "Невозможно загрузить Специальные Форматы", + "CustomFormatsSpecificationLanguage": "Язык", + "CustomFormatsSpecificationMaximumSize": "Максимальный размер", + "CustomFormatsSpecificationMinimumSize": "Минимальный размер", + "CustomFormatsSpecificationRegularExpression": "Регулярное выражение", + "CustomFormatsSpecificationReleaseGroup": "Релиз группа", + "CustomFormatsSpecificationResolution": "Разрешение", + "CustomFormatsSpecificationSource": "Источник" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index ef5f17a5a..c1633f7ce 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -7,12 +7,63 @@ "AddConditionImplementation": "Koşul Ekle - {implementationName}", "EditConnectionImplementation": "Koşul Ekle - {implementationName}", "AddConnectionImplementation": "Koşul Ekle - {implementationName}", - "AddIndexerImplementation": "Koşul Ekle - {implementationName}", + "AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}", "EditIndexerImplementation": "Koşul Ekle - {implementationName}", - "AddToDownloadQueue": "İndirme sırasına ekle", + "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", "AddedToDownloadQueue": "İndirme sırasına eklendi", "AllTitles": "Tüm Filmler", "AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları", "Actions": "Eylemler", - "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası" + "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası", + "AddListExclusionError": "Yeni bir hariç tutma listesi eklenemiyor, lütfen tekrar deneyin.", + "AddListExclusion": "Liste Hariç Tutma Ekle", + "AddNewRestriction": "Yeni kısıtlama ekle", + "AddedDate": "Eklendi: {date}", + "Activity": "Etkinlik", + "Added": "Eklendi", + "AirDate": "Yayınlanma Tarihi", + "Add": "Ekle", + "AddingTag": "Etiket ekleniyor", + "Age": "Yaş", + "AgeWhenGrabbed": "Yaş (yakalandığında)", + "AddDelayProfileError": "Yeni bir gecikme profili eklenemiyor, lütfen tekrar deneyin.", + "AddImportList": "İçe Aktarım Listesi Ekle", + "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", + "AddImportListExclusionError": "Yeni bir içe aktarım listesi dışlaması eklenemiyor, lütfen tekrar deneyin.", + "AddImportListImplementation": "İçe Aktarım Listesi Ekle -{implementationName}", + "AddIndexer": "Dizin Oluşturucu Ekle", + "AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın", + "AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", + "AddReleaseProfile": "Sürüm Profili Ekle", + "AddRemotePathMapping": "Uzak Yol Eşleme Ekleme", + "AddRootFolder": "Kök Klasör Ekle", + "AddSeriesWithTitle": "{title} Ekleyin", + "Agenda": "Ajanda", + "Airs": "Yayınlar", + "AddIndexerError": "Yeni dizin oluşturucu eklenemiyor, lütfen tekrar deneyin.", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Karşılanmamış bölümleri aramaya başlayın", + "AddQualityProfileError": "Yeni kalite profili eklenemiyor, lütfen tekrar deneyin.", + "AddRemotePathMappingError": "Yeni bir uzak yol eşlemesi eklenemiyor, lütfen tekrar deneyin.", + "AfterManualRefresh": "Manüel Yenilemeden Sonra", + "AddAutoTagError": "Yeni bir otomatik etiket eklenemiyor, lütfen tekrar deneyin.", + "AddConditionError": "Yeni bir koşul eklenemiyor, lütfen tekrar deneyin.", + "AddCustomFormat": "Özel Format Ekle", + "AddCustomFormatError": "Yeni bir özel biçim eklenemiyor, lütfen tekrar deneyin.", + "AddDelayProfile": "Gecikme Profili Ekleme", + "AddNewSeries": "Yeni Dizi Ekle", + "AddNewSeriesError": "Arama sonuçları yüklenemedi, lütfen tekrar deneyin.", + "AddNewSeriesHelpText": "Yeni bir dizi eklemek kolaydır, eklemek istediğiniz dizinin adını yazmaya başlamanız yeterlidir.", + "AddNewSeriesRootFolderHelpText": "'{folder}' alt klasörü otomatik olarak oluşturulacaktır", + "AddQualityProfile": "Kalite Profili Ekle", + "AddDownloadClient": "İndirme İstemcisi Ekle", + "AddANewPath": "Yeni bir yol ekle", + "AddDownloadClientError": "Yeni bir indirme istemcisi eklenemiyor, lütfen tekrar deneyin.", + "AddCustomFilter": "Özel Filtre Ekleyin", + "AddDownloadClientImplementation": "İndirme İstemcisi Ekle - {implementationName}", + "AddExclusion": "Hariç Tutma Ekleme", + "AddList": "Liste Ekleyin", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 5c0aa8e06..08654e60c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1787,5 +1787,6 @@ "NotificationsPushBulletSettingSenderId": "发送 ID", "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", - "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值" + "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", + "ChangeCategory": "改变分类" } From 625e500132681429061ee83774c5f6c73590e8da Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 17 Feb 2024 22:16:53 -0800 Subject: [PATCH 118/762] Use 'paths-ignore' instead of 'path' with only negative matches --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e608bed69..9e7e4c836 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,14 +5,14 @@ on: branches: - develop - main - paths: - - '!src/Sonarr.Api.*/openapi.json' + paths-ignore: + - 'src/Sonarr.Api.*/openapi.json' pull_request: branches: - develop - paths: - - '!src/NzbDrone.Core/Localization/Core/**' - - '!src/Sonarr.Api.*/openapi.json' + paths-ignore: + - 'src/NzbDrone.Core/Localization/Core/**' + - 'src/Sonarr.Api.*/openapi.json' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 6f6036a19978d1be15f37bbb984060a5fdd4d5aa Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 19 Feb 2024 17:58:40 +0000 Subject: [PATCH 119/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Chaoshuai Lü <lcs@meta.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: wgwqd <wgwqd@163.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 48 ++++- src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 177 +++++++++++++++++- .../Localization/Core/pt_BR.json | 3 +- .../Localization/Core/zh_CN.json | 62 ++++-- 5 files changed, 263 insertions(+), 28 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index e2091514a..cb90cbc58 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1138,7 +1138,6 @@ "InvalidFormat": "Formato Inválido", "IndexerValidationUnableToConnectTimeout": "No se ha podido conectar con el indexador, posiblemente debido a un tiempo de espera excedido. Inténtalo de nuevo o comprueba tu configuración de red. {exceptionMessage}.", "InteractiveSearchResultsSeriesFailedErrorMessage": "La búsqueda ha fallado porque es {message}. Intente actualizar la información de la serie y compruebe que la información necesaria está presente antes de volver a buscar.", - "IndexerValidationJackettNoResultsInConfiguredCategories": "La solicitud se ha realizado correctamente, pero no se han devuelto resultados en las categorías configuradas en su indexador. Esto puede ser un problema con el indexador o con la configuración de categorías de tu indexador.", "IndexerValidationUnableToConnectResolutionFailure": "No se puede conectar con el indexador, fallo de conexión. Compruebe su conexión con el servidor del indexador y DNS. {exceptionMessage}.", "InstallLatest": "Instala el último", "InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog", @@ -1155,7 +1154,6 @@ "LibraryImportTips": "Algunos consejos para que la importación vaya sobre ruedas:", "IndexerValidationTestAbortedDueToError": "El test fue abortado debido a un error: {exceptionMessage}", "Large": "Grande", - "IndexerValidationJackettNoRssFeedQueryAvailable": "No hay consulta de fuente RSS disponible. Puede tratarse de un problema con el indexador o con la configuración de la categoría del indexador.", "IndexerValidationUnableToConnectInvalidCredentials": "No se puede conectar al indexador, credenciales no válidas. {exceptionMessage}.", "IndexerValidationUnableToConnectServerUnavailable": "No se puede conectar con el indexador, el servidor del indexador no está disponible. Inténtelo de nuevo más tarde. {exceptionMessage}.", "InteractiveImport": "Importación Interactiva", @@ -1190,5 +1188,49 @@ "LibraryImportTipsSeriesUseRootFolder": "Dirija {appName} a la carpeta que contiene todas sus series de TV, no a una en concreto. Por ejemplo, \"`{goodFolderExample}`\" y no \"`{badFolderExample}`\". Además, cada serie debe estar en su propia carpeta dentro de la carpeta raíz/biblioteca.", "ListSyncTag": "Etiqueta de Sincronización de Lista", "ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)", - "UnableToLoadListOptions": "No se pueden cargar opciones de lista" + "MetadataLoadError": "No se puede cargar Metadatos", + "MetadataSourceSettingsSeriesSummary": "Información de dónde {appName} obtiene información de series y episodio", + "Max": "Máximo", + "MaximumSizeHelpText": "Tamaño máximo en MB para que un lanzamiento sea capturado. Establece a cero para establecer a ilimitado", + "MatchedToEpisodes": "Ajustado a Episodios", + "MediaInfo": "Información de medios", + "MediaManagementSettingsSummary": "Nombrado, opciones de gestión de archivos y carpetas raíz", + "MetadataSource": "Fuente de metadatos", + "ManualImport": "Importación manual", + "MatchedToSeries": "Ajustado a Series", + "MediaManagement": "Gestión de medios", + "MetadataPlexSettingsSeriesPlexMatchFile": "Emparejado de archivos de series de Plex", + "MetadataSettings": "Opciones de metadatos", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Crea un archivo .plexmatch en la carpeta de series", + "MetadataSettingsEpisodeMetadata": "Metadatos de episodio", + "MetadataSettingsEpisodeMetadataImageThumbs": "Metadatos de miniaturas de episodio", + "MetadataSettingsSeriesImages": "Imágenes de series", + "MetadataSettingsSeriesMetadata": "Metadatos de series", + "MatchedToSeason": "Ajustado a Temporada", + "Medium": "Mediano", + "MegabytesPerMinute": "Megabytes por minuto", + "MediaManagementSettings": "Opciones de gestión de medios", + "Mechanism": "Mecanismo", + "Mapping": "Mapeo", + "ManualImportItemsLoadError": "No se puede cargar los elementos de importación manual", + "MarkAsFailed": "Marcar como Fallido", + "MarkAsFailedConfirmation": "¿Estás seguro que quieres marcar '{sourceTitle}' como fallido?", + "MaximumLimits": "Límites máximos", + "MaximumSingleEpisodeAge": "Tiempo máximo de Episodio individual", + "MediaManagementSettingsLoadError": "No se puede cargar las opciones de gestión de medios", + "MetadataProvidedBy": "Los metadatos son proporcionados por {provider}", + "MetadataSettingsEpisodeImages": "Imágenes de episodio", + "MetadataSettingsSeasonImages": "Imágenes de temporada", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Metadatos de guía de episodio de series", + "MetadataSettingsSeriesMetadataUrl": "Metadatos de URL de series", + "MetadataSourceSettings": "Opciones de fuente de metadatos", + "MetadataSettingsSeriesSummary": "Crea archivos de metadatos cuando los episodios son importados o las series son refrescadas", + "Metadata": "Metadatos", + "MassSearchCancelWarning": "Esto no puede ser cancelado una vez empiece sin reiniciar {appName} o deshabilitar todos tus indexadores.", + "MaximumSingleEpisodeAgeHelpText": "Durante una búsqueda completa de temporada, solo serán permitidos los paquetes de temporada cuando el último episodio de temporada sea más antiguo que esta configuración. Solo series estándar. Usa 0 para deshabilitar.", + "MaximumSize": "Tamaño máximo", + "IndexerValidationNoResultsInConfiguredCategories": "Petición con éxito, pero no se devolvió ningún resultado en las categorías configuradas de tu indexador. Esto puede ser un problema con el indexador o tus ajustes de categoría de tu indexador.", + "IndexerValidationNoRssFeedQueryAvailable": "Ninguna consulta de canales RSS disponible. Esto puede ser un problema con el indexador o con los ajustes de categoría de tu indexador.", + "MappedNetworkDrivesWindowsService": "Los discos de red mapeados no están disponibles cuando se ejecutan como un servicio de Windows, consulta el [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) para más información.", + "MediaInfoFootNote": "MediaInfo Full/Idiomas de audio/Idiomas de subtítulo soporta un sufijo `:ES+DE` que te permite filtrar los idiomas incluidos en el nombre de archivo. Usa `-DE` para excluir idiomas específicos. Añadir `+` (eg `:ES+`) devolverá `[ES]`/`[ES+--]`/`[--]` dependiendo de los idiomas excluidos. Por ejemplo `{MediaInfo Full:ES+DE}`." } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 1766747ba..e4498e928 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1271,7 +1271,6 @@ "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", - "DownloadClientDownloadStationSettingsDirectory": "Valinnainen jaettu kansio latauksille. Download Stationin oletussijaintia jättämällä tyhjäksi.", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 10c0881c0..9bc89c45d 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1102,7 +1102,6 @@ "CountSelectedFile": "{selectedCount} kiválasztott fájl", "CutoffUnmetLoadError": "Hiba a nem teljesített elemek betöltésekor", "CutoffUnmetNoItems": "Nincsenek teljesítetlen elemek levágása", - "DownloadClientDownloadStationSettingsDirectory": "Opcionális megosztott mappa a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Download Station hely használatához", "DownloadClientDelugeSettingsUrlBaseHelpText": "Előtagot ad a deluge json URL-hez, lásd: {url}", "DownloadClientFloodSettingsAdditionalTags": "További címkék", "DownloadClientFloodSettingsStartOnAdd": "Kezdje a Hozzáadás lehetőséggel", @@ -1232,5 +1231,179 @@ "EnableAutomaticSearchHelpText": "Akkor lesz használatos, ha automatikus keresést hajt végre a felhasználói felületen vagy a(z) {appName} alkalmazáson keresztül", "ErrorLoadingPage": "Hiba történt az oldal betöltésekor", "RemoveFromDownloadClientHint": "Távolítsa el a letöltést és a fájlokat) a letöltési kliensből", - "RemoveMultipleFromDownloadClientHint": "Eltávolítja a letöltéseket és fájlokat a letöltési kliensből" + "RemoveMultipleFromDownloadClientHint": "Eltávolítja a letöltéseket és fájlokat a letöltési kliensből", + "InteractiveImportNoQuality": "Minden kiválasztott fájlhoz ki kell választani a minőséget", + "ImportScriptPath": "Szkript elérési út importálása", + "ImportScriptPathHelpText": "Az importáláshoz használandó szkript elérési útja", + "ImportSeries": "Sorozat importálása", + "ImportUsingScript": "Importálás Script használatával", + "InteractiveSearch": "Interaktív Keresés", + "ImportUsingScriptHelpText": "Fájlok másolása szkript segítségével történő importáláshoz (pl. átkódoláshoz)", + "IncludeUnmonitored": "Tartalmazza a Nem felügyeltet", + "InteractiveImportNoSeason": "Minden kiválasztott fájlhoz ki kell választani az évadot", + "Mixed": "Mixed", + "ImportListsTraktSettingsGenresHelpText": "Sorozatok szűrése Trakt Genre Slug szerint (vesszővel elválasztva) Csak a népszerű listákhoz", + "ImportListsTraktSettingsListName": "Lista név", + "ImportListsTraktSettingsLimitHelpText": "Korlátozza a beszerezhető sorozatok számát", + "ImportListsTraktSettingsListNameHelpText": "Az importáláshoz szükséges listanév, a listának nyilvánosnak kell lennie, vagy hozzáféréssel kell rendelkeznie a listához", + "ImportListsTraktSettingsListType": "Lista típus", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Várható műsorok", + "ImportListsTraktSettingsPopularListTypePopularShows": "Népszerű műsorok", + "ImportListsTraktSettingsListTypeHelpText": "Az importálni kívánt lista típusa", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Évek szerint ajánlott műsorok", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Ajánlott műsorok hetente", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Felkapott műsorok", + "ImportListsTraktSettingsPopularName": "Trakt Népszerű lista", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "A legjobban nézett műsorok év szerint", + "ImportListsTraktSettingsRating": "Értékelés", + "ImportListsTraktSettingsUserListName": "Trakt Felhasználó", + "ImportListsTraktSettingsUserListUsernameHelpText": "Felhasználónév az importálandó listához (hagyja üresen az Auth User használatához)", + "ImportListsTraktSettingsUserListTypeWatched": "Felhasználói figyelt lista", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Ha a Lista típusa Figyelt, válassza ki az importálni kívánt sorozattípust", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Ha a Lista típusa Figyelt, válassza ki a sorrendet a lista rendezéséhez", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Megnézve", + "ImportListsTraktSettingsWatchedListTypeAll": "Minden", + "ImportListsTraktSettingsWatchedListTypeInProgress": "Folyamatban", + "ImportListsTraktSettingsYearsHelpText": "Sorozatok szűrése év vagy évtartomány szerint", + "ImportListsValidationInvalidApiKey": "Az API-kulcs érvénytelen", + "ImportListsValidationTestFailed": "A teszt megszakadt hiba miatt: {exceptionMessage}", + "IncludeCustomFormatWhenRenaming": "Átnevezéskor adja meg az Egyéni formátumot", + "InteractiveSearchModalHeader": "Interaktív Keresés", + "InteractiveSearchSeason": "Interaktív keresés az évad összes epizódjához", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "A TVDB műsor URL-jének szerepeltetése a tvshow.nfo-ban (kombinálható a „Sorozat metaadataival”)", + "ImportedTo": "Importált ide", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Minden idők ajánlott műsorai", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Ajánlott műsorok havonta", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "A legnézettebb műsorok hónapok szerint", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "A legnézettebb műsorok heti bontásban", + "ImportListsTraktSettingsRatingHelpText": "Sorozatok szűrése értékelési tartomány szerint (0-100)", + "ImportListsTraktSettingsUsernameHelpText": "Felhasználónév az importálandó listához", + "ImportListsTraktSettingsWatchedListSorting": "Figyelőlista rendezése", + "ImportListsTraktSettingsYears": "Évek", + "ImportListsTraktSettingsWatchedListFilter": "Figyelt lista szűrője", + "MinimumAgeHelpText": "Csak Usenet: Az NZB-k minimális életkora percekben, mielőtt elkapnák őket. Használja ezt, hogy időt adjon az új kiadásoknak, hogy eljuthassanak a usenet szolgáltatóhoz.", + "LogOnly": "Csak naplózás", + "MonitorPilotEpisode": "Pilot epizód", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Minden idők legjobban nézett műsorai", + "ImportListsTraktSettingsUserListTypeCollection": "Felhasználói gyűjtőlista", + "ImportListsTraktSettingsUserListTypeWatch": "Felhasználói figyelőlista", + "InteractiveImportNoSeries": "Minden kiválasztott fájlhoz sorozatot kell választani", + "ListSyncLevelHelpText": "A könyvtárban lévő sorozatokat a választása alapján kezeljük, ha kimaradnak, vagy nem jelennek meg a listá(k)on", + "ListSyncTagHelpText": "Ez a címke akkor kerül hozzáadásra, ha egy sorozat kiesik, vagy már nem szerepel a listán(ok)", + "MediaInfoFootNote": "A MediaInfo Full/AudioLanguages/SubtitleLanguages támogatja az „:EN+DE” utótagot, amely lehetővé teszi a fájlnévben szereplő nyelvek szűrését. Adott nyelvek kizárásához használja a \"-DE\" billentyűt. A `+` hozzáfűzése (pl. `:EN+`) az `[EN]`/`[EN+--]`/`[--]` karakterláncot adja ki a kizárt nyelvektől függően. Például `{MediaInfo Full:EN+DE}`.", + "ReleaseSceneIndicatorAssumingScene": "Jelenetszámozást feltételezve.", + "RssIsNotSupportedWithThisIndexer": "Ez az indexelő nem támogatja az RSS-t", + "SceneInformation": "Jelenet Információ", + "QualityDefinitionsLoadError": "Nem sikerült betölteni a minőségi meghatározásokat", + "QualityLimitsSeriesRuntimeHelpText": "A korlátok automatikusan igazodnak a sorozat futási idejéhez és a fájlban lévő epizódok számához.", + "QualityProfileInUseSeriesListCollection": "Nem törölhető egy sorozathoz, listához vagy gyűjteményhez csatolt minőségi profil", + "RemoveFromQueue": "Eltávolítás a sorból", + "Reorder": "Újrarendelés", + "Repeat": "Ismétlés", + "ResetAPIKeyMessageText": "Biztosan visszaállítja API-kulcsát?", + "RestartLater": "Később újraindítom", + "PendingDownloadClientUnavailable": "Függőben – A letöltési kliens nem érhető el", + "QualityDefinitions": "Minőségi meghatározások", + "QueueIsEmpty": "A sor üres", + "ReleaseProfilesLoadError": "Nem sikerült betölteni a release profilokat", + "RemoveFailedDownloadsHelpText": "A sikertelen letöltések eltávolítása a letöltési ügyfélelőzményekből", + "RenameEpisodes": "Epizódok átnevezése", + "RestartNow": "Újraindítás most", + "RestartRequiredHelpTextWarning": "Újraindítás szükséges az életbe lépéshez", + "RestrictionsLoadError": "Nem sikerült betölteni a korlátozásokat", + "RestartSonarr": "{appName} újraindítása", + "QualitySettingsSummary": "Minőségi méretek és elnevezések", + "RemoveCompletedDownloadsHelpText": "Távolítsa el az importált letöltéseket a letöltési ügyfélelőzményekből", + "PendingChangesMessage": "Vannak nem mentett módosításai. Biztosan elhagyja ezt az oldalt?", + "Permissions": "Engedélyek", + "PreferredProtocol": "Preferált protokoll", + "QualitiesLoadError": "Nem lehet minőségeket betölteni", + "RemoveTagsAutomatically": "Címkék automatikus eltávolítása", + "ResetAPIKey": "API Kulcs Visszaállítása", + "RootFoldersLoadError": "Nem sikerült betölteni a gyökérmappákat", + "RootFolders": "Gyökér mappák", + "ProfilesSettingsSummary": "Minőségi, nyelv késleltetési és Release profilok", + "PendingChangesStayReview": "Maradjon és tekintse át a változtatásokat", + "PreferredSize": "Preferált méret", + "QualitySettings": "Minőség Beállítások", + "RemoveDownloadsAlert": "Az Eltávolítási beállítások átkerültek a fenti táblázatban a Letöltési kliens egyéni beállításaiba.", + "RemovingTag": "Címke eltávolítása", + "RescanAfterRefreshSeriesHelpText": "Olvassa be újra a sorozat mappát a sorozat frissítése után", + "RestoreBackup": "Biztonsági mentés visszaállítása", + "RssSyncIntervalHelpText": "Intervallum percekben. A letiltáshoz állítsa nullára (ez leállítja az összes automatikus feloldást)", + "ProxyUsernameHelpText": "Csak akkor kell megadnia egy felhasználónevet és jelszót, ha szükséges. Ellenkező esetben hagyja üresen.", + "SaveChanges": "Változtatások mentése", + "Port": "Port", + "ProxyBypassFilterHelpText": "Használja a ',' jelet elválasztóként és a '*' jelet. helyettesítő karakterként az aldomainekhez", + "PublishedDate": "Közzététel dátuma", + "QualityCutoffNotMet": "A minőségi korlát nem teljesült", + "RemoveFromBlocklist": "Eltávolítás a tiltólistáról", + "RemovedFromTaskQueue": "Eltávolítva a feladatsorból", + "RemoveSelected": "A kiválasztott eltávolítása", + "RescanSeriesFolderAfterRefresh": "A sorozatmappa újraolvasása a frissítés után", + "ResetQualityDefinitions": "Minőségi meghatározások Visszaállítása", + "ResetTitles": "Címek Visszaállítása", + "RetentionHelpText": "Csak Usenet: Állítsa nullára a korlátlan megőrzéshez", + "RootFolderPath": "Gyökérmappa elérési útja", + "Rss": "RSS", + "RssSync": "RSS Sync", + "RssSyncIntervalHelpTextWarning": "Ez minden indexelőre vonatkozik, kérjük, kövesse az általuk meghatározott szabályokat", + "SaveSettings": "Beállítások mentése", + "Scene": "Jelenet", + "SceneInfo": "Jelenet Infó", + "SceneNumberNotVerified": "A jelenet számát még nem ellenőrizték", + "SceneNumbering": "Jelenet számozás", + "SearchForCutoffUnmetEpisodes": "Az összes Cutoff Unmet epizód keresése", + "PreviewRename": "Előnézet Átnevezés", + "ReleaseSceneIndicatorAssumingTvdb": "TVDB számozást feltételezve.", + "ReleaseTitle": "Release kiadás", + "RssSyncInterval": "RSS szinkronizálási időköz", + "PortNumber": "Port száma", + "QueueFilterHasNoItems": "A kiválasztott sorszűrőben nincsenek elemek", + "SearchForAllMissingEpisodes": "Keresse meg az összes hiányzó epizódot", + "QualityProfiles": "Minőségi profilok", + "QualityProfilesLoadError": "Nem sikerült betölteni a minőségi profilokat", + "RemoveRootFolder": "A gyökérmappa eltávolítása", + "RemoveSelectedBlocklistMessageText": "Biztosan eltávolítja a kijelölt elemeket a tiltólistáról?", + "RemotePathMappingsLoadError": "Nem sikerült betölteni a távoli útvonal-leképezéseket", + "RemoveTagsAutomaticallyHelpText": "Ha a feltételek nem teljesülnek, automatikusan távolítsa el a címkéket", + "RenameFiles": "Fájlok átnevezése", + "PostImportCategory": "Import utáni kategória", + "RemoveFilter": "Szűrő Eltávolítás", + "PosterOptions": "Poszter opciók", + "Posters": "Poszterek", + "PrefixedRange": "Előtag tartomány", + "Presets": "Előbeállítások", + "PreviewRenameSeason": "Előnézet Átnevezése ebben az évadban", + "PreviouslyInstalled": "Korábban telepítve", + "PrioritySettings": "Prioritás: {priority}", + "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).", + "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", + "Score": "Pontszám", + "Search": "Keresés", + "SearchByTvdbId": "Kereshet egy műsor TVDB azonosítójával is. például. tvdb:71663", + "SearchFailedError": "A keresés sikertelen, próbálja újra később.", + "SearchForAllMissingEpisodesConfirmationCount": "Biztosan megkeresi az összes {totalRecords} hiányzó epizódot?", + "PosterSize": "Poszter méret", + "ProxyPasswordHelpText": "Csak akkor kell megadnia egy felhasználónevet és jelszót, ha szükséges. Ellenkező esetben hagyja üresen.", + "ReleaseProfileTagSeriesHelpText": "A kiadási profilok a legalább egy megfelelő címkével rendelkező sorozatokra vonatkoznak. Hagyja üresen, ha az összes sorozatra alkalmazni szeretné", + "Paused": "Szüneteltetve", + "SeasonPack": "Szezon Pack", + "SetPermissionsLinuxHelpText": "Futtatandó a chmod a fájlok importálásakor", + "Preferred": "Előnyben részesített", + "SeasonNumberToken": "Évad{seasonNumber}", + "SecretToken": "Titkos token", + "PartialSeason": "Részleges szezon", + "SeasonPassTruncated": "Csak a legutóbbi 25 évad látható, az összes évszak megtekintéséhez menjen a részletekhez", + "Seasons": "Évad", + "Pending": "Függőben levő", + "PendingChangesDiscardChanges": "Vesse el a változtatásokat, és lépjen ki", + "SeasonPremiere": "Évad Premier", + "SeasonPremieresOnly": "Csak az évad premierjei", + "PasswordConfirmation": "Jelszó megerősítése" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 5a34836b9..6fe4cde11 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1316,7 +1316,7 @@ "ShowUnknownSeriesItemsHelpText": "Mostrar itens sem uma série na fila, isso pode incluir séries, filmes removidos ou qualquer outra coisa na categoria do {appName}", "Test": "Teste", "Level": "Nível", - "AddListExclusion": "Adicionar exclusão à lista", + "AddListExclusion": "Adicionar Exclusão de Lista", "AddListExclusionSeriesHelpText": "Impedir que o {appName} adicione séries por listas", "EditSeriesModalHeader": "Editar - {title}", "EditSelectedSeries": "Editar Séries Selecionadas", @@ -2037,7 +2037,6 @@ "ListSyncTag": "Etiqueta de Sincronização de Lista", "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", "LogOnly": "Só Registro", - "UnableToLoadListOptions": "Não foi possível carregar as opções da lista", "CleanLibraryLevel": "Limpar Nível da Biblioteca", "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente." } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 08654e60c..f9e51f2af 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -156,7 +156,7 @@ "AudioLanguages": "音频语言", "Certification": "分级", "Component": "组件", - "ContinuingOnly": "仅继续", + "ContinuingOnly": "仅包含仍在继续的", "CountImportListsSelected": "已选择 {count} 个导入列表", "CountIndexersSelected": "已选择 {count} 个索引器", "CurrentlyInstalled": "已安装", @@ -550,8 +550,8 @@ "AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的版本", "AutoTaggingLoadError": "无法加载自动标记", "Automatic": "自动化", - "AutoTaggingRequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自动标记规则。否则,一个 {implementationName} 匹配就足够了。", - "AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。", + "AutoTaggingRequiredHelpText": "这个{0}条件必须匹配自动标记规则才能应用。否则,一个{0}匹配就足够了。", + "AutoTaggingNegateHelpText": "如果选中,当 {0} 条件匹配时,自动标记不会应用。", "BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理", "BackupsLoadError": "无法加载备份", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "最小自定义格式分数", @@ -794,7 +794,7 @@ "DefaultNameCopiedProfile": "{name} - 复制", "SeriesFinale": "大结局", "SeriesFolderFormat": "剧集文件夹格式", - "ReplaceIllegalCharactersHelpText": "替换非法字符。如果未选中,{appName}将删除它们", + "ReplaceIllegalCharactersHelpText": "替换非法字符,如未勾选,则会被{appName}移除", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们](https://www.thetvdb.com/subscribe)。", "DeleteSelectedSeries": "删除选中的剧集", "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", @@ -806,7 +806,7 @@ "RegularExpressionsTutorialLink": "有关正则表达式的更多详细信息,请参阅[此处](https://www.regular-expressions.info/tutorial.html)。", "FormatDateTime": "{formattedDate} {formattedTime}", "ReleaseProfileTagSeriesHelpText": "发布配置将应用于至少有一个匹配标记的剧集。留空适用于所有剧集", - "FormatShortTimeSpanHours": "{hours} 时", + "FormatShortTimeSpanHours": "{hours}小时", "RemotePathMappingHostHelpText": "与您为远程下载客户端指定的主机相同", "HideEpisodes": "隐藏集", "HistorySeason": "查看本季历史记录", @@ -1063,7 +1063,7 @@ "PreferUsenet": "首选Usenet", "Preferred": "首选的", "Presets": "预设", - "PreferredSize": "首选影片大小", + "PreferredSize": "首选专辑大小", "PreviewRename": "重命名预览", "PreviewRenameSeason": "重命名此季的预览", "PreviousAiringDate": "上一次播出: {date}", @@ -1092,7 +1092,7 @@ "RemoveFromBlocklist": "从黑名单中移除", "RemoveCompletedDownloadsHelpText": "从下载客户端记录中移除已导入的下载", "RemoveFromQueue": "从队列中移除", - "RemoveQueueItem": "删除- {sourceTitle}", + "RemoveQueueItem": "移除 - {sourceTitle}", "RemoveTagsAutomatically": "自动删除标签", "Repeat": "重复", "ResetDefinitions": "重置定义", @@ -1120,10 +1120,10 @@ "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} 的剧集被下载", "SeasonPassTruncated": "只显示最新的25季,点击详情查看所有的季", "SeasonPremieresOnly": "仅限季首播", - "SelectDropdown": "选择...", + "SelectDropdown": "选择…", "SelectEpisodesModalTitle": "{modalTitle} - 选择剧集", "SelectFolderModalTitle": "{modalTitle} - 选择文件夹", - "SelectQuality": "选择品质", + "SelectQuality": "选择质量", "SelectReleaseGroup": "选择发布组", "SeriesDetailsGoTo": "转到 {title}", "SeriesDetailsNoEpisodeFiles": "没有集文件", @@ -1175,7 +1175,7 @@ "TvdbIdExcludeHelpText": "要排除的剧集 TVDB ID", "TvdbId": "TVDB ID", "TypeOfList": "{typeOfList} 列表", - "UiLanguageHelpText": "{appName}将用于UI的语言", + "UiLanguageHelpText": "{appName}使用的UI界面语言", "UiSettings": "UI设置", "UiLanguage": "UI界面语言", "Umask770Description": "{octal} - 所有者和组写入", @@ -1191,7 +1191,7 @@ "UnmonitorSpecialsEpisodesDescription": "取消监控所有特别节目而不改变其他集的监控状态", "UnmonitorDeletedEpisodesHelpText": "从磁盘删除的集将在 {appName} 中自动取消监控", "UnmonitorSpecialEpisodes": "取消监控特别节目", - "UpdateAll": "更新全部", + "UpdateAll": "全部更新", "UpdateAutomaticallyHelpText": "自动下载并安装更新。你还可以在“系统:更新”中安装", "UpdateSelected": "更新选择的内容", "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到影片质量超出或者满足", @@ -1261,11 +1261,11 @@ "FormatAgeHours": "小时", "FormatAgeMinute": "分钟", "FormatAgeMinutes": "分钟", - "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", - "FormatRuntimeHours": "{hours}时", + "FormatDateTimeRelative": "{relativeDay},{formattedDate} {formattedTime}", + "FormatRuntimeHours": "{hours}小时", "FormatRuntimeMinutes": "{minutes}分", - "FormatShortTimeSpanMinutes": "{minutes} 分", - "FormatShortTimeSpanSeconds": "{seconds} 秒", + "FormatShortTimeSpanMinutes": "{minutes}分钟", + "FormatShortTimeSpanSeconds": "{seconds}秒钟", "FormatTimeSpanDays": "{days}天 {time}", "HistoryModalHeaderSeason": "{season} 历史记录", "ImportSeries": "导入剧集", @@ -1378,7 +1378,7 @@ "OnUpgrade": "升级中", "RemotePathMappings": "远程路径映射", "RemotePathMappingsLoadError": "无法加载远程路径映射", - "RemoveQueueItemConfirmation": "您确定要从队列中删除'{sourceTitle}'吗?", + "RemoveQueueItemConfirmation": "您确定要从队列中移除“{sourceTitle}”吗?", "RequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自定义格式。 否则,单个 {implementationName} 匹配就足够了。", "RescanSeriesFolderAfterRefresh": "刷新后重新扫描剧集文件夹", "RestartRequiredToApplyChanges": "{appName}需要重新启动才能应用更改,您想现在重新启动吗?", @@ -1433,7 +1433,7 @@ "Monday": "星期一", "Monitor": "是否监控", "NotificationTriggersHelpText": "选择触发此通知的事件", - "ImportListsSettingsSummary": "从另一个{appName}或Trakt列表导入并管理排除列表", + "ImportListsSettingsSummary": "从另一个 {appName} 实例或 Trakt 列表导入并管理列表排除项", "ParseModalHelpTextDetails": "{appName} 将尝试解析标题并向您显示有关详情", "Proxy": "代理", "ImportScriptPath": "导入脚本路径", @@ -1478,8 +1478,8 @@ "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {appName} 导入之前,已下载的文件会被从您的客户端中移除。", "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", "AutoRedownloadFailed": "重新下载失败", - "AutoRedownloadFailedFromInteractiveSearch": "从交互式搜索中重新下载失败", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从交互式搜索中获取失败的版本时,自动搜索并尝试下载其他版本", + "AutoRedownloadFailedFromInteractiveSearch": "手动搜索重新下载失败", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从手动搜索中获取失败的发行版时,自动搜索并尝试下载不同的发行版", "ImportListSearchForMissingEpisodes": "搜索缺失集", "QueueFilterHasNoItems": "选定的队列过滤器没有项目", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "确认新密码", @@ -1788,5 +1788,27 @@ "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", - "ChangeCategory": "改变分类" + "ChangeCategory": "改变分类", + "IgnoreDownload": "忽略下载", + "IgnoreDownloads": "忽略下载", + "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", + "DoNotBlocklist": "不要列入黑名单", + "DoNotBlocklistHint": "删除而不列入黑名单", + "RemoveQueueItemRemovalMethod": "删除方法", + "BlocklistAndSearch": "黑名单和搜索", + "BlocklistAndSearchMultipleHint": "列入黑名单后开始搜索替代版本", + "BlocklistMultipleOnlyHint": "无需搜索替换的黑名单", + "BlocklistOnly": "仅限黑名单", + "BlocklistOnlyHint": "无需寻找替代版本的黑名单", + "ChangeCategoryMultipleHint": "将下载从下载客户端更改为“导入后类别”", + "CustomFormatsSpecificationRegularExpressionHelpText": "自定义格式正则表达式不区分大小写", + "CustomFormatsSpecificationRegularExpression": "正则表达式", + "RemoveFromDownloadClientHint": "从下载客户端删除下载和文件", + "RemoveMultipleFromDownloadClientHint": "从下载客户端删除下载和文件", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", + "BlocklistAndSearchHint": "列入黑名单后开始寻找一个替代版本", + "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", + "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。" } From 8dd8c95f36d8b0e0dfbf653b6e64ad2161ccdac8 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:40:51 +0200 Subject: [PATCH 120/762] Fixed: Avoid upgrades for custom formats cut-off already met --- .../Specifications/UpgradableSpecification.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index 322e5c568..5b16bb046 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -46,25 +46,34 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return false; } - var qualityRevisionComapre = newQuality?.Revision.CompareTo(currentQuality.Revision); + var qualityRevisionCompare = newQuality?.Revision.CompareTo(currentQuality.Revision); // Accept unless the user doesn't want to prefer propers, optionally they can // use preferred words to prefer propers/repacks over non-propers/repacks. if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && - qualityRevisionComapre > 0) + qualityRevisionCompare > 0) { _logger.Debug("New item has a better quality revision, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); return true; } + // Reject unless the user does not prefer propers/repacks and it's a revision downgrade. + if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && + qualityRevisionCompare < 0) + { + _logger.Debug("Existing item has a better quality revision, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); + return false; + } + var currentFormatScore = qualityProfile.CalculateCustomFormatScore(currentCustomFormats); var newFormatScore = qualityProfile.CalculateCustomFormatScore(newCustomFormats); - // Reject unless the user does not prefer propers/repacks and it's a revision downgrade. - if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && - qualityRevisionComapre < 0) + if (qualityProfile.UpgradeAllowed && currentFormatScore >= qualityProfile.CutoffFormatScore) { - _logger.Debug("Existing item has a better quality revision, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); + _logger.Debug("Existing item meets cut-off for custom formats, skipping. Existing: [{0}] ({1}). Cutoff score: {2}", + currentCustomFormats.ConcatToString(), + currentFormatScore, + qualityProfile.CutoffFormatScore); return false; } @@ -123,7 +132,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return true; } - _logger.Debug("Existing item meets cut-off. skipping. Existing: {0}", currentQuality); + _logger.Debug("Existing item meets cut-off, skipping. Existing: {0}", currentQuality); return false; } From 5c4f82999368edfedd038a0a27d323e04b81a400 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 16 Feb 2024 17:05:52 -0800 Subject: [PATCH 121/762] Fixed: Multi-word genres in Auto Tags Fixed #6488 --- frontend/src/Components/Form/TextTagInputConnector.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Components/Form/TextTagInputConnector.js b/frontend/src/Components/Form/TextTagInputConnector.js index aef065cfa..17677a51e 100644 --- a/frontend/src/Components/Form/TextTagInputConnector.js +++ b/frontend/src/Components/Form/TextTagInputConnector.js @@ -91,6 +91,7 @@ class TextTagInputConnector extends Component { render() { return ( <TagInput + delimiters={['Tab', 'Enter', ',']} tagList={[]} onTagAdd={this.onTagAdd} onTagDelete={this.onTagDelete} From 43797b326d0f9bf9de8b4abe6703b2c5ffff4479 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 13 Feb 2024 17:14:00 -0800 Subject: [PATCH 122/762] New: Parse releases with season and episode numbers separated by a period Closes #6492 --- .../ParserTests/SingleEpisodeParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 9bcb8ca7b..5b88912bb 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -166,6 +166,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title [HDTV 720p][Cap.101](website.com).mkv", "Series Title", 1, 1)] [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] [TestCase("Босх: Спадок / Series: Legacy / S2E1 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] + [TestCase("Titles.s06e01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)] + [TestCase("Titles.s06.01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 622ea72a7..fd040a17b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -191,7 +191,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)(?:[-_. ]|$)(?!\\)", + new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)(?:[-_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with a title, 4 digit season number, Single episodes (S2016E05, etc) & Multi-episode (S2016E05E06, S2016E05-06, S2016E05 E06, etc) @@ -202,6 +202,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:x|\Wx){1,2}(?<episode>\d{2,4}(?!\d+))(?:(?:\-|x|\Wx|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes with a title, Single episodes (s01.05) + new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{2})(?!\d+))(?:\.)(?<episode>\d{2,3}(?!\d+))(?:[-_. ]|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-season pack new Regex(@"^(?<title>.+?)(Complete Series)?[-_. ]+(?:S|(?:Season|Saison|Series|Stagione)[_. ])(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[-_. ]{1}|[-_. ]{3})(?:S|(?:Season|Saison|Series|Stagione)[_. ])?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))", RegexOptions.IgnoreCase | RegexOptions.Compiled), From a7607ac7d63504c9849c78f45186d01b48a6bc7c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 17 Feb 2024 21:59:23 -0800 Subject: [PATCH 123/762] Fixed: Only match via TV Rage ID if TheTVDB ID is not available Closes #6517 --- .../ParserTests/ParsingServiceTests/MapFixture.cs | 13 ++++++++++++- src/NzbDrone.Core/Parser/ParsingService.cs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs index e0277d3e4..0aac2ecbc 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs @@ -191,12 +191,23 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 0, 10, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once()); } + [Test] + public void should_not_FindByTvRageId_when_search_criteria_and_FindByTitle_matching_fails_and_tvdb_id_is_specified() + { + GivenParseResultSeriesDoesntMatchSearchCriteria(); + + Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never()); + } + [Test] public void should_use_tvdbid_matching_when_alias_is_found() { diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index d8cd88cac..5819704f7 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -396,7 +396,7 @@ namespace NzbDrone.Core.Parser return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); } - if (tvRageId > 0 && tvRageId == searchCriteria.Series.TvRageId) + if (tvRageId > 0 && tvRageId == searchCriteria.Series.TvRageId && tvdbId <= 0) { _logger.Debug() .Message("Found matching series by TVRage ID {0}, an alias may be needed for: {1}", tvRageId, parsedEpisodeInfo.SeriesTitle) @@ -446,7 +446,7 @@ namespace NzbDrone.Core.Parser } } - if (series == null && tvRageId > 0) + if (series == null && tvRageId > 0 && tvdbId <= 0) { series = _seriesService.FindByTvRageId(tvRageId); From 2a47a237d4e22d5dabcb181a76f5cdaba2b063c9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 17 Feb 2024 21:20:08 +0200 Subject: [PATCH 124/762] Fix typo in log message matching by TVRage ID --- src/NzbDrone.Core/Parser/ParsingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 5819704f7..a07cfaebc 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -453,7 +453,7 @@ namespace NzbDrone.Core.Parser if (series != null) { _logger.Debug() - .Message("Found matching series by TVRage ID {0}, an alias may be needed for: {1}", tvdbId, parsedEpisodeInfo.SeriesTitle) + .Message("Found matching series by TVRage ID {0}, an alias may be needed for: {1}", tvRageId, parsedEpisodeInfo.SeriesTitle) .Property("TvRageId", tvRageId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("TvRageIdMatch", tvRageId.ToString(), parsedEpisodeInfo.SeriesTitle) From c6071f6d81a968d3ed7c6bf4bae035961b54d128 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 17 Feb 2024 22:30:35 -0800 Subject: [PATCH 125/762] Upgrade node to 20.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f6a975c3..719159698 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "worker-loader": "3.0.8" }, "volta": { - "node": "16.17.0", + "node": "20.11.1", "yarn": "1.22.19" } } From 1a6f45bafd91c1f118e1c61c3d589a6bbba24694 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 19 Feb 2024 09:30:10 -0800 Subject: [PATCH 126/762] Upgrade actions/checkout to v4 --- .github/workflows/api_docs.yml | 2 +- .github/workflows/build.yml | 10 +++++----- .github/workflows/deploy.yml | 4 ++-- .github/workflows/publish-test-results.yml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/api_docs.yml b/.github/workflows/api_docs.yml index 1fc69c0fa..f133d3f23 100644 --- a/.github/workflows/api_docs.yml +++ b/.github/workflows/api_docs.yml @@ -26,7 +26,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup dotnet uses: actions/setup-dotnet@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e7e4c836..f91634c5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: version: ${{ steps.variables.outputs.version }} steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3 @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Volta uses: volta-cli/action@v4 @@ -149,7 +149,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test uses: ./.github/actions/test @@ -164,7 +164,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test uses: ./.github/actions/test @@ -199,7 +199,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test uses: ./.github/actions/test diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index af683e9d8..c477cd8b9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Package uses: ./.github/actions/package @@ -60,7 +60,7 @@ jobs: contents: write steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download release artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index 5e6012559..e5ca89a73 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download Test Reports uses: actions/download-artifact@v4 From a57254640f9750341ddb92d93a61fa13f053af87 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 19 Feb 2024 09:30:32 -0800 Subject: [PATCH 127/762] Upgrade actions/setup-dotnet to v4 --- .github/actions/test/action.yml | 2 +- .github/workflows/api_docs.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 65e928bb0..e7db4c018 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -27,7 +27,7 @@ runs: using: 'composite' steps: - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 - name: Setup Postgres if: ${{ inputs.use_postgres }} diff --git a/.github/workflows/api_docs.yml b/.github/workflows/api_docs.yml index f133d3f23..dfd8ce0e2 100644 --- a/.github/workflows/api_docs.yml +++ b/.github/workflows/api_docs.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 id: setup-dotnet - name: Create openapi.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f91634c5c..729e3ae4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 - name: Setup Environment Variables id: variables From 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 21 Feb 2024 06:12:45 +0200 Subject: [PATCH 128/762] New: Indexer flags Closes #2782 --- frontend/src/App/State/SettingsAppState.ts | 5 +- .../src/Components/Form/FormInputGroup.js | 5 ++ .../Form/IndexerFlagsSelectInput.tsx | 62 +++++++++++++++ frontend/src/Components/Page/PageConnector.js | 21 +++++- frontend/src/Episode/IndexerFlags.tsx | 26 +++++++ frontend/src/EpisodeFile/EpisodeFile.ts | 1 + frontend/src/Helpers/Props/icons.js | 2 + frontend/src/Helpers/Props/inputTypes.js | 1 + .../IndexerFlags/SelectIndexerFlagsModal.tsx | 34 +++++++++ .../SelectIndexerFlagsModalContent.css | 7 ++ .../SelectIndexerFlagsModalContent.css.d.ts | 7 ++ .../SelectIndexerFlagsModalContent.tsx | 75 +++++++++++++++++++ .../InteractiveImportModalContent.tsx | 57 +++++++++++++- .../Interactive/InteractiveImportRow.tsx | 63 +++++++++++++++- .../InteractiveImport/InteractiveImport.ts | 2 + .../InteractiveSearch/InteractiveSearch.js | 9 +++ .../InteractiveSearchRow.css | 3 +- .../InteractiveSearchRow.css.d.ts | 1 + .../InteractiveSearchRow.tsx | 16 +++- frontend/src/Series/Details/EpisodeRow.css | 6 ++ .../src/Series/Details/EpisodeRow.css.d.ts | 1 + frontend/src/Series/Details/EpisodeRow.js | 31 +++++++- .../src/Series/Details/EpisodeRowConnector.js | 2 +- .../Store/Actions/Settings/indexerFlags.js | 48 ++++++++++++ frontend/src/Store/Actions/episodeActions.js | 9 +++ .../src/Store/Actions/episodeFileActions.js | 3 + .../Store/Actions/interactiveImportActions.js | 1 + frontend/src/Store/Actions/settingsActions.js | 5 ++ .../Selectors/createIndexerFlagsSelector.ts | 9 +++ frontend/src/typings/IndexerFlag.ts | 6 ++ .../ImportApprovedEpisodesFixture.cs | 5 ++ src/NzbDrone.Core/Blocklisting/Blocklist.cs | 2 + .../Blocklisting/BlocklistService.cs | 33 ++++---- .../CustomFormatCalculationService.cs | 13 +++- .../CustomFormats/CustomFormatInput.cs | 1 + .../IndexerFlagSpecification.cs | 44 +++++++++++ .../Migration/202_add_indexer_flags.cs | 15 ++++ .../TrackedDownloadService.cs | 20 +++-- src/NzbDrone.Core/History/HistoryService.cs | 44 ++++++----- .../BroadcastheNet/BroadcastheNetParser.cs | 21 +++++- .../Indexers/FileList/FileListParser.cs | 24 +++++- .../Indexers/FileList/FileListTorrent.cs | 1 + .../Indexers/HDBits/HDBitsParser.cs | 21 +++++- .../Indexers/Torznab/TorznabRssParser.cs | 62 ++++++++++++++- src/NzbDrone.Core/Localization/Core/en.json | 6 ++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 20 +++++ .../EpisodeImport/Manual/ManualImportFile.cs | 1 + .../EpisodeImport/Manual/ManualImportItem.cs | 1 + .../Manual/ManualImportService.cs | 20 +++-- .../CustomScript/CustomScript.cs | 1 + .../Parser/Model/LocalEpisode.cs | 1 + src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 17 ++++- .../EpisodeFiles/EpisodeFileController.cs | 6 ++ .../EpisodeFiles/EpisodeFileResource.cs | 4 +- .../Indexers/IndexerFlagController.cs | 23 ++++++ .../Indexers/IndexerFlagResource.cs | 13 ++++ src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 3 + .../ManualImport/ManualImportController.cs | 3 +- .../ManualImportReprocessResource.cs | 1 + .../ManualImport/ManualImportResource.cs | 2 + 61 files changed, 876 insertions(+), 72 deletions(-) create mode 100644 frontend/src/Components/Form/IndexerFlagsSelectInput.tsx create mode 100644 frontend/src/Episode/IndexerFlags.tsx create mode 100644 frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx create mode 100644 frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css create mode 100644 frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts create mode 100644 frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx create mode 100644 frontend/src/Store/Actions/Settings/indexerFlags.js create mode 100644 frontend/src/Store/Selectors/createIndexerFlagsSelector.ts create mode 100644 frontend/src/typings/IndexerFlag.ts create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/202_add_indexer_flags.cs create mode 100644 src/Sonarr.Api.V3/Indexers/IndexerFlagController.cs create mode 100644 src/Sonarr.Api.V3/Indexers/IndexerFlagResource.cs diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index cb0c78ba8..a0bea0973 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -9,6 +9,7 @@ import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; +import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import { UiSettings } from 'typings/UiSettings'; @@ -40,19 +41,21 @@ export interface ImportListOptionsSettingsAppState extends AppSectionItemState<ImportListOptionsSettings>, AppSectionSaveState {} +export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>; export type LanguageSettingsAppState = AppSectionState<Language>; export type UiSettingsAppState = AppSectionItemState<UiSettings>; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; + indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; ui: UiSettingsAppState; - importListOptions: ImportListOptionsSettingsAppState; } export default SettingsAppState; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index d3b3eb206..f7b2ce75e 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -11,6 +11,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import FormInputHelpText from './FormInputHelpText'; +import IndexerFlagsSelectInput from './IndexerFlagsSelectInput'; import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput'; @@ -71,6 +72,9 @@ function getComponent(type) { case inputTypes.INDEXER_SELECT: return IndexerSelectInputConnector; + case inputTypes.INDEXER_FLAGS_SELECT: + return IndexerFlagsSelectInput; + case inputTypes.DOWNLOAD_CLIENT_SELECT: return DownloadClientSelectInputConnector; @@ -279,6 +283,7 @@ FormInputGroup.propTypes = { includeNoChange: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool, selectedValueOptions: PropTypes.object, + indexerFlags: PropTypes.number, pending: PropTypes.bool, errors: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object), diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx new file mode 100644 index 000000000..8dbd27a70 --- /dev/null +++ b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx @@ -0,0 +1,62 @@ +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +const selectIndexerFlagsValues = (selectedFlags: number) => + createSelector( + (state: AppState) => state.settings.indexerFlags, + (indexerFlags) => { + const value = indexerFlags.items.reduce((acc: number[], { id }) => { + // eslint-disable-next-line no-bitwise + if ((selectedFlags & id) === id) { + acc.push(id); + } + + return acc; + }, []); + + const values = indexerFlags.items.map(({ id, name }) => ({ + key: id, + value: name, + })); + + return { + value, + values, + }; + } + ); + +interface IndexerFlagsSelectInputProps { + name: string; + indexerFlags: number; + onChange(payload: object): void; +} + +function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) { + const { indexerFlags, onChange } = props; + + const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); + + const onChangeWrapper = useCallback( + ({ name, value }: { name: string; value: number[] }) => { + const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0); + + onChange({ name, value: indexerFlags }); + }, + [onChange] + ); + + return ( + <EnhancedSelectInput + {...props} + value={value} + values={values} + onChange={onChangeWrapper} + /> + ); +} + +export default IndexerFlagsSelectInput; diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 3aa82f31e..95416ea3c 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -6,7 +6,13 @@ import { createSelector } from 'reselect'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchSeries } from 'Store/Actions/seriesActions'; -import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; +import { + fetchImportLists, + fetchIndexerFlags, + fetchLanguages, + fetchQualityProfiles, + fetchUISettings +} from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -51,6 +57,7 @@ const selectIsPopulated = createSelector( (state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.languages.isPopulated, (state) => state.settings.importLists.isPopulated, + (state) => state.settings.indexerFlags.isPopulated, (state) => state.system.status.isPopulated, (state) => state.app.translations.isPopulated, ( @@ -61,6 +68,7 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated, languagesIsPopulated, importListsIsPopulated, + indexerFlagsIsPopulated, systemStatusIsPopulated, translationsIsPopulated ) => { @@ -72,6 +80,7 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated && languagesIsPopulated && importListsIsPopulated && + indexerFlagsIsPopulated && systemStatusIsPopulated && translationsIsPopulated ); @@ -86,6 +95,7 @@ const selectErrors = createSelector( (state) => state.settings.qualityProfiles.error, (state) => state.settings.languages.error, (state) => state.settings.importLists.error, + (state) => state.settings.indexerFlags.error, (state) => state.system.status.error, (state) => state.app.translations.error, ( @@ -96,6 +106,7 @@ const selectErrors = createSelector( qualityProfilesError, languagesError, importListsError, + indexerFlagsError, systemStatusError, translationsError ) => { @@ -107,6 +118,7 @@ const selectErrors = createSelector( qualityProfilesError || languagesError || importListsError || + indexerFlagsError || systemStatusError || translationsError ); @@ -120,6 +132,7 @@ const selectErrors = createSelector( qualityProfilesError, languagesError, importListsError, + indexerFlagsError, systemStatusError, translationsError }; @@ -174,6 +187,9 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchImportLists() { dispatch(fetchImportLists()); }, + dispatchFetchIndexerFlags() { + dispatch(fetchIndexerFlags()); + }, dispatchFetchUISettings() { dispatch(fetchUISettings()); }, @@ -213,6 +229,7 @@ class PageConnector extends Component { this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchLanguages(); this.props.dispatchFetchImportLists(); + this.props.dispatchFetchIndexerFlags(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); this.props.dispatchFetchTranslations(); @@ -238,6 +255,7 @@ class PageConnector extends Component { dispatchFetchQualityProfiles, dispatchFetchLanguages, dispatchFetchImportLists, + dispatchFetchIndexerFlags, dispatchFetchUISettings, dispatchFetchStatus, dispatchFetchTranslations, @@ -278,6 +296,7 @@ PageConnector.propTypes = { dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, + dispatchFetchIndexerFlags: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired, diff --git a/frontend/src/Episode/IndexerFlags.tsx b/frontend/src/Episode/IndexerFlags.tsx new file mode 100644 index 000000000..74e2e033c --- /dev/null +++ b/frontend/src/Episode/IndexerFlags.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector'; + +interface IndexerFlagsProps { + indexerFlags: number; +} + +function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) { + const allIndexerFlags = useSelector(createIndexerFlagsSelector); + + const flags = allIndexerFlags.items.filter( + // eslint-disable-next-line no-bitwise + (item) => (indexerFlags & item.id) === item.id + ); + + return flags.length ? ( + <ul> + {flags.map((flag, index) => { + return <li key={index}>{flag.name}</li>; + })} + </ul> + ) : null; +} + +export default IndexerFlags; diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts index a3ea2bed4..53dd53750 100644 --- a/frontend/src/EpisodeFile/EpisodeFile.ts +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -16,6 +16,7 @@ export interface EpisodeFile extends ModelBase { languages: Language[]; quality: QualityModel; customFormats: CustomFormat[]; + indexerFlags: number; mediaInfo: MediaInfo; qualityCutoffNotMet: boolean; } diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index ee3fea802..4fbd5914c 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -62,6 +62,7 @@ import { faFileExport as fasFileExport, faFileInvoice as farFileInvoice, faFilter as fasFilter, + faFlag as fasFlag, faFolderOpen as fasFolderOpen, faForward as fasForward, faHeart as fasHeart, @@ -154,6 +155,7 @@ export const FILE_MISSING = fasFileCircleQuestion; export const FILTER = fasFilter; export const FINALE_SEASON = fasCirclePause; export const FINALE_SERIES = fasCircleStop; +export const FLAG = fasFlag; export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 126b45954..dcf4b539c 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -12,6 +12,7 @@ export const PASSWORD = 'password'; export const PATH = 'path'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const INDEXER_SELECT = 'indexerSelect'; +export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const LANGUAGE_SELECT = 'languageSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx new file mode 100644 index 000000000..9136554cc --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent'; + +interface SelectIndexerFlagsModalProps { + isOpen: boolean; + indexerFlags: number; + modalTitle: string; + onIndexerFlagsSelect(indexerFlags: number): void; + onModalClose(): void; +} + +function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) { + const { + isOpen, + indexerFlags, + modalTitle, + onIndexerFlagsSelect, + onModalClose, + } = props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <SelectIndexerFlagsModalContent + indexerFlags={indexerFlags} + modalTitle={modalTitle} + onIndexerFlagsSelect={onIndexerFlagsSelect} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +export default SelectIndexerFlagsModal; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css new file mode 100644 index 000000000..72dfb1cb6 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css @@ -0,0 +1,7 @@ +.modalBody { + composes: modalBody from '~Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts new file mode 100644 index 000000000..3fc49a060 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'modalBody': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx new file mode 100644 index 000000000..f36f46602 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx @@ -0,0 +1,75 @@ +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, scrollDirections } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './SelectIndexerFlagsModalContent.css'; + +interface SelectIndexerFlagsModalContentProps { + indexerFlags: number; + modalTitle: string; + onIndexerFlagsSelect(indexerFlags: number): void; + onModalClose(): void; +} + +function SelectIndexerFlagsModalContent( + props: SelectIndexerFlagsModalContentProps +) { + const { modalTitle, onIndexerFlagsSelect, onModalClose } = props; + const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags); + + const onIndexerFlagsChange = useCallback( + ({ value }: { value: number }) => { + setIndexerFlags(value); + }, + [setIndexerFlags] + ); + + const onIndexerFlagsSelectWrapper = useCallback(() => { + onIndexerFlagsSelect(indexerFlags); + }, [indexerFlags, onIndexerFlagsSelect]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader> + {translate('SetIndexerFlagsModalTitle', { modalTitle })} + </ModalHeader> + + <ModalBody + className={styles.modalBody} + scrollDirection={scrollDirections.NONE} + > + <Form> + <FormGroup> + <FormLabel>{translate('IndexerFlags')}</FormLabel> + + <FormInputGroup + type={inputTypes.INDEXER_FLAGS_SELECT} + name="indexerFlags" + indexerFlags={indexerFlags} + autoFocus={true} + onChange={onIndexerFlagsChange} + /> + </FormGroup> + </Form> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}> + {translate('SetIndexerFlags')} + </Button> + </ModalFooter> + </ModalContent> + ); +} + +export default SelectIndexerFlagsModalContent; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index b778388a5..e421db602 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -29,6 +29,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import ImportMode from 'InteractiveImport/ImportMode'; +import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal'; import InteractiveImport, { InteractiveImportCommandOptions, } from 'InteractiveImport/InteractiveImport'; @@ -71,7 +72,8 @@ type SelectType = | 'episode' | 'releaseGroup' | 'quality' - | 'language'; + | 'language' + | 'indexerFlags'; type FilterExistingFiles = 'all' | 'new'; @@ -135,11 +137,21 @@ const COLUMNS = [ isSortable: true, isVisible: true, }, + { + name: 'indexerFlags', + label: React.createElement(Icon, { + name: icons.FLAG, + title: () => translate('IndexerFlags'), + }), + isSortable: true, + isVisible: true, + }, { name: 'rejections', label: React.createElement(Icon, { name: icons.DANGER, kind: kinds.DANGER, + title: () => translate('Rejections'), }), isSortable: true, isVisible: true, @@ -284,8 +296,18 @@ function InteractiveImportModalContent( } } + const showIndexerFlags = items.some((item) => item.indexerFlags); + + if (!showIndexerFlags) { + const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags'); + + if (indexerFlagsColumn) { + indexerFlagsColumn.isVisible = false; + } + } + return result; - }, [showSeries]); + }, [showSeries, items]); const selectedIds: number[] = useMemo(() => { return getSelectedIds(selectedState); @@ -343,6 +365,10 @@ function InteractiveImportModalContent( key: 'language', value: translate('SelectLanguage'), }, + { + key: 'indexerFlags', + value: translate('SelectIndexerFlags'), + }, ]; if (allowSeriesChange) { @@ -483,6 +509,7 @@ function InteractiveImportModalContent( releaseGroup, quality, languages, + indexerFlags, episodeFileId, } = item; @@ -532,6 +559,7 @@ function InteractiveImportModalContent( releaseGroup, quality, languages, + indexerFlags, }); return; @@ -546,6 +574,7 @@ function InteractiveImportModalContent( releaseGroup, quality, languages, + indexerFlags, downloadId, episodeFileId, }); @@ -742,6 +771,22 @@ function InteractiveImportModalContent( [selectedIds, dispatch] ); + const onIndexerFlagsSelect = useCallback( + (indexerFlags: number) => { + dispatch( + updateInteractiveImportItems({ + ids: selectedIds, + indexerFlags, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: selectedIds })); + + setSelectModalOpen(null); + }, + [selectedIds, dispatch] + ); + const orderedSelectedIds = items.reduce((acc: number[], file) => { if (selectedIds.includes(file.id)) { acc.push(file.id); @@ -947,6 +992,14 @@ function InteractiveImportModalContent( onModalClose={onSelectModalClose} /> + <SelectIndexerFlagsModal + isOpen={selectModalOpen === 'indexerFlags'} + indexerFlags={0} + modalTitle={modalTitle} + onIndexerFlagsSelect={onIndexerFlagsSelect} + onModalClose={onSelectModalClose} + /> + <ConfirmModal isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index ff81794bc..2f6f11af4 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -12,9 +12,11 @@ import Episode from 'Episode/Episode'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import IndexerFlags from 'Episode/IndexerFlags'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; +import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; @@ -41,7 +43,8 @@ type SelectType = | 'episode' | 'releaseGroup' | 'quality' - | 'language'; + | 'language' + | 'indexerFlags'; type SelectedChangeProps = SelectStateInputProps & { hasEpisodeFileId: boolean; @@ -60,6 +63,7 @@ interface InteractiveImportRowProps { size: number; customFormats?: object[]; customFormatScore?: number; + indexerFlags: number; rejections: Rejection[]; columns: Column[]; episodeFileId?: number; @@ -84,6 +88,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { size, customFormats, customFormatScore, + indexerFlags, rejections, isReprocessing, isSelected, @@ -100,6 +105,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { () => columns.find((c) => c.name === 'series')?.isVisible ?? false, [columns] ); + const isIndexerFlagsColumnVisible = useMemo( + () => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false, + [columns] + ); const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>( null @@ -306,6 +315,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { [id, dispatch, setSelectModalOpen, selectRowAfterChange] ); + const onSelectIndexerFlagsPress = useCallback(() => { + setSelectModalOpen('indexerFlags'); + }, [setSelectModalOpen]); + + const onIndexerFlagsSelect = useCallback( + (indexerFlags: number) => { + dispatch( + updateInteractiveImportItem({ + id, + indexerFlags, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: [id] })); + + setSelectModalOpen(null); + selectRowAfterChange(); + }, + [id, dispatch, setSelectModalOpen, selectRowAfterChange] + ); + const seriesTitle = series ? series.title : ''; const isAnime = series?.seriesType === 'anime'; @@ -332,6 +362,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; const showLanguagePlaceholder = isSelected && !languages; + const showIndexerFlagsPlaceholder = isSelected && !indexerFlags; return ( <TableRow> @@ -448,6 +479,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { ) : null} </TableRowCell> + {isIndexerFlagsColumnVisible ? ( + <TableRowCellButton + title={translate('ClickToChangeIndexerFlags')} + onPress={onSelectIndexerFlagsPress} + > + {showIndexerFlagsPlaceholder ? ( + <InteractiveImportRowCellPlaceholder isOptional={true} /> + ) : ( + <> + {indexerFlags ? ( + <Popover + anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />} + title={translate('IndexerFlags')} + body={<IndexerFlags indexerFlags={indexerFlags} />} + position={tooltipPositions.LEFT} + /> + ) : null} + </> + )} + </TableRowCellButton> + ) : null} + <TableRowCell> {rejections.length ? ( <Popover @@ -518,6 +571,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { onLanguagesSelect={onLanguagesSelect} onModalClose={onSelectModalClose} /> + + <SelectIndexerFlagsModal + isOpen={selectModalOpen === 'indexerFlags'} + indexerFlags={indexerFlags ?? 0} + modalTitle={modalTitle} + onIndexerFlagsSelect={onIndexerFlagsSelect} + onModalClose={onSelectModalClose} + /> </TableRow> ); } diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index b73aa6d4b..9ec91a4aa 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -13,6 +13,7 @@ export interface InteractiveImportCommandOptions { releaseGroup?: string; quality: QualityModel; languages: Language[]; + indexerFlags: number; downloadId?: string; episodeFileId?: number; } @@ -31,6 +32,7 @@ interface InteractiveImport extends ModelBase { episodes: Episode[]; qualityWeight: number; customFormats: object[]; + indexerFlags: number; rejections: Rejection[]; episodeFileId?: number; } diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index 1961de02c..bea804902 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -72,6 +72,15 @@ const columns = [ isSortable: true, isVisible: true }, + { + name: 'indexerFlags', + label: React.createElement(Icon, { + name: icons.FLAG, + title: () => translate('IndexerFlags') + }), + isSortable: true, + isVisible: true + }, { name: 'rejections', label: React.createElement(Icon, { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index a2f5883c8..03f454da2 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -44,7 +44,8 @@ cursor: default; } -.rejected { +.rejected, +.indexerFlags { composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 50px; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts index 0f32b14eb..fd4007966 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts @@ -6,6 +6,7 @@ interface CssExports { 'download': string; 'downloadIcon': string; 'indexer': string; + 'indexerFlags': string; 'interactiveIcon': string; 'languages': string; 'manualDownloadContent': string; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index 4f6295ef6..49b8d7823 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -12,6 +12,7 @@ import type DownloadProtocol from 'DownloadClient/DownloadProtocol'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import IndexerFlags from 'Episode/IndexerFlags'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; @@ -98,6 +99,7 @@ interface InteractiveSearchRowProps { mappedEpisodeNumbers?: number[]; mappedAbsoluteEpisodeNumbers?: number[]; mappedEpisodeInfo: ReleaseEpisode[]; + indexerFlags: number; rejections: string[]; episodeRequested: boolean; downloadAllowed: boolean; @@ -139,6 +141,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { mappedEpisodeNumbers, mappedAbsoluteEpisodeNumbers, mappedEpisodeInfo, + indexerFlags = 0, rejections = [], episodeRequested, downloadAllowed, @@ -254,10 +257,21 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { customFormats.length )} tooltip={<EpisodeFormats formats={customFormats} />} - position={tooltipPositions.BOTTOM} + position={tooltipPositions.LEFT} /> </TableRowCell> + <TableRowCell className={styles.indexerFlags}> + {indexerFlags ? ( + <Popover + anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />} + title={translate('IndexerFlags')} + body={<IndexerFlags indexerFlags={indexerFlags} />} + position={tooltipPositions.LEFT} + /> + ) : null} + </TableRowCell> + <TableRowCell className={styles.rejected}> {rejections.length ? ( <Popover diff --git a/frontend/src/Series/Details/EpisodeRow.css b/frontend/src/Series/Details/EpisodeRow.css index 4a0940362..086aa23fe 100644 --- a/frontend/src/Series/Details/EpisodeRow.css +++ b/frontend/src/Series/Details/EpisodeRow.css @@ -62,3 +62,9 @@ width: 55px; } + +.indexerFlags { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} diff --git a/frontend/src/Series/Details/EpisodeRow.css.d.ts b/frontend/src/Series/Details/EpisodeRow.css.d.ts index d4a5cfe93..42893ee69 100644 --- a/frontend/src/Series/Details/EpisodeRow.css.d.ts +++ b/frontend/src/Series/Details/EpisodeRow.css.d.ts @@ -6,6 +6,7 @@ interface CssExports { 'customFormatScore': string; 'episodeNumber': string; 'episodeNumberAnime': string; + 'indexerFlags': string; 'languages': string; 'monitored': string; 'releaseGroup': string; diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index 6539b9477..c7dbf04b9 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -1,22 +1,26 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Icon from 'Components/Icon'; import MonitorToggleButton from 'Components/MonitorToggleButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeNumber from 'Episode/EpisodeNumber'; import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import IndexerFlags from 'Episode/IndexerFlags'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; -import { tooltipPositions } from 'Helpers/Props'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import formatRuntime from 'Utilities/Number/formatRuntime'; +import translate from 'Utilities/String/translate'; import styles from './EpisodeRow.css'; class EpisodeRow extends Component { @@ -77,6 +81,7 @@ class EpisodeRow extends Component { releaseGroup, customFormats, customFormatScore, + indexerFlags, alternateTitles, columns } = this.props; @@ -211,7 +216,7 @@ class EpisodeRow extends Component { customFormats.length )} tooltip={<EpisodeFormats formats={customFormats} />} - position={tooltipPositions.BOTTOM} + position={tooltipPositions.LEFT} /> </TableRowCell> ); @@ -322,6 +327,24 @@ class EpisodeRow extends Component { ); } + if (name === 'indexerFlags') { + return ( + <TableRowCell + key={name} + className={styles.indexerFlags} + > + {indexerFlags ? ( + <Popover + anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />} + title={translate('IndexerFlags')} + body={<IndexerFlags indexerFlags={indexerFlags} />} + position={tooltipPositions.LEFT} + /> + ) : null} + </TableRowCell> + ); + } + if (name === 'status') { return ( <TableRowCell @@ -381,6 +404,7 @@ EpisodeRow.propTypes = { releaseGroup: PropTypes.string, customFormats: PropTypes.arrayOf(PropTypes.object), customFormatScore: PropTypes.number.isRequired, + indexerFlags: PropTypes.number.isRequired, mediaInfo: PropTypes.object, alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -389,7 +413,8 @@ EpisodeRow.propTypes = { EpisodeRow.defaultProps = { alternateTitles: [], - customFormats: [] + customFormats: [], + indexerFlags: 0 }; export default EpisodeRow; diff --git a/frontend/src/Series/Details/EpisodeRowConnector.js b/frontend/src/Series/Details/EpisodeRowConnector.js index 9559c289e..59c6818d4 100644 --- a/frontend/src/Series/Details/EpisodeRowConnector.js +++ b/frontend/src/Series/Details/EpisodeRowConnector.js @@ -1,4 +1,3 @@ -/* eslint max-params: 0 */ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; @@ -20,6 +19,7 @@ function createMapStateToProps() { releaseGroup: episodeFile ? episodeFile.releaseGroup : null, customFormats: episodeFile ? episodeFile.customFormats : [], customFormatScore: episodeFile ? episodeFile.customFormatScore : 0, + indexerFlags: episodeFile ? episodeFile.indexerFlags : 0, alternateTitles: series.alternateTitles }; } diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js new file mode 100644 index 000000000..a53fe1c61 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/indexerFlags.js @@ -0,0 +1,48 @@ +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import { createThunk } from 'Store/thunks'; + +// +// Variables + +const section = 'settings.indexerFlags'; + +// +// Actions Types + +export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags'; + +// +// Action Creators + +export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag') + }, + + // + // Reducers + + reducers: { + + } + +}; diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js index a769913a0..f875516fb 100644 --- a/frontend/src/Store/Actions/episodeActions.js +++ b/frontend/src/Store/Actions/episodeActions.js @@ -129,6 +129,15 @@ export const defaultState = { isVisible: false, isSortable: true }, + { + name: 'indexerFlags', + columnLabel: () => translate('IndexerFlags'), + label: React.createElement(Icon, { + name: icons.FLAG, + title: () => translate('IndexerFlags') + }), + isVisible: false + }, { name: 'status', label: () => translate('Status'), diff --git a/frontend/src/Store/Actions/episodeFileActions.js b/frontend/src/Store/Actions/episodeFileActions.js index 0d2804135..c483f770d 100644 --- a/frontend/src/Store/Actions/episodeFileActions.js +++ b/frontend/src/Store/Actions/episodeFileActions.js @@ -161,9 +161,12 @@ export const actionHandlers = handleThunks({ const episodeFile = data.find((f) => f.id === id); props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet; + props.customFormats = episodeFile.customFormats; + props.customFormatScore = episodeFile.customFormatScore; props.languages = file.languages; props.quality = file.quality; props.releaseGroup = file.releaseGroup; + props.indexerFlags = file.indexerFlags; return updateItem({ section, diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 789fa7464..ce6da8a21 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -162,6 +162,7 @@ export const actionHandlers = handleThunks({ quality: item.quality, languages: item.languages, releaseGroup: item.releaseGroup, + indexerFlags: item.indexerFlags, downloadId: item.downloadId }; }); diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 32ec41f8a..e7b5e40f6 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,4 +1,5 @@ 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'; @@ -37,6 +38,7 @@ export * from './Settings/general'; export * from './Settings/importListOptions'; export * from './Settings/importLists'; export * from './Settings/importListExclusions'; +export * from './Settings/indexerFlags'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languages'; @@ -72,6 +74,7 @@ export const defaultState = { importLists: importLists.defaultState, importListExclusions: importListExclusions.defaultState, importListOptions: importListOptions.defaultState, + indexerFlags: indexerFlags.defaultState, indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, languages: languages.defaultState, @@ -116,6 +119,7 @@ export const actionHandlers = handleThunks({ ...importLists.actionHandlers, ...importListExclusions.actionHandlers, ...importListOptions.actionHandlers, + ...indexerFlags.actionHandlers, ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...languages.actionHandlers, @@ -151,6 +155,7 @@ export const reducers = createHandleActions({ ...importLists.reducers, ...importListExclusions.reducers, ...importListOptions.reducers, + ...indexerFlags.reducers, ...indexerOptions.reducers, ...indexers.reducers, ...languages.reducers, diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts new file mode 100644 index 000000000..90587639c --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts @@ -0,0 +1,9 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +const createIndexerFlagsSelector = createSelector( + (state: AppState) => state.settings.indexerFlags, + (indexerFlags) => indexerFlags +); + +export default createIndexerFlagsSelector; diff --git a/frontend/src/typings/IndexerFlag.ts b/frontend/src/typings/IndexerFlag.ts new file mode 100644 index 000000000..2c7d97a73 --- /dev/null +++ b/frontend/src/typings/IndexerFlag.ts @@ -0,0 +1,6 @@ +interface IndexerFlag { + id: number; + name: string; +} + +export default IndexerFlag; diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index c3110c2d9..9020601ff 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; +using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.Events; @@ -66,6 +67,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>())) .Returns(new EpisodeFileMoveResult()); + Mocker.GetMock<IHistoryService>() + .Setup(x => x.FindByDownloadId(It.IsAny<string>())) + .Returns(new List<EpisodeHistory>()); + _downloadClientItem = Builder<DownloadClientItem>.CreateNew() .With(d => d.OutputPath = new OsPath(outputPath)) .Build(); diff --git a/src/NzbDrone.Core/Blocklisting/Blocklist.cs b/src/NzbDrone.Core/Blocklisting/Blocklist.cs index 5d90a9514..4fdc4f24c 100644 --- a/src/NzbDrone.Core/Blocklisting/Blocklist.cs +++ b/src/NzbDrone.Core/Blocklisting/Blocklist.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -20,6 +21,7 @@ namespace NzbDrone.Core.Blocklisting public long? Size { get; set; } public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } + public IndexerFlags IndexerFlags { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } public List<Language> Languages { get; set; } diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 7da8f2a53..0015f7ea2 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -174,20 +174,25 @@ namespace NzbDrone.Core.Blocklisting public void Handle(DownloadFailedEvent message) { var blocklist = new Blocklist - { - SeriesId = message.SeriesId, - EpisodeIds = message.EpisodeIds, - SourceTitle = message.SourceTitle, - Quality = message.Quality, - Date = DateTime.UtcNow, - PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")), - Size = long.Parse(message.Data.GetValueOrDefault("size", "0")), - Indexer = message.Data.GetValueOrDefault("indexer"), - Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), - Message = message.Message, - TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), - Languages = message.Languages - }; + { + SeriesId = message.SeriesId, + EpisodeIds = message.EpisodeIds, + SourceTitle = message.SourceTitle, + Quality = message.Quality, + Date = DateTime.UtcNow, + PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")), + Size = long.Parse(message.Data.GetValueOrDefault("size", "0")), + Indexer = message.Data.GetValueOrDefault("indexer"), + Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), + Message = message.Message, + TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), + Languages = message.Languages + }; + + if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + blocklist.IndexerFlags = flags; + } _blocklistRepository.Insert(blocklist); } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index 1840d087c..c07db977e 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -39,7 +40,8 @@ namespace NzbDrone.Core.CustomFormats EpisodeInfo = remoteEpisode.ParsedEpisodeInfo, Series = remoteEpisode.Series, Size = size, - Languages = remoteEpisode.Languages + Languages = remoteEpisode.Languages, + IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0 }; return ParseCustomFormat(input); @@ -73,7 +75,8 @@ namespace NzbDrone.Core.CustomFormats EpisodeInfo = episodeInfo, Series = series, Size = blocklist.Size ?? 0, - Languages = blocklist.Languages + Languages = blocklist.Languages, + IndexerFlags = blocklist.IndexerFlags }; return ParseCustomFormat(input); @@ -84,6 +87,7 @@ namespace NzbDrone.Core.CustomFormats var parsed = Parser.Parser.ParseTitle(history.SourceTitle); long.TryParse(history.Data.GetValueOrDefault("size"), out var size); + Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags); var episodeInfo = new ParsedEpisodeInfo { @@ -99,7 +103,8 @@ namespace NzbDrone.Core.CustomFormats EpisodeInfo = episodeInfo, Series = series, Size = size, - Languages = history.Languages + Languages = history.Languages, + IndexerFlags = indexerFlags }; return ParseCustomFormat(input); @@ -122,6 +127,7 @@ namespace NzbDrone.Core.CustomFormats Series = localEpisode.Series, Size = localEpisode.Size, Languages = localEpisode.Languages, + IndexerFlags = localEpisode.IndexerFlags, Filename = Path.GetFileName(localEpisode.Path) }; @@ -191,6 +197,7 @@ namespace NzbDrone.Core.CustomFormats Series = series, Size = episodeFile.Size, Languages = episodeFile.Languages, + IndexerFlags = episodeFile.IndexerFlags, Filename = Path.GetFileName(episodeFile.RelativePath) }; diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs index ab035213a..e202ffccf 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.CustomFormats public ParsedEpisodeInfo EpisodeInfo { get; set; } public Series Series { get; set; } public long Size { get; set; } + public IndexerFlags IndexerFlags { get; set; } public List<Language> Languages { get; set; } public string Filename { get; set; } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs new file mode 100644 index 000000000..56f73f8b9 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -0,0 +1,44 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class IndexerFlagSpecificationValidator : AbstractValidator<IndexerFlagSpecification> + { + public IndexerFlagSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + RuleFor(c => c.Value).Custom((qualityValue, context) => + { + if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue)) + { + context.AddFailure($"Invalid indexer flag condition value: {qualityValue}"); + } + }); + } + } + + public class IndexerFlagSpecification : CustomFormatSpecificationBase + { + private static readonly IndexerFlagSpecificationValidator Validator = new (); + + public override int Order => 4; + public override string ImplementationName => "Indexer Flag"; + + [FieldDefinition(1, Label = "CustomFormatsSpecificationFlag", Type = FieldType.Select, SelectOptions = typeof(IndexerFlags))] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + return input.IndexerFlags.HasFlag((IndexerFlags)Value); + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/202_add_indexer_flags.cs b/src/NzbDrone.Core/Datastore/Migration/202_add_indexer_flags.cs new file mode 100644 index 000000000..b776db357 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/202_add_indexer_flags.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(202)] + public class add_indexer_flags : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blocklist").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0); + Alter.Table("EpisodeFiles").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index bab0a35f9..1c06d369c 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Download.History; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -109,10 +110,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads try { - var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId) - .OrderByDescending(h => h.Date) - .ToList(); + .OrderByDescending(h => h.Date) + .ToList(); + + var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); if (parsedEpisodeInfo != null) { @@ -134,12 +136,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.First(); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EpisodeHistoryEventType.Grabbed); - trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer"); trackedDownload.Added = grabbedEvent?.Date; if (parsedEpisodeInfo == null || - trackedDownload.RemoteEpisode == null || - trackedDownload.RemoteEpisode.Series == null || + trackedDownload.RemoteEpisode?.Series == null || trackedDownload.RemoteEpisode.Episodes.Empty()) { // Try parsing the original source title and if that fails, try parsing it as a special @@ -155,6 +156,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads .Select(h => h.EpisodeId).Distinct()); } } + + if (trackedDownload.RemoteEpisode != null && + Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + trackedDownload.RemoteEpisode.Release ??= new ReleaseInfo(); + trackedDownload.RemoteEpisode.Release.IndexerFlags = flags; + } } // Calculate custom formats diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index b893e0959..df2788762 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -169,6 +169,7 @@ namespace NzbDrone.Core.History history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString()); history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString()); history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString()); + history.Data.Add("IndexerFlags", message.Episode.Release.IndexerFlags.ToString()); if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) { @@ -201,16 +202,16 @@ namespace NzbDrone.Core.History foreach (var episode in message.EpisodeInfo.Episodes) { var history = new EpisodeHistory - { - EventType = EpisodeHistoryEventType.DownloadFolderImported, - Date = DateTime.UtcNow, - Quality = message.EpisodeInfo.Quality, - SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), - SeriesId = message.ImportedEpisode.SeriesId, - EpisodeId = episode.Id, - DownloadId = downloadId, - Languages = message.EpisodeInfo.Languages - }; + { + EventType = EpisodeHistoryEventType.DownloadFolderImported, + Date = DateTime.UtcNow, + Quality = message.EpisodeInfo.Quality, + SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), + SeriesId = message.ImportedEpisode.SeriesId, + EpisodeId = episode.Id, + DownloadId = downloadId, + Languages = message.EpisodeInfo.Languages + }; history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); history.Data.Add("DroppedPath", message.EpisodeInfo.Path); @@ -220,6 +221,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseGroup", message.EpisodeInfo.ReleaseGroup); history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString()); history.Data.Add("Size", message.EpisodeInfo.Size.ToString()); + history.Data.Add("IndexerFlags", message.ImportedEpisode.IndexerFlags.ToString()); _historyRepository.Insert(history); } @@ -280,6 +282,7 @@ namespace NzbDrone.Core.History history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); + history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); _historyRepository.Insert(history); } @@ -311,6 +314,7 @@ namespace NzbDrone.Core.History history.Data.Add("RelativePath", relativePath); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); + history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); _historyRepository.Insert(history); } @@ -323,16 +327,16 @@ namespace NzbDrone.Core.History foreach (var episodeId in message.EpisodeIds) { var history = new EpisodeHistory - { - EventType = EpisodeHistoryEventType.DownloadIgnored, - Date = DateTime.UtcNow, - Quality = message.Quality, - SourceTitle = message.SourceTitle, - SeriesId = message.SeriesId, - EpisodeId = episodeId, - DownloadId = message.DownloadId, - Languages = message.Languages - }; + { + EventType = EpisodeHistoryEventType.DownloadIgnored, + Date = DateTime.UtcNow, + Quality = message.Quality, + SourceTitle = message.SourceTitle, + SeriesId = message.SeriesId, + EpisodeId = episodeId, + DownloadId = message.DownloadId, + Languages = message.Languages + }; history.Data.Add("DownloadClient", message.DownloadClientInfo.Type); history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name); diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs index 0849d9625..15341e067 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs @@ -81,7 +81,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet Source = torrent.Source, Container = torrent.Container, Codec = torrent.Codec, - Resolution = torrent.Resolution + Resolution = torrent.Resolution, + IndexerFlags = GetIndexerFlags(torrent) }; if (torrent.TvdbID is > 0) @@ -100,6 +101,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet return results; } + private static IndexerFlags GetIndexerFlags(BroadcastheNetTorrent item) + { + IndexerFlags flags = 0; + flags |= IndexerFlags.Freeleech; + + switch (item.Origin.ToUpperInvariant()) + { + case "INTERNAL": + flags |= IndexerFlags.Internal; + break; + case "SCENE": + flags |= IndexerFlags.Scene; + break; + } + + return flags; + } + private string CleanReleaseName(string releaseName) { return releaseName.Replace("\\", ""); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index 2d0a16a8f..59e6237d0 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -38,9 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList { var id = result.Id; - // if (result.FreeLeech) - - torrentInfos.Add(new TorrentInfo() + torrentInfos.Add(new TorrentInfo { Guid = $"FileList-{id}", Title = result.Name, @@ -50,13 +48,31 @@ namespace NzbDrone.Core.Indexers.FileList Seeders = result.Seeders, Peers = result.Leechers + result.Seeders, PublishDate = result.UploadDate.ToUniversalTime(), - ImdbId = result.ImdbId + ImdbId = result.ImdbId, + IndexerFlags = GetIndexerFlags(result) }); } return torrentInfos.ToArray(); } + private static IndexerFlags GetIndexerFlags(FileListTorrent item) + { + IndexerFlags flags = 0; + + if (item.FreeLeech) + { + flags |= IndexerFlags.Freeleech; + } + + if (item.Internal) + { + flags |= IndexerFlags.Internal; + } + + return flags; + } + private string GetDownloadUrl(string torrentId) { var url = new HttpUri(_settings.BaseUrl) diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs b/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs index 01ea834ed..a22fc1c9b 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers.FileList public uint Files { get; set; } [JsonProperty(PropertyName = "imdb")] public string ImdbId { get; set; } + public bool Internal { get; set; } [JsonProperty(PropertyName = "freeleech")] public bool FreeLeech { get; set; } [JsonProperty(PropertyName = "upload_date")] diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index 4bff86c7c..2a0d0f352 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -49,6 +49,7 @@ namespace NzbDrone.Core.Indexers.HDBits foreach (var result in queryResults) { var id = result.Id; + torrentInfos.Add(new TorrentInfo { Guid = $"HDBits-{id}", @@ -59,13 +60,31 @@ namespace NzbDrone.Core.Indexers.HDBits InfoUrl = GetInfoUrl(id), Seeders = result.Seeders, Peers = result.Leechers + result.Seeders, - PublishDate = result.Added.ToUniversalTime() + PublishDate = result.Added.ToUniversalTime(), + IndexerFlags = GetIndexerFlags(result) }); } return torrentInfos.ToArray(); } + private static IndexerFlags GetIndexerFlags(TorrentQueryResponse item) + { + IndexerFlags flags = 0; + + if (item.FreeLeech == "yes") + { + flags |= IndexerFlags.Freeleech; + } + + if (item.TypeOrigin == 1) + { + flags |= IndexerFlags.Internal; + } + + return flags; + } + private string GetDownloadUrl(string torrentId) { var url = new HttpUri(_settings.BaseUrl) diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 7b9420c46..94a44828b 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -78,8 +78,12 @@ namespace NzbDrone.Core.Indexers.Torznab { var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; - torrentInfo.TvdbId = GetTvdbId(item); - torrentInfo.TvRageId = GetTvRageId(item); + if (torrentInfo != null) + { + torrentInfo.TvdbId = GetTvdbId(item); + torrentInfo.TvRageId = GetTvRageId(item); + torrentInfo.IndexerFlags = GetFlags(item); + } return torrentInfo; } @@ -214,6 +218,53 @@ namespace NzbDrone.Core.Indexers.Torznab return base.GetPeers(item); } + protected IndexerFlags GetFlags(XElement item) + { + IndexerFlags flags = 0; + + var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1); + var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1); + + if (downloadFactor == 0.5) + { + flags |= IndexerFlags.Halfleech; + } + + if (downloadFactor == 0.75) + { + flags |= IndexerFlags.Freeleech25; + } + + if (downloadFactor == 0.25) + { + flags |= IndexerFlags.Freeleech75; + } + + if (downloadFactor == 0.0) + { + flags |= IndexerFlags.Freeleech; + } + + if (uploadFactor == 2.0) + { + flags |= IndexerFlags.DoubleUpload; + } + + var tags = TryGetMultipleTorznabAttributes(item, "tag"); + + if (tags.Any(t => t.EqualsIgnoreCase("internal"))) + { + flags |= IndexerFlags.Internal; + } + + if (tags.Any(t => t.EqualsIgnoreCase("scene"))) + { + flags |= IndexerFlags.Scene; + } + + return flags; + } + protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "") { var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); @@ -229,6 +280,13 @@ namespace NzbDrone.Core.Indexers.Torznab return defaultValue; } + protected float TryGetFloatTorznabAttribute(XElement item, string key, float defaultValue = 0) + { + var attr = TryGetTorznabAttribute(item, key, defaultValue.ToString()); + + return float.TryParse(attr, out var result) ? result : defaultValue; + } + protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key) { var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 74feb6723..2f4a3bbb2 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -214,6 +214,7 @@ "ClearBlocklist": "Clear blocklist", "ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?", "ClickToChangeEpisode": "Click to change episode", + "ClickToChangeIndexerFlags": "Click to change indexer flags", "ClickToChangeLanguage": "Click to change language", "ClickToChangeQuality": "Click to change quality", "ClickToChangeReleaseGroup": "Click to change release group", @@ -276,6 +277,7 @@ "CustomFormatsLoadError": "Unable to load Custom Formats", "CustomFormatsSettings": "Custom Formats Settings", "CustomFormatsSettingsSummary": "Custom Formats and Settings", + "CustomFormatsSpecificationFlag": "Flag", "CustomFormatsSpecificationLanguage": "Language", "CustomFormatsSpecificationMaximumSize": "Maximum Size", "CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size", @@ -916,6 +918,7 @@ "Indexer": "Indexer", "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.", "IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer", + "IndexerFlags": "Indexer Flags", "IndexerHDBitsSettingsCategories": "Categories", "IndexerHDBitsSettingsCategoriesHelpText": "If unspecified, all options are used.", "IndexerHDBitsSettingsCodecs": "Codecs", @@ -1749,6 +1752,7 @@ "SelectEpisodesModalTitle": "{modalTitle} - Select Episode(s)", "SelectFolder": "Select Folder", "SelectFolderModalTitle": "{modalTitle} - Select Folder", + "SelectIndexerFlags": "Select Indexer Flags", "SelectLanguage": "Select Language", "SelectLanguageModalTitle": "{modalTitle} - Select Language", "SelectLanguages": "Select Languages", @@ -1790,6 +1794,8 @@ "SeriesType": "Series Type", "SeriesTypes": "Series Types", "SeriesTypesHelpText": "Series type is used for renaming, parsing and searching", + "SetIndexerFlags": "Set Indexer Flags", + "SetIndexerFlagsModalTitle": "{modalTitle} - Set Indexer Flags", "SetPermissions": "Set Permissions", "SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?", "SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index df77d28d0..a02fdd44a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -21,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles public string SceneName { get; set; } public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } + public IndexerFlags IndexerFlags { get; set; } public MediaInfoModel MediaInfo { get; set; } public LazyLoaded<List<Episode>> Episodes { get; set; } public LazyLoaded<Series> Series { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 308a210df..e4650746d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Extras; +using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Commands; @@ -28,6 +29,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IExtraService _extraService; private readonly IExistingExtraFiles _existingExtraFiles; private readonly IDiskProvider _diskProvider; + private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; private readonly Logger _logger; @@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport IExtraService extraService, IExistingExtraFiles existingExtraFiles, IDiskProvider diskProvider, + IHistoryService historyService, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, Logger logger) @@ -46,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _extraService = extraService; _existingExtraFiles = existingExtraFiles; _diskProvider = diskProvider; + _historyService = historyService; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; _logger = logger; @@ -93,6 +97,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; episodeFile.Languages = localEpisode.Languages; + if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true) + { + var grabHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed); + + if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + episodeFile.IndexerFlags = flags; + } + } + else + { + episodeFile.IndexerFlags = localEpisode.IndexerFlags; + } + bool copyOnly; switch (importMode) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index a2ec3ef35..f4daed6e8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public QualityModel Quality { get; set; } public List<Language> Languages { get; set; } public string ReleaseGroup { get; set; } + public int IndexerFlags { get; set; } public string DownloadId { get; set; } public bool Equals(ManualImportFile other) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index a7fc461b8..703428535 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public string DownloadId { get; set; } public List<CustomFormat> CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable<Rejection> Rejections { get; set; } public ManualImportItem() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index bd5393d4a..c327a1418 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<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags); } public class ManualImportService : IExecute<ManualImportCommand>, 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<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages) + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags) { var rootFolder = Path.GetDirectoryName(path); var series = _seriesService.GetSeries(seriesId); @@ -171,6 +171,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; 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); } @@ -197,7 +198,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual Size = _diskProvider.GetFileSize(path), 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 + Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality, + IndexerFlags = (IndexerFlags)indexerFlags }; return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); @@ -422,6 +424,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Languages = decision.LocalEpisode.Languages; item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); item.Rejections = decision.Rejections; + item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags; return item; } @@ -440,6 +443,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.ReleaseGroup = episodeFile.ReleaseGroup; item.Quality = episodeFile.Quality; item.Languages = episodeFile.Languages; + item.IndexerFlags = (int)episodeFile.IndexerFlags; item.Size = _diskProvider.GetFileSize(item.Path); item.Rejections = Enumerable.Empty<Rejection>(); item.EpisodeFileId = episodeFile.Id; @@ -476,6 +480,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual ReleaseGroup = file.ReleaseGroup, Quality = file.Quality, Languages = file.Languages, + IndexerFlags = (IndexerFlags)file.IndexerFlags, Series = series, Size = 0 }; @@ -504,6 +509,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.ReleaseGroup = file.ReleaseGroup; localEpisode.Quality = file.Quality; localEpisode.Languages = file.Languages; + localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags; // TODO: Cleanup non-tracked downloads @@ -520,10 +526,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual imported.Add(importResult); importedTrackedDownload.Add(new ManuallyImportedFile - { - TrackedDownload = trackedDownload, - ImportResult = importResult - }); + { + TrackedDownload = trackedDownload, + ImportResult = importResult + }); } } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index d72f668e3..a0902c1fc 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -89,6 +89,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Release_Quality", remoteEpisode.ParsedEpisodeInfo.Quality.Quality.Name); environmentVariables.Add("Sonarr_Release_QualityVersion", remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version.ToString()); environmentVariables.Add("Sonarr_Release_ReleaseGroup", releaseGroup ?? string.Empty); + environmentVariables.Add("Sonarr_Release_IndexerFlags", remoteEpisode.Release.IndexerFlags.ToString()); environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientName ?? string.Empty); environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 0c4212d61..2ca924779 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Parser.Model public List<DeletedEpisodeFile> OldFiles { get; set; } public QualityModel Quality { get; set; } public List<Language> Languages { get; set; } + public IndexerFlags IndexerFlags { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } public bool SceneSource { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 8d37deea6..ade3e3467 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Text; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; @@ -39,6 +39,9 @@ namespace NzbDrone.Core.Parser.Model public List<Language> Languages { get; set; } + [JsonIgnore] + public IndexerFlags IndexerFlags { get; set; } + // Used to track pending releases that are being reprocessed [JsonIgnore] public PendingReleaseReason? PendingReleaseReason { get; set; } @@ -107,4 +110,16 @@ namespace NzbDrone.Core.Parser.Model } } } + + [Flags] + public enum IndexerFlags + { + Freeleech = 1, // General + Halfleech = 2, // General, only 1/2 of download counted + DoubleUpload = 4, // General + Internal = 8, // General, uploader is an internal release group + Scene = 16, // General, the torrent comes from a "scene" group + Freeleech75 = 32, // Signifies a torrent counts towards 75 percent of your download quota. + Freeleech25 = 64, // Signifies a torrent counts towards 25 percent of your download quota. + } } diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs index 29230376a..a2bdbbe41 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Http; @@ -203,6 +204,11 @@ namespace Sonarr.Api.V3.EpisodeFiles { episodeFile.ReleaseGroup = resourceEpisodeFile.ReleaseGroup; } + + if (resourceEpisodeFile.IndexerFlags.HasValue) + { + episodeFile.IndexerFlags = (IndexerFlags)resourceEpisodeFile.IndexerFlags; + } } _mediaFileService.Update(episodeFiles); diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index 4d07eb117..0b6adcdc1 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.EpisodeFiles public QualityModel Quality { get; set; } public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int? IndexerFlags { get; set; } public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -88,7 +89,8 @@ namespace Sonarr.Api.V3.EpisodeFiles MediaInfo = model.MediaInfo.ToResource(model.SceneName), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality), CustomFormats = customFormats.ToResource(false), - CustomFormatScore = customFormatScore + CustomFormatScore = customFormatScore, + IndexerFlags = (int)model.IndexerFlags }; } } diff --git a/src/Sonarr.Api.V3/Indexers/IndexerFlagController.cs b/src/Sonarr.Api.V3/Indexers/IndexerFlagController.cs new file mode 100644 index 000000000..c2694a957 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerFlagController.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Parser.Model; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Indexers +{ + [V3ApiController] + public class IndexerFlagController : Controller + { + [HttpGet] + public List<IndexerFlagResource> GetAll() + { + return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource + { + Id = (int)f, + Name = f.ToString() + }).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/IndexerFlagResource.cs b/src/Sonarr.Api.V3/Indexers/IndexerFlagResource.cs new file mode 100644 index 000000000..84214d5bb --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerFlagResource.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Indexers +{ + public class IndexerFlagResource : RestResource + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public new int Id { get; set; } + public string Name { get; set; } + public string NameLower => Name.ToLowerInvariant(); + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index a15a1b234..bc4b5982d 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -65,6 +65,7 @@ namespace Sonarr.Api.V3.Indexers public int? Seeders { get; set; } public int? Leechers { get; set; } public DownloadProtocol Protocol { get; set; } + public int IndexerFlags { get; set; } public bool IsDaily { get; set; } public bool IsAbsoluteNumbering { get; set; } @@ -100,6 +101,7 @@ namespace Sonarr.Api.V3.Indexers var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; var remoteEpisode = model.RemoteEpisode; var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); + var indexerFlags = torrentInfo.IndexerFlags; // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) return new ReleaseResource @@ -152,6 +154,7 @@ namespace Sonarr.Api.V3.Indexers Seeders = torrentInfo.Seeders, Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, Protocol = releaseInfo.DownloadProtocol, + IndexerFlags = (int)indexerFlags, IsDaily = parsedEpisodeInfo.IsDaily, IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering, diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index 65b7f3bf1..f537f2e2f 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); + 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.SeasonNumber = processedItem.SeasonNumber; item.Episodes = processedItem.Episodes.ToResource(); + item.IndexerFlags = processedItem.IndexerFlags; item.Rejections = processedItem.Rejections; item.CustomFormats = processedItem.CustomFormats.ToResource(false); item.CustomFormatScore = processedItem.CustomFormatScore; diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 744f6ce51..66bb78ba9 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.ManualImport public string DownloadId { get; set; } public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable<Rejection> Rejections { get; set; } } } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index 590a66333..0e47dcd60 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -30,6 +30,7 @@ namespace Sonarr.Api.V3.ManualImport public string DownloadId { get; set; } public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable<Rejection> Rejections { get; set; } } @@ -65,6 +66,7 @@ namespace Sonarr.Api.V3.ManualImport // QualityWeight DownloadId = model.DownloadId, + IndexerFlags = model.IndexerFlags, Rejections = model.Rejections }; } From 0242b40eda2dc0b257793e5a136f46dfba805438 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 18 Feb 2024 20:32:43 -0800 Subject: [PATCH 129/762] Use GitHubActionsTestLogger for test reporting --- .github/actions/test/action.yml | 11 +---- .github/workflows/publish-test-results.yml | 41 ------------------- src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj | 1 + .../Sonarr.Automation.Test.csproj | 1 + .../Sonarr.Common.Test.csproj | 3 ++ .../Sonarr.Core.Test.csproj | 1 + .../Sonarr.Host.Test.csproj | 3 ++ .../Sonarr.Integration.Test.csproj | 1 + .../Sonarr.Libraries.Test.csproj | 3 ++ .../Sonarr.Mono.Test.csproj | 1 + .../Sonarr.Update.Test.csproj | 3 ++ .../Sonarr.Windows.Test.csproj | 3 ++ 12 files changed, 21 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/publish-test-results.yml diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index e7db4c018..bd62f4830 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -77,7 +77,7 @@ runs: - name: Run tests shell: bash - run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" + run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" - name: Upload Test Results if: ${{ !cancelled() }} @@ -85,12 +85,3 @@ runs: with: name: results-${{ env.RESULTS_NAME }} path: TestResults/*.trx - - - name: Publish Test Results - uses: phoenix-actions/test-reporting@v12 - with: - name: Test Results - output-to: step-summary - path: '*.trx' - reporter: dotnet-trx - working-directory: TestResults diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml deleted file mode 100644 index e5ca89a73..000000000 --- a/.github/workflows/publish-test-results.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Publish Test Results - -on: - workflow_run: - workflows: ['Build'] - types: - - completed - -permissions: - contents: read - actions: read - checks: write - -jobs: - report: - if: ${{ github.event.workflow_run.conclusion != 'skipped' && github.event.workflow_run.conclusion != 'cancelled' }} - runs-on: ubuntu-latest - steps: - - name: Check out - uses: actions/checkout@v4 - - - name: Download Test Reports - uses: actions/download-artifact@v4 - with: - path: test-results - pattern: results-* - merge-multiple: true - repository: ${{ github.event.repository.owner.login }}/${{ github.event.repository.name }} - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish Test Results - uses: phoenix-actions/test-reporting@v12 - with: - list-suites: failed - list-tests: failed - name: Test Results - only-summary: true - path: '*.trx' - reporter: dotnet-trx - working-directory: test-results diff --git a/src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj b/src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj index b7755139a..053b42cc9 100644 --- a/src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj @@ -3,6 +3,7 @@ <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> <PackageReference Include="NBuilder" Version="6.1.0" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj b/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj index 106e06954..9cf833789 100644 --- a/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj +++ b/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj @@ -3,6 +3,7 @@ <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> <PackageReference Include="Selenium.Support" Version="3.141.0" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="111.0.5563.6400" /> </ItemGroup> diff --git a/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj b/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj index 0c42e8606..402cae2bf 100644 --- a/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj @@ -2,6 +2,9 @@ <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> + <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> diff --git a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj index 4d510b27e..6a1477da4 100644 --- a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj @@ -4,6 +4,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Dapper" Version="2.0.123" /> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> <PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> </ItemGroup> diff --git a/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj b/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj index de5add8a7..00783be18 100644 --- a/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj +++ b/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj @@ -2,6 +2,9 @@ <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> + <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> diff --git a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj index 79c596097..b78253880 100644 --- a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj @@ -4,6 +4,7 @@ <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.21" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj b/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj index 2c8b3456a..511c54cba 100644 --- a/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj +++ b/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj @@ -2,6 +2,9 @@ <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> + <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> </ItemGroup> diff --git a/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj b/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj index 2660db2e4..60c6f901c 100644 --- a/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj +++ b/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj @@ -7,6 +7,7 @@ See https://github.com/xamarin/XamarinComponents/issues/282 --> <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr24" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj b/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj index c56dc9b98..5b4d7dfd7 100644 --- a/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj +++ b/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj @@ -2,6 +2,9 @@ <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> + <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Update\Sonarr.Update.csproj" /> diff --git a/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj b/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj index 522bcf4fc..15bcf5d51 100644 --- a/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj +++ b/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj @@ -2,6 +2,9 @@ <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> + <ItemGroup> + <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common.Test\Sonarr.Common.Test.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> From f10ccf587d07cf47f822e4c1f8892f3bf3580018 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 19 Feb 2024 11:42:54 -0800 Subject: [PATCH 130/762] Don't fail fast for integration tests --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 729e3ae4b..47dff9073 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -178,6 +178,7 @@ jobs: integration_test: needs: backend strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] include: From 2f041f9ec19555495598bfe38f37088edade55ec Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Wed, 21 Feb 2024 04:15:15 +0000 Subject: [PATCH 131/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e69dccae3..e6e89c035 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -3587,6 +3587,44 @@ } } }, + "/api/v3/indexerflag": { + "get": { + "tags": [ + "IndexerFlag" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + } + } + } + } + } + }, "/api/v3/language": { "get": { "tags": [ @@ -8177,6 +8215,11 @@ "type": "integer", "format": "int32" }, + "indexerFlags": { + "type": "integer", + "format": "int32", + "nullable": true + }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" }, @@ -9012,6 +9055,25 @@ }, "additionalProperties": false }, + "IndexerFlagResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "nameLower": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, "IndexerResource": { "type": "object", "properties": { @@ -9371,6 +9433,10 @@ "type": "integer", "format": "int32" }, + "indexerFlags": { + "type": "integer", + "format": "int32" + }, "rejections": { "type": "array", "items": { @@ -9461,6 +9527,10 @@ "type": "integer", "format": "int32" }, + "indexerFlags": { + "type": "integer", + "format": "int32" + }, "rejections": { "type": "array", "items": { @@ -10843,6 +10913,10 @@ "protocol": { "$ref": "#/components/schemas/DownloadProtocol" }, + "indexerFlags": { + "type": "integer", + "format": "int32" + }, "isDaily": { "type": "boolean" }, From e1be3b20e9dd1c146f1b3cfdf4f2544e33eb151b Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 21 Feb 2024 04:12:49 +0000 Subject: [PATCH 132/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: 闫锦彪 <yanjinbiaohere@163.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index f9e51f2af..854c4a475 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -761,7 +761,7 @@ "HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查[日志]({link})来查找有关这些运行状况检查消息原因的更多信息。如果你在理解这些信息方面有困难,你可以通过下面的链接联系我们的支持。", "StandardEpisodeFormat": "标准单集格式", "DefaultNameCopiedSpecification": "{name} - 复制", - "AddListExclusion": "添加列表例外", + "AddListExclusion": "新增 列表", "AddListExclusionSeriesHelpText": "防止剧集通过列表添加到{appName}", "AddNewSeriesSearchForCutoffUnmetEpisodes": "开始搜索未达截止条件的集", "AddedDate": "加入于:{date}", From 724dd7e7335119f857fd603a5174a59f625f911b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 20 Feb 2024 20:31:08 -0800 Subject: [PATCH 133/762] Clean branch name to remove slashes --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47dff9073..9c6e9c0da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ concurrency: env: FRAMEWORK: net6.0 - BRANCH: ${{ github.head_ref || github.ref_name }} + RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 VERSION: 4.0.1 @@ -48,6 +48,8 @@ jobs: echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV" echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV" + echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV" + echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT" echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT" echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT" From 7a37f130f99d092c41c60908b94651061c4c7990 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 26 Feb 2024 20:29:00 -0800 Subject: [PATCH 134/762] Bump version to 4.0.2 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c6e9c0da..5c65b49f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.1 + VERSION: 4.0.2 jobs: backend: From 6377c688fc7b35749d608bf62796446bb5bcb11b Mon Sep 17 00:00:00 2001 From: Bruno Garcia <bruno@brunogarcia.com> Date: Tue, 27 Feb 2024 00:30:32 -0500 Subject: [PATCH 135/762] Update Sentry SDK add features Co-authored-by: Stefan Jandl <reg@bitfox.at> --- package.json | 3 +- src/Directory.Build.props | 29 ++++ .../SentryTargetFixture.cs | 3 +- .../Instrumentation/NzbDroneLogger.cs | 6 +- .../Instrumentation/Sentry/SentryTarget.cs | 43 +++++- src/NzbDrone.Common/Sonarr.Common.csproj | 2 +- yarn.lock | 131 ++++++++---------- 7 files changed, 134 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 719159698..267e78f5f 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,7 @@ "@fortawesome/react-fontawesome": "0.2.0", "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", - "@sentry/browser": "7.51.2", - "@sentry/integrations": "7.51.2", + "@sentry/browser": "7.100.0", "@types/node": "18.16.8", "@types/react": "18.2.6", "@types/react-dom": "18.2.4", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 370dfce70..ef0944ea9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -98,6 +98,35 @@ <RootNamespace Condition="'$(SonarrProject)'=='true'">$(MSBuildProjectName.Replace('Sonarr','NzbDrone'))</RootNamespace> </PropertyGroup> + <ItemGroup Condition="'$(TestProject)'!='true'"> + <!-- Annotates .NET assemblies with repository information including SHA --> + <!-- Sentry uses this to link directly to GitHub at the exact version/file/line --> + <!-- This is built-in on .NET 8 and can be removed once the project is updated --> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> + </ItemGroup> + + <!-- Sentry specific configuration: Only in Release mode --> + <PropertyGroup Condition="'$(Configuration)' == 'Release'"> + <!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ --> + <!-- OrgSlug, ProjectSlug and AuthToken are required. + They can be set below, via argument to 'msbuild -p:' or environment variable --> + <SentryOrg></SentryOrg> + <SentryProject></SentryProject> + <SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io --> + <SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN --> + + <!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths + without the need to deploy the application with PDBs --> + <SentryUploadSymbols>true</SentryUploadSymbols> + + <!-- Source Link settings --> + <!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl --> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded + to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller --> + <EmbedAllSources>true</EmbedAllSources> + </PropertyGroup> + <!-- Standard testing packages --> <ItemGroup Condition="'$(TestProject)'=='true'"> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs index 7392c3b85..b60fe2e54 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs @@ -4,6 +4,7 @@ using System.Linq; using FluentAssertions; using NLog; using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Test.Common; @@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests [SetUp] public void Setup() { - _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111"); + _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object); } private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message) diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index f654de585..750ad659c 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Common.Instrumentation RegisterDebugger(); } - RegisterSentry(updateApp); + RegisterSentry(updateApp, appFolderInfo); if (updateApp) { @@ -60,7 +60,7 @@ namespace NzbDrone.Common.Instrumentation LogManager.ReconfigExistingLoggers(); } - private static void RegisterSentry(bool updateClient) + private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo) { string dsn; @@ -80,7 +80,7 @@ namespace NzbDrone.Common.Instrumentation Target target; try { - target = new SentryTarget(dsn) + target = new SentryTarget(dsn, appFolderInfo) { Name = "sentryTarget", Layout = "${message}" diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 886063f30..0c8ed2f78 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -9,6 +9,7 @@ using NLog; using NLog.Common; using NLog.Targets; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using Sentry; namespace NzbDrone.Common.Instrumentation.Sentry @@ -96,7 +97,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry public bool FilterEvents { get; set; } public bool SentryEnabled { get; set; } - public SentryTarget(string dsn) + public SentryTarget(string dsn, IAppFolderInfo appFolderInfo) { _sdk = SentrySdk.Init(o => { @@ -104,9 +105,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry o.AttachStacktrace = true; o.MaxBreadcrumbs = 200; o.Release = BuildInfo.Release; - o.BeforeSend = x => SentryCleanser.CleanseEvent(x); - o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); + o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x)); + o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x)); o.Environment = BuildInfo.Branch; + + // Crash free run statistics (sends a ping for healthy and for crashes sessions) + o.AutoSessionTracking = true; + + // Caches files in the event device is offline + // Sentry creates a 'sentry' sub directory, no need to concat here + o.CacheDirectoryPath = appFolderInfo.GetAppDataPath(); + + // default environment is production + if (!RuntimeInfo.IsProduction) + { + if (RuntimeInfo.IsDevelopment) + { + o.Environment = "development"; + } + else if (RuntimeInfo.IsTesting) + { + o.Environment = "testing"; + } + else + { + o.Environment = "other"; + } + } }); InitializeScope(); @@ -125,7 +150,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry { SentrySdk.ConfigureScope(scope => { - scope.User = new User + scope.User = new SentryUser { Id = HashUtil.AnonymousToken() }; @@ -285,13 +310,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry } } + var level = LoggingLevelMap[logEvent.Level]; var sentryEvent = new SentryEvent(logEvent.Exception) { - Level = LoggingLevelMap[logEvent.Level], + Level = level, Logger = logEvent.LoggerName, Message = logEvent.FormattedMessage }; + if (level is SentryLevel.Fatal && logEvent.Exception is not null) + { + // Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with + // the 'unhandled' exception flag + logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false); + } + sentryEvent.SetExtras(extras); sentryEvent.SetFingerprint(fingerPrint); diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 1a63fd21d..831798a2e 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -11,7 +11,7 @@ <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> - <PackageReference Include="Sentry" Version="3.23.1" /> + <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="System.Text.Json" Version="6.0.8" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" /> diff --git a/yarn.lock b/yarn.lock index 3f761abd6..fe5804ff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1261,68 +1261,76 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@sentry-internal/tracing@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.51.2.tgz#17833047646426ca71445327018ffcb33506a699" - integrity sha512-OBNZn7C4CyocmlSMUPfkY9ORgab346vTHu5kX35PgW5XR51VD2nO5iJCFbyFcsmmRWyCJcZzwMNARouc2V4V8A== +"@sentry-internal/feedback@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.100.0.tgz#38d8d4cb8ac3e6e24d91b13878bd6208a55bcab3" + integrity sha512-SMW2QhNKOuSjw8oPtvryDlJjiwrNyAKljbgtMk057os/fd8QMp38Yt1ImqLCM4B2rTQZ6REJ6hRGRTRcfqoG+w== dependencies: - "@sentry/core" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" + "@sentry/core" "7.100.0" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" -"@sentry/browser@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.51.2.tgz#c01758a54c613be45df58ab503805737256f51a4" - integrity sha512-FQFEaTFbvYHPQE2emFjNoGSy+jXplwzoM/XEUBRjrGo62lf8BhMvWnPeG3H3UWPgrWA1mq0amvHRwXUkwofk0g== +"@sentry-internal/replay-canvas@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.100.0.tgz#b462346832631ed5a9446686419113ff331bd984" + integrity sha512-DePinj5IgNiC4RZv0yX0DLccMZebfFdKl3zHwDeLBeZqtMz9VrPzchv57IWP+5MI1+iuOn+WOg4oTNBUG6hFRw== dependencies: - "@sentry-internal/tracing" "7.51.2" - "@sentry/core" "7.51.2" - "@sentry/replay" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" + "@sentry/core" "7.100.0" + "@sentry/replay" "7.100.0" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" -"@sentry/core@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.51.2.tgz#f2c938de334f9bf26f4416079168275832423964" - integrity sha512-p8ZiSBxpKe+rkXDMEcgmdoyIHM/1bhpINLZUFPiFH8vzomEr7sgnwRhyrU8y/ADnkPeNg/2YF3QpDpk0OgZJUA== +"@sentry-internal/tracing@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.100.0.tgz#01f0925a287a6e5d0becd731ab361cabbd27c007" + integrity sha512-qf4W1STXky9WOQYoPSw2AmCBDK4FzvAyq5yeD2sLU7OCUEfbRUcN0lQljUvmWRKv/jTIAyeU5icDLJPZuR50nA== dependencies: - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" + "@sentry/core" "7.100.0" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" -"@sentry/integrations@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.51.2.tgz#fce58b9ced601c7f93344b508c67c69a9c883f3d" - integrity sha512-ZnSptbuDQOoQ13mFX9vvLDfXlbMGjenW2fMIssi9+08B7fD6qxmetkYnWmBK+oEipjoGA//0240Fj8FUvZr0Qg== +"@sentry/browser@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.100.0.tgz#adf57f660baa6190a7e1709605f73b94818ee04b" + integrity sha512-XpM0jEVe6DJWXjMSOjtJxsSNR/XnJKrlcuyoI4Re3qLG+noEF5QLc0r3VJkySXPRFnmdW05sLswQ6a/n9Sijmg== dependencies: - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - localforage "^1.8.1" - tslib "^1.9.3" + "@sentry-internal/feedback" "7.100.0" + "@sentry-internal/replay-canvas" "7.100.0" + "@sentry-internal/tracing" "7.100.0" + "@sentry/core" "7.100.0" + "@sentry/replay" "7.100.0" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" -"@sentry/replay@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.51.2.tgz#1f54e92b472ab87dfdb4e8cd6b8c8252600fe7b0" - integrity sha512-W8YnSxkK9LTUXDaYciM7Hn87u57AX9qvH8jGcxZZnvpKqHlDXOpSV8LRtBkARsTwgLgswROImSifY0ic0lyCWg== +"@sentry/core@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.100.0.tgz#5b28c7b3e41e45e4d50e3bdea5d35434fd78e86b" + integrity sha512-eWRPuP0Zdj4a2F7SybqNjf13LGOVgGwvW6sojweQp9oxGAfCPp/EMDGBhlpYbMJeLbzmqzJ4ZFHIedaiEC+7kg== dependencies: - "@sentry/core" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" -"@sentry/types@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.51.2.tgz#cb742f374d9549195f62c462c915adeafed31d65" - integrity sha512-/hLnZVrcK7G5BQoD/60u9Qak8c9AvwV8za8TtYPJDUeW59GrqnqOkFji7RVhI7oH1OX4iBxV+9pAKzfYE6A6SA== - -"@sentry/utils@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.51.2.tgz#2a52ac2cfb00ffd128248981279c0a561b39eccb" - integrity sha512-EcjBU7qG4IG+DpIPvdgIBcdIofROMawKoRUNKraeKzH/waEYH9DzCaqp/mzc5/rPBhpDB4BShX9xDDSeH+8c0A== +"@sentry/replay@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.100.0.tgz#4f2e35155626ab286692ade3e31da282c73bd402" + integrity sha512-6Yo56J+x+eedaMXri8pPlFxXOofnSXVdsUuFj+kJ7lC/qHrwIbgC5g1ONEK/WlYwpVH4gA0aNnCa5AOkMu+ZTg== dependencies: - "@sentry/types" "7.51.2" - tslib "^1.9.3" + "@sentry-internal/tracing" "7.100.0" + "@sentry/core" "7.100.0" + "@sentry/types" "7.100.0" + "@sentry/utils" "7.100.0" + +"@sentry/types@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.100.0.tgz#a16f60d78613bd9810298e9e8d80134be58b24f8" + integrity sha512-c+RHwZwpKeBk7h8sUX4nQcelxBz8ViCojifnbEe3tcn8O15HOLvZqRKgLLOiff3MoErxiv4oxs0sPbEFRm/IvA== + +"@sentry/utils@7.100.0": + version "7.100.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.100.0.tgz#a9d36c01eede117c3e17b0350d399a87934e9c66" + integrity sha512-LAhZMEGq3C125prZN/ShqeXpRfdfgJkl9RAKjfq8cmMFsF7nsF72dEHZgIwrZ0lgNmtaWAB83AwJcyN83RwOxQ== + dependencies: + "@sentry/types" "7.100.0" "@types/archiver@^5.3.1": version "5.3.2" @@ -3747,11 +3755,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== - "immutable@^3.8.1 || ^4.0.0", immutable@^4.0.0: version "4.3.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" @@ -4205,13 +4208,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lie@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== - dependencies: - immediate "~3.0.5" - lilconfig@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -4267,13 +4263,6 @@ loader-utils@^3.2.1: resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== -localforage@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" - integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== - dependencies: - lie "3.1.1" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -6448,7 +6437,7 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== From 98d60e1a8e9abce6b31b3cdd745eff0fed181458 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Tue, 27 Feb 2024 05:30:58 +0000 Subject: [PATCH 136/762] Replace URLs in translations with tokens --- .../FileBrowser/FileBrowserModalContent.js | 2 +- .../Specifications/EditSpecificationModalContent.js | 4 ++-- frontend/src/Settings/MetadataSource/TheTvdb.js | 4 ++-- .../Specifications/EditSpecificationModalContent.js | 4 ++-- src/NzbDrone.Core/Localization/Core/de.json | 6 +++--- src/NzbDrone.Core/Localization/Core/en.json | 8 ++++---- src/NzbDrone.Core/Localization/Core/fi.json | 8 ++++---- src/NzbDrone.Core/Localization/Core/fr.json | 6 +++--- src/NzbDrone.Core/Localization/Core/hu.json | 6 +++--- src/NzbDrone.Core/Localization/Core/pt_BR.json | 6 +++--- src/NzbDrone.Core/Localization/Core/zh_CN.json | 12 ++++++------ 11 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js index f517b4d1b..61ebfaaa4 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component { className={styles.mappedDrivesWarning} kind={kinds.WARNING} > - <InlineMarkdown data={translate('MappedNetworkDrivesWindowsService')} /> + <InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} /> </Alert> } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js index 855832620..71f3cffa9 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js @@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) { <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> </div> <div> - <InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> + <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} /> </div> <div> - <InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> + <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} /> </div> </Alert> } diff --git a/frontend/src/Settings/MetadataSource/TheTvdb.js b/frontend/src/Settings/MetadataSource/TheTvdb.js index d1a51d67a..b1abb0c99 100644 --- a/frontend/src/Settings/MetadataSource/TheTvdb.js +++ b/frontend/src/Settings/MetadataSource/TheTvdb.js @@ -4,6 +4,7 @@ import translate from 'Utilities/String/translate'; import styles from './TheTvdb.css'; function TheTvdb(props) { + debugger; return ( <div className={styles.container}> <img @@ -15,8 +16,7 @@ function TheTvdb(props) { <div className={styles.title}> {translate('TheTvdb')} </div> - - <InlineMarkdown data={translate('SeriesAndEpisodeInformationIsProvidedByTheTVDB')} /> + <InlineMarkdown data={translate('SeriesAndEpisodeInformationIsProvidedByTheTVDB', { url: 'https://www.thetvdb.com/subscribe' })} /> </div> </div> diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js index 2ab1e4a1c..04302729b 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js @@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) { <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> </div> <div> - <InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> + <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} /> </div> <div> - <InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> + <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} /> </div> </Alert> } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 912418807..40ea83ad4 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -422,7 +422,7 @@ "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sie müssen die Datumssortierung für die von {appName} verwendete Kategorie deaktivieren, um Importprobleme zu vermeiden. Gehen Sie zu Sabnzbd, um das Problem zu beheben.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deaktivieren Sie die Filmsortierung", "AllResultsAreHiddenByTheAppliedFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet", - "RegularExpressionsCanBeTested": "Reguläre Ausdrücke können [hier] getestet werden (http://regexstorm.net/tester).", + "RegularExpressionsCanBeTested": "Reguläre Ausdrücke können [hier] getestet werden ({url}).", "ReleaseSceneIndicatorUnknownSeries": "Unbekannte Folge oder Serie.", "RemoveFilter": "Filter entfernen", "RemoveFailedDownloadsHelpText": "Entfernen Sie fehlgeschlagene Downloads aus dem Download-Client-Verlauf", @@ -669,7 +669,7 @@ "AppUpdatedVersion": "{appName} wurde auf die Version „{version}“ aktualisiert. Um die neuesten Änderungen zu erhalten, müssen Sie {appName} neu laden ", "UseHardlinksInsteadOfCopy": "Verwende Hardlinks statt Kopieren", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Der Download-Client {downloadClientName} ist so eingestellt, dass abgeschlossene Downloads entfernt werden. Dies kann dazu führen, dass Downloads von Ihrem Client entfernt werden, bevor {appName} sie importieren kann.", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Informationen zu Serien und Episoden werden von TheTVDB.com bereitgestellt. [Bitte denken Sie darüber nach, sie zu unterstützen](https://www.thetvdb.com/subscribe).", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Informationen zu Serien und Episoden werden von TheTVDB.com bereitgestellt. [Bitte denken Sie darüber nach, sie zu unterstützen]({url}).", "ShownClickToHide": "Angezeigt, zum Ausblenden klicken", "Tba": "Wird noch bekannt gegeben", "TaskUserAgentTooltip": "Benutzeragent, bereitgestellt von der App, die die API aufgerufen hat", @@ -694,7 +694,7 @@ "Discord": "Discord", "Restart": "Neu starten", "Rejections": "Ablehnungen", - "RegularExpressionsTutorialLink": "Weitere Details zu regulären Ausdrücken finden Sie [hier](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsTutorialLink": "Weitere Details zu regulären Ausdrücken finden Sie [hier]({url}).", "RegularExpression": "Regulären Ausdruck", "RenameEpisodes": "Episoden umbenennen", "RemovedSeriesSingleRemovedHealthCheckMessage": "Die Serie {series} wurde aus TheTVDB entfernt", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 2f4a3bbb2..3f2cb75ee 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1079,7 +1079,7 @@ "ManualGrab": "Manual Grab", "ManualImport": "Manual Import", "ManualImportItemsLoadError": "Unable to load manual import items", - "MappedNetworkDrivesWindowsService": "Mapped network drives are not available when running as a Windows Service, see the [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) for more information.", + "MappedNetworkDrivesWindowsService": "Mapped network drives are not available when running as a Windows Service, see the [FAQ]({url}) for more information.", "Mapping": "Mapping", "MarkAsFailed": "Mark as Failed", "MarkAsFailedConfirmation": "Are you sure you want to mark '{sourceTitle}' as failed?", @@ -1567,8 +1567,8 @@ "RefreshAndScanTooltip": "Refresh information and scan disk", "RefreshSeries": "Refresh Series", "RegularExpression": "Regular Expression", - "RegularExpressionsCanBeTested": "Regular expressions can be tested [here](http://regexstorm.net/tester).", - "RegularExpressionsTutorialLink": "More details on regular expressions can be found [here](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsCanBeTested": "Regular expressions can be tested [here]({url}).", + "RegularExpressionsTutorialLink": "More details on regular expressions can be found [here]({url}).", "RejectionCount": "Rejection Count", "Rejections": "Rejections", "RelativePath": "Relative Path", @@ -1763,7 +1763,7 @@ "SelectSeries": "Select Series", "SendAnonymousUsageData": "Send Anonymous Usage Data", "Series": "Series", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them](https://www.thetvdb.com/subscribe).", + "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", "SeriesDetailsGoTo": "Go to {title}", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index e4498e928..af390db5a 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -735,7 +735,7 @@ "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kohdistukset ja säiliön tallennusmedian asetukset.", "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista](https://www.thetvdb.com/subscribe)", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista]({url})", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakosuhderajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", "Yesterday": "Eilen", @@ -1727,7 +1727,7 @@ "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Yli 14 päivää sitten julkaistujen jaksojen kaappauksille käytettävä painotus.", "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Trakt-lajityyppien slug-arvoilla (pilkuin eroteltuna). Koskee vain suosituimpia listoja.", "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse sarjatyyppi, jonka haluat tuoda.", - "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja [UKK:sta](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server).", + "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja [UKK:sta]({url}).", "MetadataSettingsSeriesMetadata": "Sarjojen metatiedot", "MetadataSettingsSeriesMetadataUrl": "Sarjojen metatietojen URL", "MetadataSettingsSeriesMetadataEpisodeGuide": "Sarjojen metatietojen jakso-opas", @@ -1744,7 +1744,7 @@ "UpgradesAllowedHelpText": "Jos käytöstä poistettuja laatuja ei päivitetä.", "Repack": "Uudelleenpaketoitu", "SupportedAutoTaggingProperties": "{appName} tukee automaattimerkinnän säännöissä seuraavia arvoja", - "RegularExpressionsCanBeTested": "Säännöllisiä lausekkeita voidaan testata [täällä](http://regexstorm.net/tester).", + "RegularExpressionsCanBeTested": "Säännöllisiä lausekkeita voidaan testata [täällä]({url}).", "RssSyncIntervalHelpTextWarning": "Tämä koskee kaikkia tietolähteitä. Noudata niiden asettamia sääntöjä.", "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla. Esimerkiksi \"{url}\". Oletus on \"{defaultApiUrl}\".", "DownloadClientFreeboxSettingsHostHelpText": "Freeboxin isäntänimi tai IP-osoite. Oletus on \"{url}\" (toimii vain samassa verkossa).", @@ -1764,7 +1764,7 @@ "MetadataPlexSettingsSeriesPlexMatchFile": "Luo Plex Match -tiedostot", "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Luo sarjojen kansioihin .plexmatch-tiedostot.", "NoResultsFound": "Tuloksia ei löytynyt.", - "RegularExpressionsTutorialLink": "Lisätietoja säännöllisistä lausekkeista löytyy [täältä](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsTutorialLink": "Lisätietoja säännöllisistä lausekkeista löytyy [täältä]({url}).", "ResetAPIKey": "Korvaa rajapinnan avain", "Reset": "Uudista", "ResetDefinitions": "Palauta määritykset", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index a89f0e92b..94adeea3a 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -647,7 +647,7 @@ "RefreshAndScan": "Actualiser et analyser", "RefreshAndScanTooltip": "Actualiser les informations et analyser le disque", "RegularExpression": "Expression régulière", - "RegularExpressionsTutorialLink": "Plus de détails sur les expressions régulières peuvent être trouvés [ici](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsTutorialLink": "Plus de détails sur les expressions régulières peuvent être trouvés [ici]({url}).", "RelativePath": "Chemin relatif", "Release": "Version", "ReleaseGroup": "Groupe de versions", @@ -703,7 +703,7 @@ "SelectFolder": "Sélectionner le dossier", "SelectLanguage": "Choisir la langue", "SendAnonymousUsageData": "Envoyer des données d'utilisation anonymes", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Les informations sur les séries et les épisodes sont fournies par TheTVDB.com. [Veuillez envisager de les soutenir](https://www.thetvdb.com/subscribe).", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Les informations sur les séries et les épisodes sont fournies par TheTVDB.com. [Veuillez envisager de les soutenir]({url}).", "SeriesCannotBeFound": "Désolé, cette série est introuvable.", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} fichiers d'épisode", "SeriesDetailsRuntime": "{runtime} Minutes", @@ -933,7 +933,7 @@ "OnEpisodeFileDeleteForUpgrade": "Lors de la suppression du fichier de l'épisode pour la mise à niveau", "OnGrab": "À saisir", "OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse", - "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici](http://regexstorm.net/tester).", + "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici]({url}).", "ReleaseProfileIndexerHelpText": "Spécifiez à quel indexeur 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'", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 9bc89c45d..baea0e8a9 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -627,7 +627,7 @@ "LastWriteTime": "Utolsó írási idő", "LogFiles": "Naplófájlok", "ManualImport": "Kézi importálás", - "MappedNetworkDrivesWindowsService": "A leképezett hálózati meghajtók nem érhetők el, ha Windows szolgáltatásként futnak, lásd a [GYIK](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) for more information.", + "MappedNetworkDrivesWindowsService": "A leképezett hálózati meghajtók nem érhetők el, ha Windows szolgáltatásként futnak, lásd a [GYIK]({url}) for more information.", "MediaManagementSettingsLoadError": "Nem sikerült betölteni a médiakezelési beállításokat", "MediaManagementSettings": "Médiakezelési beállítások", "NoMonitoredEpisodesSeason": "Ebben az évadban nincsenek felügyelt epizódok", @@ -856,7 +856,7 @@ "DeleteAutoTagHelpText": "Biztosan törli a(z) „{name}” automatikus címkét?", "RecyclingBinHelpText": "A fájlok törléskor ide kerülnek a végleges törlés helyett", "ApplyTagsHelpTextReplace": "Csere: Cserélje ki a címkéket a megadott címkékkel (az összes címke törléséhez ne írjon be címkéket)", - "RegularExpressionsCanBeTested": "A reguláris kifejezések [here] tesztelhetők (http://regexstorm.net/tester).", + "RegularExpressionsCanBeTested": "A reguláris kifejezések [here] tesztelhetők ({url}).", "SelectLanguage": "Válasszon nyelvet", "SeriesEditor": "Sorozat szerkesztő", "SeriesFolderFormat": "Sorozat mappa formátum", @@ -901,7 +901,7 @@ "RemotePathMappingHostHelpText": "Ugyanaz a gazdagép, amelyet a távoli letöltési klienshez megadott", "SelectQuality": "Minőség kiválasztása", "SeriesIndexFooterEnded": "Befejeződött (az összes epizód letöltve)", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "A sorozatokkal és epizódokkal kapcsolatos információkat a TheTVDB.com biztosítja. [Kérjük, fontolja meg támogatásukat](https://www.thetvdb.com/subscribe).", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "A sorozatokkal és epizódokkal kapcsolatos információkat a TheTVDB.com biztosítja. [Kérjük, fontolja meg támogatásukat]({url}).", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} epizódfájl", "SeriesIsMonitored": "A sorozatot figyelik", "SeriesTitle": "Sorozat címe", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 6fe4cde11..8520f4a28 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -768,8 +768,8 @@ "QualityProfileInUseSeriesListCollection": "Não é possível excluir um perfil de qualidade anexado a uma série, lista ou coleção", "EnableColorImpairedModeHelpText": "Estilo alterado para permitir que usuários com deficiência de cor distingam melhor as informações codificadas por cores", "RegularExpression": "Expressão Regular", - "RegularExpressionsCanBeTested": "Expressões regulares podem ser testadas [aqui](http://regexstorm.net/tester).", - "RegularExpressionsTutorialLink": "Mais detalhes sobre expressões regulares podem ser encontrados [aqui](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsCanBeTested": "Expressões regulares podem ser testadas [aqui]({url}).", + "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", @@ -821,7 +821,7 @@ "SeasonFolderFormat": "Formato da Pasta da Temporada", "Security": "Segurança", "SendAnonymousUsageData": "Enviar dados de uso anônimos", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "As informações sobre séries e episódios são fornecidas por TheTVDB.com. [Por favor, considere apoiá-los](https://www.thetvdb.com/subscribe).", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "As informações sobre séries e episódios são fornecidas por TheTVDB.com. [Por favor, considere apoiá-los]({url}).", "SeriesFolderFormat": "Formato de Pasta das Séries", "SeriesFolderFormatHelpText": "Usado ao adicionar uma nova série ou mover uma série por meio do editor de séries", "SeriesID": "ID da Série", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 854c4a475..f95f32f6b 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -550,8 +550,8 @@ "AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的版本", "AutoTaggingLoadError": "无法加载自动标记", "Automatic": "自动化", - "AutoTaggingRequiredHelpText": "这个{0}条件必须匹配自动标记规则才能应用。否则,一个{0}匹配就足够了。", - "AutoTaggingNegateHelpText": "如果选中,当 {0} 条件匹配时,自动标记不会应用。", + "AutoTaggingRequiredHelpText": "这个{implementationName}条件必须匹配自动标记规则才能应用。否则,一个{implementationName}匹配就足够了。", + "AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。", "BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理", "BackupsLoadError": "无法加载备份", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "最小自定义格式分数", @@ -795,7 +795,7 @@ "SeriesFinale": "大结局", "SeriesFolderFormat": "剧集文件夹格式", "ReplaceIllegalCharactersHelpText": "替换非法字符,如未勾选,则会被{appName}移除", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们](https://www.thetvdb.com/subscribe)。", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们]({url})。", "DeleteSelectedSeries": "删除选中的剧集", "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", "DeleteSeriesFolderCountConfirmation": "你确定要删除选中的 {count} 个剧集吗?", @@ -803,7 +803,7 @@ "FormatAgeDay": "天", "RegularExpression": "正则表达式", "FormatAgeDays": "天", - "RegularExpressionsTutorialLink": "有关正则表达式的更多详细信息,请参阅[此处](https://www.regular-expressions.info/tutorial.html)。", + "RegularExpressionsTutorialLink": "有关正则表达式的更多详细信息,请参阅[此处]({url})。", "FormatDateTime": "{formattedDate} {formattedTime}", "ReleaseProfileTagSeriesHelpText": "发布配置将应用于至少有一个匹配标记的剧集。留空适用于所有剧集", "FormatShortTimeSpanHours": "{hours}小时", @@ -963,7 +963,7 @@ "LocalStorageIsNotSupported": "不支持或禁用本地存储。插件或私人浏览可能已将其禁用。", "ManualGrab": "手动抓取", "ManualImport": "手动导入", - "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server)获取更多信息。", + "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答]({url})获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", @@ -1453,7 +1453,7 @@ "RecentChanges": "最近修改", "RecyclingBinCleanupHelpText": "设置为0关闭自动清理", "RecyclingBinHelpText": "文件将在删除时移动到此处,而不是永久删除", - "RegularExpressionsCanBeTested": "正则表达式可在[此处](http://regexstorm.net/tester)测试。", + "RegularExpressionsCanBeTested": "正则表达式可在[此处]({url})测试。", "SslPort": "SSL端口", "TablePageSizeMinimum": "页面大小必须至少为 {minimumValue}", "TorrentDelayTime": "Torrent延时:{torrentDelay}", From a11ee7bc116170542352708807c6f15b78731aba Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 27 Feb 2024 04:54:14 +0000 Subject: [PATCH 137/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: EDUYO <eduardoestabiel@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Magyar <kochnorbert@icloud.com> Co-authored-by: Sadi A. Nogueira <contato@sadi.eti.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Xupix <colinaubert25@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 2 +- src/NzbDrone.Core/Localization/Core/fr.json | 6 ++++-- src/NzbDrone.Core/Localization/Core/hu.json | 7 ++++++- src/NzbDrone.Core/Localization/Core/pt.json | 2 +- src/NzbDrone.Core/Localization/Core/pt_BR.json | 12 +++++++++--- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index cb90cbc58..28afd80b8 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -18,7 +18,7 @@ "Year": "Año", "Reload": "Recargar", "AbsoluteEpisodeNumber": "Número de Episodio Absoluto", - "AddAutoTagError": "No se pudo añadir una nueva etiqueta automática, inténtelo de nuevo.", + "AddAutoTagError": "", "AddConditionError": "No se pudo añadir una nueva condición, inténtelo de nuevo.", "AddConnection": "Añadir Conexión", "AddCustomFormat": "Añadir Formato Personalizado", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 94adeea3a..65046c53b 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1870,7 +1870,7 @@ "EpisodeFileMissingTooltip": "Fichier de l'épisode manquant", "AutoTaggingSpecificationGenre": "Genre(s)", "AutoTaggingSpecificationMaximumYear": "Année maximum", - "AutoTaggingSpecificationMinimumYear": "Année maximum", + "AutoTaggingSpecificationMinimumYear": "Année minimum", "AutoTaggingSpecificationOriginalLanguage": "Langue", "AutoTaggingSpecificationQualityProfile": "Profil de Qualité", "AutoTaggingSpecificationRootFolder": "Dossier Racine", @@ -1921,5 +1921,7 @@ "IgnoreDownloadsHint": "Empêche {appName} de poursuivre le traitement de ces téléchargements", "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrents bloqués lors de la saisie", "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" + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index baea0e8a9..f8ca3b381 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1405,5 +1405,10 @@ "PendingChangesDiscardChanges": "Vesse el a változtatásokat, és lépjen ki", "SeasonPremiere": "Évad Premier", "SeasonPremieresOnly": "Csak az évad premierjei", - "PasswordConfirmation": "Jelszó megerősítése" + "PasswordConfirmation": "Jelszó megerősítése", + "QualitiesHelpText": "A listán magasabb minőséget részesítik előnyben. Ugyanazon csoporton belül a tulajdonságok egyenlőek. Csak ellenőrzött minőségek szükségesek", + "Progress": "Előrehalad", + "ParseModalUnableToParse": "A megadott cím nem elemezhető, próbálkozzon újra.", + "ParseModalHelpText": "Adja meg a kiadás címét a fenti bevitelben", + "ProtocolHelpText": "Válassza ki, melyik protokoll(oka)t használja, és melyiket részesíti előnyben, ha az egyébként egyenlő kiadások közül választ" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 9caecef1a..8f2497675 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -60,7 +60,7 @@ "AutoTaggingNegateHelpText": "Se marcada, a regra de etiqueta automática não será aplicada se esta condição {implementationName} corresponder.", "AddNew": "Adicionar Novo", "Age": "Idade", - "AddAutoTagError": "Não foi possível adicionar uma nova etiqueta automática, tente novamente.", + "AddAutoTagError": "Não foi possível adicionar uma nova tag automática. Por favor, tente novamente.", "AddConditionError": "Não foi possível adicionar uma nova condição, tente novamente.", "AddConnection": "Adicionar Conexão", "AddCustomFormat": "Adicionar formato personalizado", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8520f4a28..4f8f94413 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1872,8 +1872,8 @@ "EpisodeFileMissingTooltip": "Arquivo do episódio ausente", "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2", "DownloadClientPriorityHelpText": "Prioridade do Cliente de Download de 1 (mais alta) a 50 (mais baixa). Padrão: 1. Round-Robin é usado para clientes com a mesma prioridade.", - "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar Torrent com Hashes na Lista de Bloqueio Enquanto Capturando", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "se um torrent for bloqueado por hash, ele pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar Hashes de Torrent Bloqueados Durante a Captura", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Se um torrent for bloqueado por hash, ele pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", "ImportListsSimklSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Séries Mais Assistidas por Semana", "ImportListsTraktSettingsPopularListTypeTopYearShows": "Séries Mais Assistidas por Ano", @@ -2038,5 +2038,11 @@ "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", "LogOnly": "Só Registro", "CleanLibraryLevel": "Limpar Nível da Biblioteca", - "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente." + "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente.", + "ClickToChangeIndexerFlags": "Clique para alterar sinalizadores do indexador", + "SelectIndexerFlags": "Selecionar Sinalizadores do Indexador", + "SetIndexerFlagsModalTitle": "{modalTitle} - Definir Sinalizadores do Indexador", + "CustomFormatsSpecificationFlag": "Sinalizador", + "IndexerFlags": "Sinalizadores do Indexador", + "SetIndexerFlags": "Definir Sinalizadores de Indexador" } From cb72e752f9e24e5691292d86ac1f46c8fa35a844 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 26 Feb 2024 21:32:31 -0800 Subject: [PATCH 138/762] Fixed: Parsing of subtitle languages separated by dash Closes #6494 --- src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs | 4 ++++ src/NzbDrone.Core/Parser/LanguageParser.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 0f3cf2749..af422fa1b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -441,6 +441,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")] + [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.en.ass", new string[0], "mytitle", "English")] public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage) { var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle); diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 77b51e828..d1c995b87 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -29,9 +29,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex GermanDualLanguageRegex = new (@"(?<!WEB[-_. ]?)\bDL\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex GermanMultiLanguageRegex = new (@"\bML\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SubtitleLanguageRegex = new Regex(".+?([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SubtitleLanguageRegex = new Regex(".+?([-_. ](?<tags>forced|foreign|default|cc|psdh|sdh))*[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?<tags1>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*\.(?<title>[^.]*)(\.((?<tags2>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?<tags1>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*[-_. ](?<title>[^.]*)(\.((?<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); From 33b44a8a53ba69bf59c3e42e66ec98bcb609c04a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 23 Feb 2024 21:03:42 -0800 Subject: [PATCH 139/762] New: Option to sync season monitoring state when importing from another Sonarr instance Closes #6542 --- .../ImportLists/ImportListSyncService.cs | 5 ++++- .../ImportLists/Sonarr/SonarrAPIResource.cs | 7 +++++++ .../ImportLists/Sonarr/SonarrImport.cs | 16 ++++++++++++++-- .../ImportLists/Sonarr/SonarrSettings.cs | 13 ++++++++----- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ .../Parser/Model/ImportListItemInfo.cs | 8 ++++++++ src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs | 6 ++++++ src/NzbDrone.Core/Tv/MonitoringOptions.cs | 3 ++- 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 1c4c9ea7a..2c51ea6c0 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -222,11 +222,14 @@ namespace NzbDrone.Core.ImportLists QualityProfileId = importList.QualityProfileId, SeriesType = importList.SeriesType, SeasonFolder = importList.SeasonFolder, + Seasons = item.Seasons, Tags = importList.Tags, AddOptions = new AddSeriesOptions { SearchForMissingEpisodes = importList.SearchForMissingEpisodes, - Monitor = importList.ShouldMonitor + + // If seasons are provided use them for syncing monitored status, otherwise use the list setting. + Monitor = item.Seasons.Any() ? MonitorTypes.Skip : importList.ShouldMonitor } }); } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrAPIResource.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrAPIResource.cs index 884d1247c..363964626 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrAPIResource.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrAPIResource.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr public int QualityProfileId { get; set; } public int LanguageProfileId { get; set; } public string RootFolderPath { get; set; } + public List<SonarrSeason> Seasons { get; set; } public HashSet<int> Tags { get; set; } } @@ -35,4 +36,10 @@ namespace NzbDrone.Core.ImportLists.Sonarr public string Path { get; set; } public int Id { get; set; } } + + public class SonarrSeason + { + public int SeasonNumber { get; set; } + public bool Monitored { get; set; } + } } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs index 3a35ecf6d..d06b294f2 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Sonarr @@ -61,11 +62,22 @@ namespace NzbDrone.Core.ImportLists.Sonarr continue; } - series.Add(new ImportListItemInfo + var info = new ImportListItemInfo { TvdbId = item.TvdbId, Title = item.Title - }); + }; + + if (Settings.SyncSeasonMonitoring) + { + info.Seasons = item.Seasons.Select(s => new Season + { + SeasonNumber = s.SeasonNumber, + Monitored = s.Monitored + }).ToList(); + } + + series.Add(info); } _importListStatusService.RecordSuccess(Definition.Id); diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs index 8037a4efa..77772f003 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs @@ -35,12 +35,11 @@ namespace NzbDrone.Core.ImportLists.Sonarr [FieldDefinition(1, Label = "ApiKey", HelpText = "ImportListsSonarrSettingsApiKeyHelpText")] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "QualityProfiles", HelpText = "ImportListsSonarrSettingsQualityProfilesHelpText")] - public IEnumerable<int> ProfileIds { get; set; } + [FieldDefinition(2, Label = "ImportListsSonarrSettingsSyncSeasonMonitoring", HelpText = "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText", Type = FieldType.Checkbox)] + public bool SyncSeasonMonitoring { get; set; } - // TODO: Remove this eventually, no translation added as deprecated - [FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getLanguageProfiles", Label = "Language Profiles", HelpText = "Language Profiles from the source instance to import from")] - public IEnumerable<int> LanguageProfileIds { get; set; } + [FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "QualityProfiles", HelpText = "ImportListsSonarrSettingsQualityProfilesHelpText")] + public IEnumerable<int> ProfileIds { get; set; } [FieldDefinition(4, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "ImportListsSonarrSettingsTagsHelpText")] public IEnumerable<int> TagIds { get; set; } @@ -48,6 +47,10 @@ namespace NzbDrone.Core.ImportLists.Sonarr [FieldDefinition(5, Type = FieldType.Select, SelectOptionsProviderAction = "getRootFolders", Label = "RootFolders", HelpText = "ImportListsSonarrSettingsRootFoldersHelpText")] public IEnumerable<string> RootFolderPaths { get; set; } + // TODO: Remove this eventually, no translation added as deprecated + [FieldDefinition(6, Type = FieldType.Select, SelectOptionsProviderAction = "getLanguageProfiles", Label = "Language Profiles", HelpText = "Language Profiles from the source instance to import from")] + public IEnumerable<int> LanguageProfileIds { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 3f2cb75ee..8bc21c8e8 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -851,6 +851,8 @@ "ImportListsSimklSettingsUserListTypePlanToWatch": "Plan To Watch", "ImportListsSimklSettingsUserListTypeWatching": "Watching", "ImportListsSonarrSettingsApiKeyHelpText": "API Key of the {appName} instance to import from", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored", "ImportListsSonarrSettingsFullUrl": "Full URL", "ImportListsSonarrSettingsFullUrlHelpText": "URL, including port, of the {appName} instance to import from", "ImportListsSonarrSettingsQualityProfilesHelpText": "Quality Profiles from the source instance to import from", diff --git a/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs b/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs index 8d0e37534..34f1c3bce 100644 --- a/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ImportListItemInfo.cs @@ -1,10 +1,17 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser.Model { public class ImportListItemInfo : ModelBase { + public ImportListItemInfo() + { + Seasons = new List<Season>(); + } + public int ImportListId { get; set; } public string ImportList { get; set; } public string Title { get; set; } @@ -15,6 +22,7 @@ namespace NzbDrone.Core.Parser.Model public int MalId { get; set; } public int AniListId { get; set; } public DateTime ReleaseDate { get; set; } + public List<Season> Seasons { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs index 3f2acf9bf..c28c1d522 100644 --- a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs @@ -40,6 +40,12 @@ namespace NzbDrone.Core.Tv return; } + // Skip episode level monitoring and use season information when series was added + if (monitoringOptions.Monitor == MonitorTypes.Skip) + { + return; + } + var firstSeason = series.Seasons.Select(s => s.SeasonNumber).Where(s => s > 0).MinOrDefault(); var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault(); var episodes = _episodeService.GetEpisodeBySeries(series.Id); diff --git a/src/NzbDrone.Core/Tv/MonitoringOptions.cs b/src/NzbDrone.Core/Tv/MonitoringOptions.cs index b49a5ee6a..82983a26c 100644 --- a/src/NzbDrone.Core/Tv/MonitoringOptions.cs +++ b/src/NzbDrone.Core/Tv/MonitoringOptions.cs @@ -27,7 +27,8 @@ namespace NzbDrone.Core.Tv Recent, MonitorSpecials, UnmonitorSpecials, - None + None, + Skip } public enum NewItemMonitorTypes From 6dc0a8800435afb50e7974e10e2a82287fdeddd0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 25 Feb 2024 15:16:06 -0800 Subject: [PATCH 140/762] New: Search for recently aired anime episodes with added absolute episode number Closes #2044 --- .../TvTests/RefreshEpisodeServiceFixture.cs | 28 ++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 1 + src/NzbDrone.Core/Tv/Episode.cs | 1 + ...dService.cs => EpisodeRefreshedService.cs} | 64 +++++++++++-------- src/NzbDrone.Core/Tv/RefreshEpisodeService.cs | 8 +++ src/NzbDrone.Core/Tv/SeriesScannedHandler.cs | 8 +-- 6 files changed, 79 insertions(+), 31 deletions(-) rename src/NzbDrone.Core/Tv/{EpisodeAddedService.cs => EpisodeRefreshedService.cs} (50%) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 101305f52..99189c053 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -453,5 +453,33 @@ namespace NzbDrone.Core.Test.TvTests _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); } + + [Test] + public void should_mark_updated_episodes_that_have_newly_added_absolute_episode_number() + { + var episodes = Builder<Episode>.CreateListOfSize(3) + .Build() + .ToList(); + + var existingEpisodes = new List<Episode> + { + episodes[0], + episodes[1] + }; + + existingEpisodes[0].AbsoluteEpisodeNumber = null; + + Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) + .Returns(existingEpisodes); + + Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); + + _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); + _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); + _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().NotBeNull(); + _updatedEpisodes.First().AbsoluteEpisodeNumberAdded.Should().BeTrue(); + + _insertedEpisodes.Any(e => e.AbsoluteEpisodeNumberAdded).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 6496c5466..c4ad03da5 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -126,6 +126,7 @@ namespace NzbDrone.Core.Datastore .Ignore(e => e.SeriesTitle) .Ignore(e => e.Series) .Ignore(e => e.HasFile) + .Ignore(e => e.AbsoluteEpisodeNumberAdded) .HasOne(s => s.EpisodeFile, s => s.EpisodeFileId); Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel() diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs index 39a95db26..05384cf26 100644 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ b/src/NzbDrone.Core/Tv/Episode.cs @@ -46,6 +46,7 @@ namespace NzbDrone.Core.Tv public Series Series { get; set; } public bool HasFile => EpisodeFileId > 0; + public bool AbsoluteEpisodeNumberAdded { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Tv/EpisodeAddedService.cs b/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs similarity index 50% rename from src/NzbDrone.Core/Tv/EpisodeAddedService.cs rename to src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs index f49746e65..5d2e5c1e9 100644 --- a/src/NzbDrone.Core/Tv/EpisodeAddedService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -11,19 +11,19 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv { - public interface IEpisodeAddedService + public interface IEpisodeRefreshedService { - void SearchForRecentlyAdded(int seriesId); + void Search(int seriesId); } - public class EpisodeAddedService : IHandle<EpisodeInfoRefreshedEvent>, IEpisodeAddedService + public class EpisodeRefreshedService : IEpisodeRefreshedService, IHandle<EpisodeInfoRefreshedEvent> { private readonly IManageCommandQueue _commandQueueManager; private readonly IEpisodeService _episodeService; private readonly Logger _logger; - private readonly ICached<List<int>> _addedEpisodesCache; + private readonly ICached<List<int>> _searchCache; - public EpisodeAddedService(ICacheManager cacheManager, + public EpisodeRefreshedService(ICacheManager cacheManager, IManageCommandQueue commandQueueManager, IEpisodeService episodeService, Logger logger) @@ -31,12 +31,12 @@ namespace NzbDrone.Core.Tv _commandQueueManager = commandQueueManager; _episodeService = episodeService; _logger = logger; - _addedEpisodesCache = cacheManager.GetCache<List<int>>(GetType()); + _searchCache = cacheManager.GetCache<List<int>>(GetType()); } - public void SearchForRecentlyAdded(int seriesId) + public void Search(int seriesId) { - var previouslyAired = _addedEpisodesCache.Find(seriesId.ToString()); + var previouslyAired = _searchCache.Find(seriesId.ToString()); if (previouslyAired != null && previouslyAired.Any()) { @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Tv } } - _addedEpisodesCache.Remove(seriesId.ToString()); + _searchCache.Remove(seriesId.ToString()); } public void Handle(EpisodeInfoRefreshedEvent message) @@ -61,29 +61,39 @@ namespace NzbDrone.Core.Tv return; } - if (message.Added.Empty()) - { - _logger.Debug("No new episodes, skipping search"); - return; - } - - if (message.Added.None(a => a.AirDateUtc.HasValue)) - { - _logger.Debug("No new episodes have an air date"); - return; - } - - var previouslyAired = message.Added.Where(a => a.AirDateUtc.HasValue - && a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1)) - && a.Monitored).ToList(); + var previouslyAired = message.Added.Where(a => + a.AirDateUtc.HasValue && + a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1)) && + a.Monitored) + .ToList(); if (previouslyAired.Empty()) { _logger.Debug("Newly added episodes all air in the future"); - return; + _searchCache.Set(message.Series.Id.ToString(), previouslyAired.Select(e => e.Id).ToList()); } - _addedEpisodesCache.Set(message.Series.Id.ToString(), previouslyAired.Select(e => e.Id).ToList()); + var absoluteEpisodeNumberAdded = message.Updated.Where(a => + a.AbsoluteEpisodeNumberAdded && + a.AirDateUtc.HasValue && + a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1)) && + a.Monitored) + .ToList(); + + if (absoluteEpisodeNumberAdded.Empty()) + { + _logger.Debug("No updated episodes recently aired and had absolute episode number added"); + } + + var toSearch = new List<int>(); + + toSearch.AddRange(previouslyAired.Select(e => e.Id)); + toSearch.AddRange(absoluteEpisodeNumberAdded.Select(e => e.Id)); + + if (toSearch.Any()) + { + _searchCache.Set(message.Series.Id.ToString(), toSearch.Distinct().ToList()); + } } } } diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index 893e326e1..6b2b7e63f 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -59,6 +59,14 @@ namespace NzbDrone.Core.Tv { existingEpisodes.Remove(episodeToUpdate); updateList.Add(episodeToUpdate); + + // Anime series with newly added absolute episode number + if (series.SeriesType == SeriesTypes.Anime && + !episodeToUpdate.AbsoluteEpisodeNumber.HasValue && + episode.AbsoluteEpisodeNumber.HasValue) + { + episodeToUpdate.AbsoluteEpisodeNumberAdded = true; + } } else { diff --git a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs b/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs index f472099a9..60efcc6e5 100644 --- a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs +++ b/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Tv private readonly IEpisodeMonitoredService _episodeMonitoredService; private readonly ISeriesService _seriesService; private readonly IManageCommandQueue _commandQueueManager; - private readonly IEpisodeAddedService _episodeAddedService; + private readonly IEpisodeRefreshedService _episodeRefreshedService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -21,14 +21,14 @@ namespace NzbDrone.Core.Tv public SeriesScannedHandler(IEpisodeMonitoredService episodeMonitoredService, ISeriesService seriesService, IManageCommandQueue commandQueueManager, - IEpisodeAddedService episodeAddedService, + IEpisodeRefreshedService episodeRefreshedService, IEventAggregator eventAggregator, Logger logger) { _episodeMonitoredService = episodeMonitoredService; _seriesService = seriesService; _commandQueueManager = commandQueueManager; - _episodeAddedService = episodeAddedService; + _episodeRefreshedService = episodeRefreshedService; _eventAggregator = eventAggregator; _logger = logger; } @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Tv if (addOptions == null) { - _episodeAddedService.SearchForRecentlyAdded(series.Id); + _episodeRefreshedService.Search(series.Id); return; } From 4c170d045290127baa8424a73119458c1e330165 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 25 Feb 2024 16:32:32 -0800 Subject: [PATCH 141/762] New: Update anime episodes by season/episode number instead of absolute episode number Closes #6547 --- .../TvTests/RefreshEpisodeServiceFixture.cs | 109 ++---------------- src/NzbDrone.Core/Tv/RefreshEpisodeService.cs | 20 +--- 2 files changed, 13 insertions(+), 116 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 99189c053..7779dfb54 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -40,7 +40,6 @@ namespace NzbDrone.Core.Test.TvTests private Series GetSeries() { var series = _gameOfThrones.Item1.JsonClone(); - series.Seasons = new List<Season>(); return series; } @@ -49,7 +48,6 @@ namespace NzbDrone.Core.Test.TvTests { var series = Builder<Series>.CreateNew().Build(); series.SeriesType = SeriesTypes.Anime; - series.Seasons = new List<Season>(); return series; } @@ -178,34 +176,6 @@ namespace NzbDrone.Core.Test.TvTests ExceptionVerification.ExpectedWarns(1); } - [Test] - public void should_set_monitored_status_for_old_episodes_to_false_if_no_episodes_existed() - { - var series = GetSeries(); - series.Seasons = new List<Season>(); - - var episodes = GetEpisodes().OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).Take(4).ToList(); - - episodes[1].AirDateUtc = DateTime.UtcNow.AddDays(-15); - episodes[2].AirDateUtc = DateTime.UtcNow.AddDays(-10); - episodes[3].AirDateUtc = DateTime.UtcNow.AddDays(1); - - Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) - .Returns(new List<Episode>()); - - Subject.RefreshEpisodeInfo(series, episodes); - - _insertedEpisodes = _insertedEpisodes.OrderBy(v => v.EpisodeNumber).ToList(); - - _insertedEpisodes.Should().HaveSameCount(episodes); - _insertedEpisodes[0].Monitored.Should().Be(false); - _insertedEpisodes[1].Monitored.Should().Be(false); - _insertedEpisodes[2].Monitored.Should().Be(false); - _insertedEpisodes[3].Monitored.Should().Be(true); - - ExceptionVerification.ExpectedWarns(1); - } - [Test] public void should_remove_duplicate_remote_episodes_before_processing() { @@ -259,65 +229,6 @@ namespace NzbDrone.Core.Test.TvTests _deletedEpisodes.Should().BeEmpty(); } - [Test] - public void should_get_new_season_and_episode_numbers_when_absolute_episode_number_match_found() - { - const int expectedSeasonNumber = 10; - const int expectedEpisodeNumber = 5; - const int expectedAbsoluteNumber = 3; - - var episode = Builder<Episode>.CreateNew() - .With(e => e.SeasonNumber = expectedSeasonNumber) - .With(e => e.EpisodeNumber = expectedEpisodeNumber) - .With(e => e.AbsoluteEpisodeNumber = expectedAbsoluteNumber) - .Build(); - - var existingEpisode = episode.JsonClone(); - existingEpisode.SeasonNumber = 1; - existingEpisode.EpisodeNumber = 1; - existingEpisode.AbsoluteEpisodeNumber = expectedAbsoluteNumber; - - Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) - .Returns(new List<Episode> { existingEpisode }); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), new List<Episode> { episode }); - - _insertedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().BeEmpty(); - - _updatedEpisodes.First().SeasonNumber.Should().Be(expectedSeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(expectedEpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(expectedAbsoluteNumber); - } - - [Test] - public void should_prefer_absolute_match_over_season_and_epsiode_match() - { - var episodes = Builder<Episode>.CreateListOfSize(2) - .Build() - .ToList(); - - episodes[0].AbsoluteEpisodeNumber = null; - episodes[0].SeasonNumber.Should().NotBe(episodes[1].SeasonNumber); - episodes[0].EpisodeNumber.Should().NotBe(episodes[1].EpisodeNumber); - - var existingEpisode = new Episode - { - SeasonNumber = episodes[0].SeasonNumber, - EpisodeNumber = episodes[0].EpisodeNumber, - AbsoluteEpisodeNumber = episodes[1].AbsoluteEpisodeNumber - }; - - Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) - .Returns(new List<Episode> { existingEpisode }); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); - } - [Test] public void should_ignore_episodes_with_no_absolute_episode_in_distinct_by_absolute() { @@ -427,14 +338,14 @@ namespace NzbDrone.Core.Test.TvTests } [Test] - public void should_prefer_regular_season_when_absolute_numbers_conflict() + public void should_match_anime_episodes_by_season_and_episode_numbers() { var episodes = Builder<Episode>.CreateListOfSize(2) - .Build() - .ToList(); + .Build() + .ToList(); - episodes[0].AbsoluteEpisodeNumber = episodes[1].AbsoluteEpisodeNumber; - episodes[0].SeasonNumber = 0; + episodes[0].AbsoluteEpisodeNumber = null; + episodes[0].SeasonNumber.Should().NotBe(episodes[1].SeasonNumber); episodes[0].EpisodeNumber.Should().NotBe(episodes[1].EpisodeNumber); var existingEpisode = new Episode @@ -449,9 +360,13 @@ namespace NzbDrone.Core.Test.TvTests Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); + _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[0].SeasonNumber); + _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[0].EpisodeNumber); + _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[0].AbsoluteEpisodeNumber); + + _insertedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); + _insertedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); + _insertedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); } [Test] diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index 6b2b7e63f..708a5f4c9 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Tv { try { - var episodeToUpdate = GetEpisodeToUpdate(series, episode, existingEpisodes); + var episodeToUpdate = existingEpisodes.FirstOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); if (episodeToUpdate != null) { @@ -235,24 +235,6 @@ namespace NzbDrone.Core.Tv .ToList(); } - private Episode GetEpisodeToUpdate(Series series, Episode episode, List<Episode> existingEpisodes) - { - if (series.SeriesType == SeriesTypes.Anime) - { - if (episode.AbsoluteEpisodeNumber.HasValue) - { - var matchingEpisode = existingEpisodes.FirstOrDefault(e => e.AbsoluteEpisodeNumber == episode.AbsoluteEpisodeNumber); - - if (matchingEpisode != null) - { - return matchingEpisode; - } - } - } - - return existingEpisodes.FirstOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); - } - private IEnumerable<Episode> OrderEpisodes(Series series, List<Episode> episodes) { if (series.SeriesType == SeriesTypes.Anime) From b34e0f8259b8e1f0b5bdf6bc9a891350ffbad273 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 26 Feb 2024 21:33:07 -0800 Subject: [PATCH 142/762] Fixed: Ignore language in split episode title --- .../NormalizeEpisodeTitleFixture.cs | 23 +++++++++++++++++++ ...ture.cs => NormalizeSeriesTitleFixture.cs} | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core.Test/ParserTests/NormalizeEpisodeTitleFixture.cs rename src/NzbDrone.Core.Test/ParserTests/{NormalizeTitleFixture.cs => NormalizeSeriesTitleFixture.cs} (98%) diff --git a/src/NzbDrone.Core.Test/ParserTests/NormalizeEpisodeTitleFixture.cs b/src/NzbDrone.Core.Test/ParserTests/NormalizeEpisodeTitleFixture.cs new file mode 100644 index 000000000..1f94af578 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/NormalizeEpisodeTitleFixture.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class NormalizeEpisodeTitleFixture : CoreTest + { + [TestCase("Episode Title", "episode title")] + [TestCase("A.B,C;", "a b c")] + [TestCase("Episode Title", "episode title")] + [TestCase("French Title (1)", "french title")] + [TestCase("Series.Title.S01.Special.Episode.Title.720p.HDTV.x264-Sonarr", "episode title")] + [TestCase("Series.Title.S01E00.Episode.Title.720p.HDTV.x264-Sonarr", "episode title")] + public void should_normalize_episode_title(string input, string expected) + { + var result = Parser.Parser.NormalizeEpisodeTitle(input); + + result.Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs similarity index 98% rename from src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs rename to src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs index 8131efcd1..4c9b683b6 100644 --- a/src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ParserTests { [TestFixture] - public class NormalizeTitleFixture : CoreTest + public class NormalizeSeriesTitleFixture : CoreTest { [TestCase("Series", "series")] [TestCase("Series (2009)", "series2009")] diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index fd040a17b..421ca70cf 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -541,6 +541,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex TitleComponentsRegex = new Regex(@"^(?:(?<title>.+?) \((?<title>.+?)\)|(?<title>.+?) \| (?<title>.+?))$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex PartRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled); private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); 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); @@ -792,6 +793,7 @@ namespace NzbDrone.Core.Parser // Disabled, Until we run into specific testcases for the removal of these words. // title = SpecialEpisodeWordRegex.Replace(title, string.Empty); + title = PartRegex.Replace(title, ""); title = PunctuationRegex.Replace(title, " "); title = DuplicateSpacesRegex.Replace(title, " "); From 1f97679868012b70beecc553557e96e6c8bc80e3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 23 Feb 2024 05:30:37 +0200 Subject: [PATCH 143/762] Fixed: Selection of last added custom filter Plus some translations and typos --- .../Builder/FilterBuilderModalContent.js | 18 +++++++++++------- .../Filter/CustomFilters/CustomFilter.js | 4 ++-- src/NzbDrone.Core/Localization/Core/en.json | 4 ++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js index d718aab0c..0c4a31657 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,3 +1,4 @@ +import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -8,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import FilterBuilderRow from './FilterBuilderRow'; import styles from './FilterBuilderModalContent.css'; @@ -49,7 +51,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = customFilters[customFilters.length -1]; + const last = maxBy(customFilters, 'id'); dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -107,7 +109,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: 'Label is required' + message: translate('LabelIsRequired') } ] }); @@ -145,13 +147,13 @@ class FilterBuilderModalContent extends Component { return ( <ModalContent onModalClose={onModalClose}> <ModalHeader> - Custom Filter + {translate('CustomFilter')} </ModalHeader> <ModalBody> <div className={styles.labelContainer}> <div className={styles.label}> - Label + {translate('Label')} </div> <div className={styles.labelInputContainer}> @@ -165,7 +167,9 @@ class FilterBuilderModalContent extends Component { </div> </div> - <div className={styles.label}>Filters</div> + <div className={styles.label}> + {translate('Filters')} + </div> <div className={styles.rows}> { @@ -192,7 +196,7 @@ class FilterBuilderModalContent extends Component { <ModalFooter> <Button onPress={onCancelPress}> - Cancel + {translate('Cancel')} </Button> <SpinnerErrorButton @@ -200,7 +204,7 @@ class FilterBuilderModalContent extends Component { error={saveError} onPress={this.onSaveFilterPress} > - Save + {translate('Save')} </SpinnerErrorButton> </ModalFooter> </ModalContent> diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 7407f729a..9f378d5a2 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -37,8 +37,8 @@ class CustomFilter extends Component { dispatchSetFilter } = this.props; - // Assume that delete and then unmounting means the delete was successful. - // Moving this check to a ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the deletion was successful. + // Moving this check to an ancestor would be more accurate, but would have // more boilerplate. if (this.state.isDeleting && id === selectedFilterKey) { dispatchSetFilter({ selectedFilterKey: 'all' }); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 8bc21c8e8..e7f80c263 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -266,6 +266,7 @@ "CreateGroup": "Create Group", "CurrentlyInstalled": "Currently Installed", "Custom": "Custom", + "CustomFilter": "Custom Filter", "CustomFilters": "Custom Filters", "CustomFormat": "Custom Format", "CustomFormatHelpText": "{appName} scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then {appName} will grab it.", @@ -696,6 +697,7 @@ "FilterNotInNext": "not in the next", "FilterSeriesPlaceholder": "Filter series", "FilterStartsWith": "starts with", + "Filters": "Filters", "FinaleTooltip": "Series or season finale", "FirstDayOfWeek": "First Day of Week", "Fixed": "Fixed", @@ -1026,6 +1028,8 @@ "KeyboardShortcutsFocusSearchBox": "Focus Search Box", "KeyboardShortcutsOpenModal": "Open This Modal", "KeyboardShortcutsSaveSettings": "Save Settings", + "Label": "Label", + "LabelIsRequired": "Label is required", "Language": "Language", "Languages": "Languages", "LanguagesLoadError": "Unable to load languages", From fb6fc568c5f5e651ef387bd2404d982e037a1005 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 27 Feb 2024 07:20:23 -0800 Subject: [PATCH 144/762] Fixed: Don't store seasons from import list items in database Closes #6555 --- src/NzbDrone.Core/Datastore/TableMapping.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index c4ad03da5..26fa02c4b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -82,7 +82,8 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.Enable); Mapper.Entity<ImportListItemInfo>("ImportListItems").RegisterModel() - .Ignore(i => i.ImportList); + .Ignore(i => i.ImportList) + .Ignore(i => i.Seasons); Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel() .Ignore(x => x.ImplementationName) From fa600e62e00ad9dba6881a080af7c75865fb1989 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 28 Feb 2024 06:42:08 +0200 Subject: [PATCH 145/762] Fixed: Error when download client information is unavailable for Manual Interaction Required event Closes #6558 (cherry picked from commit 173b1d6a4c0f2125c4413c0c09b269d87a1f1ee8) Co-authored-by: Qstick <qstick@gmail.com> --- src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs | 4 ++-- .../Notifications/ManualInteractionRequiredMessage.cs | 4 ++-- src/NzbDrone.Core/Notifications/NotificationService.cs | 3 +-- src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index a0902c1fc..ec2974b54 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -351,8 +351,8 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); - environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientName ?? string.Empty); - environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); + environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientInfo?.Name ?? string.Empty); + environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientInfo?.Type ?? string.Empty); environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); environmentVariables.Add("Sonarr_Download_Size", message.TrackedDownload.DownloadItem.TotalSize.ToString()); environmentVariables.Add("Sonarr_Download_Title", message.TrackedDownload.DownloadItem.Title); diff --git a/src/NzbDrone.Core/Notifications/ManualInteractionRequiredMessage.cs b/src/NzbDrone.Core/Notifications/ManualInteractionRequiredMessage.cs index 0d9f8f6b8..e8745fdf7 100644 --- a/src/NzbDrone.Core/Notifications/ManualInteractionRequiredMessage.cs +++ b/src/NzbDrone.Core/Notifications/ManualInteractionRequiredMessage.cs @@ -1,3 +1,4 @@ +using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -12,8 +13,7 @@ namespace NzbDrone.Core.Notifications public RemoteEpisode Episode { get; set; } public TrackedDownload TrackedDownload { get; set; } public QualityModel Quality { get; set; } - public string DownloadClientType { get; set; } - public string DownloadClientName { get; set; } + public DownloadClientItemClientInfo DownloadClientInfo { get; set; } public string DownloadId { get; set; } public GrabbedReleaseInfo Release { get; set; } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 83966433c..1dbd1fe9d 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -240,8 +240,7 @@ namespace NzbDrone.Core.Notifications Quality = message.Episode.ParsedEpisodeInfo.Quality, Episode = message.Episode, TrackedDownload = message.TrackedDownload, - DownloadClientType = message.TrackedDownload.DownloadItem.DownloadClientInfo.Type, - DownloadClientName = message.TrackedDownload.DownloadItem.DownloadClientInfo.Name, + DownloadClientInfo = message.TrackedDownload.DownloadItem.DownloadClientInfo, DownloadId = message.TrackedDownload.DownloadItem.DownloadId, Release = message.Release }; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index 2214a1f03..a5a1dec6d 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -175,8 +175,8 @@ namespace NzbDrone.Core.Notifications.Webhook Series = new WebhookSeries(message.Series), Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)), DownloadInfo = new WebhookDownloadClientItem(quality, message.TrackedDownload.DownloadItem), - DownloadClient = message.DownloadClientName, - DownloadClientType = message.DownloadClientType, + DownloadClient = message.DownloadClientInfo?.Name, + DownloadClientType = message.DownloadClientInfo?.Type, DownloadId = message.DownloadId, CustomFormatInfo = new WebhookCustomFormatInfo(remoteEpisode.CustomFormats, remoteEpisode.CustomFormatScore), Release = new WebhookGrabbedRelease(message.Release) From 16d3827dbd3a50a46eb9660bf2aec83e627ebd96 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 27 Feb 2024 16:56:59 -0800 Subject: [PATCH 146/762] Fixed: Processing updated episodes in series after refresh Closes #6560 --- src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs | 12 +++++++----- .../Tv/Events/EpisodeInfoRefreshedEvent.cs | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs b/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs index 5d2e5c1e9..0b535dc25 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRefreshedService.cs @@ -55,6 +55,8 @@ namespace NzbDrone.Core.Tv { if (message.Series.AddOptions == null) { + var toSearch = new List<int>(); + if (!message.Series.Monitored) { _logger.Debug("Series is not monitored"); @@ -65,19 +67,22 @@ namespace NzbDrone.Core.Tv a.AirDateUtc.HasValue && a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1)) && a.Monitored) + .Select(e => e.Id) .ToList(); if (previouslyAired.Empty()) { _logger.Debug("Newly added episodes all air in the future"); - _searchCache.Set(message.Series.Id.ToString(), previouslyAired.Select(e => e.Id).ToList()); } + toSearch.AddRange(previouslyAired); + var absoluteEpisodeNumberAdded = message.Updated.Where(a => a.AbsoluteEpisodeNumberAdded && a.AirDateUtc.HasValue && a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1)) && a.Monitored) + .Select(e => e.Id) .ToList(); if (absoluteEpisodeNumberAdded.Empty()) @@ -85,10 +90,7 @@ namespace NzbDrone.Core.Tv _logger.Debug("No updated episodes recently aired and had absolute episode number added"); } - var toSearch = new List<int>(); - - toSearch.AddRange(previouslyAired.Select(e => e.Id)); - toSearch.AddRange(absoluteEpisodeNumberAdded.Select(e => e.Id)); + toSearch.AddRange(absoluteEpisodeNumberAdded); if (toSearch.Any()) { diff --git a/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs b/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs index 090ea270e..6a3f24b65 100644 --- a/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs +++ b/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using NzbDrone.Common.Messaging; @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Tv.Events { Series = series; Added = new ReadOnlyCollection<Episode>(added); + Updated = new ReadOnlyCollection<Episode>(updated); Removed = new ReadOnlyCollection<Episode>(removed); } } From 236d8e4c50d130993f6dad41d596c281cca67c8d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 28 Feb 2024 04:40:39 +0000 Subject: [PATCH 147/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/pt_BR.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 4f8f94413..8c1394231 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1123,7 +1123,7 @@ "KeyboardShortcutsFocusSearchBox": "Selecionar a caixa de pesquisa", "KeyboardShortcutsSaveSettings": "Salvar configurações", "LocalStorageIsNotSupported": "O armazenamento local não é compatível ou está desabilitado. Um plugin ou a navegação privada pode tê-lo desativado.", - "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis ao executar como um serviço do Windows, consulte as [Perguntas frequentes](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote -server) para saber mais.", + "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis quando executadas como um serviço do Windows. Consulte as [FAQ]({url}) para obter mais informações.", "AirsTbaOn": "A ser anunciado em {networkLabel}", "AirsTimeOn": "{time} em {networkLabel}", "AirsTomorrowOn": "Amanhã às {time} em {networkLabel}", @@ -2044,5 +2044,7 @@ "SetIndexerFlagsModalTitle": "{modalTitle} - Definir Sinalizadores do Indexador", "CustomFormatsSpecificationFlag": "Sinalizador", "IndexerFlags": "Sinalizadores do Indexador", - "SetIndexerFlags": "Definir Sinalizadores de Indexador" + "SetIndexerFlags": "Definir Sinalizadores de Indexador", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado" } From 0a84b4a8e9615416ce71bcd2740d6b5af496cdee Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 29 Feb 2024 10:58:40 +0000 Subject: [PATCH 148/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/pt_BR.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8c1394231..8f5e7e63a 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2046,5 +2046,9 @@ "IndexerFlags": "Sinalizadores do Indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", - "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado" + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado", + "CustomFilter": "Filtro Personalizado", + "Filters": "Filtros", + "Label": "Rótulo", + "LabelIsRequired": "Rótulo é requerido" } From 2773f77e1c4e3a8c8d01bcbea67333801c7840df Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 29 Feb 2024 02:16:46 +0200 Subject: [PATCH 149/762] New: Options button for Missing/Cutoff Unmet --- frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js | 11 +++++++++++ frontend/src/Wanted/Missing/Missing.js | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 29c925702..3b2703de1 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import getFilterValue from 'Utilities/Filter/getFilterValue'; @@ -180,6 +181,16 @@ class CutoffUnmet extends Component { </PageToolbarSection> <PageToolbarSection alignContent={align.RIGHT}> + <TableOptionsModalWrapper + {...otherProps} + columns={columns} + > + <PageToolbarButton + label={translate('Options')} + iconName={icons.TABLE} + /> + </TableOptionsModalWrapper> + <FilterMenu alignMenu={align.RIGHT} selectedFilterKey={selectedFilterKey} diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js index 70054a73b..734b7e6c5 100644 --- a/frontend/src/Wanted/Missing/Missing.js +++ b/frontend/src/Wanted/Missing/Missing.js @@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; @@ -193,6 +194,16 @@ class Missing extends Component { </PageToolbarSection> <PageToolbarSection alignContent={align.RIGHT}> + <TableOptionsModalWrapper + {...otherProps} + columns={columns} + > + <PageToolbarButton + label={translate('Options')} + iconName={icons.TABLE} + /> + </TableOptionsModalWrapper> + <FilterMenu alignMenu={align.RIGHT} selectedFilterKey={selectedFilterKey} From 64f4365fe98b569efdf436710d5f56684f2aab66 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 29 Feb 2024 02:35:49 +0200 Subject: [PATCH 150/762] Update caniuse-lite --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe5804ff8..9c7bf1ffa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2276,9 +2276,9 @@ camelcase@^5.3.1: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517: - version "1.0.30001525" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz#d2e8fdec6116ffa36284ca2c33ef6d53612fe1c8" - integrity sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q== + version "1.0.30001591" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz" + integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" From 9fd193d2a82d5c2cdc0f36c1f984e4b6b68aaa8d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 27 Feb 2024 21:03:35 -0800 Subject: [PATCH 151/762] New: URL Base setting for Media Server connections Closes #4416 --- src/NzbDrone.Core/Localization/Core/en.json | 1 + .../MediaBrowser/MediaBrowserService.cs | 4 ++-- .../MediaBrowser/MediaBrowserSettings.cs | 18 ++++++++++++------ .../Plex/Server/PlexServerProxy.cs | 2 +- .../Plex/Server/PlexServerSettings.cs | 15 ++++++++++----- .../Notifications/Xbmc/XbmcSettings.cs | 19 ++++++++++++------- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index e7f80c263..90de21e06 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -247,6 +247,7 @@ "ConnectionLostReconnect": "{appName} will try to connect automatically, or you can click reload below.", "ConnectionLostToBackend": "{appName} has lost its connection to the backend and will need to be reloaded to restore functionality.", "Connections": "Connections", + "ConnectionSettingsUrlBaseHelpText": "Adds a prefix to the {connectionName} url, such as {url}", "Continuing": "Continuing", "ContinuingOnly": "Continuing Only", "ContinuingSeriesDescription": "More episodes/another season is expected", diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs index a89c2180f..a8a535b05 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using FluentValidation.Results; @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Notifications.Emby { try { - _logger.Debug("Testing connection to MediaBrowser: {0}", settings.Address); + _logger.Debug("Testing connection to Emby/Jellyfin : {0}", settings.Address); Notify(settings, "Test from Sonarr", "Success! MediaBrowser has been successfully configured!"); } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index 237ac9998..1d04e9054 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Emby RuleFor(c => c.ApiKey).NotEmpty(); RuleFor(c => c.MapFrom).NotEmpty().Unless(c => c.MapTo.IsNullOrWhiteSpace()); RuleFor(c => c.MapTo).NotEmpty().Unless(c => c.MapFrom.IsNullOrWhiteSpace()); + RuleFor(c => c.UrlBase).ValidUrlBase(); } } @@ -37,25 +38,30 @@ namespace NzbDrone.Core.Notifications.Emby [FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Emby/Jellyfin")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Emby/Jellyfin")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/mediabrowser")] + public string UrlBase { get; set; } + + [FieldDefinition(4, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "NotificationsEmbySettingsSendNotifications", HelpText = "NotificationsEmbySettingsSendNotificationsHelpText", Type = FieldType.Checkbox)] + [FieldDefinition(5, Label = "NotificationsEmbySettingsSendNotifications", HelpText = "NotificationsEmbySettingsSendNotificationsHelpText", Type = FieldType.Checkbox)] public bool Notify { get; set; } - [FieldDefinition(5, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsEmbySettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] + [FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsEmbySettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(6, Label = "NotificationsSettingsUpdateMapPathsFrom", HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText", Type = FieldType.Textbox, Advanced = true)] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Emby/Jellyfin")] public string MapFrom { get; set; } - [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsTo", HelpText = "NotificationsSettingsUpdateMapPathsToHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", HelpText = "NotificationsSettingsUpdateMapPathsToHelpText", Type = FieldType.Textbox, Advanced = true)] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Emby/Jellyfin")] public string MapTo { get; set; } [JsonIgnore] - public string Address => $"{Host.ToUrlHost()}:{Port}"; + public string Address => $"{Host.ToUrlHost()}:{Port}{UrlBase}"; public bool IsValid => !string.IsNullOrWhiteSpace(Host) && Port > 0; diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs index 1c3e79a9a..c3dd681f2 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -94,7 +94,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server { var scheme = settings.UseSsl ? "https" : "http"; - var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host.ToUrlHost()}:{settings.Port}") + var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host.ToUrlHost()}:{settings.Port}{settings.UrlBase}") .Accept(HttpAccept.Json) .AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier) .AddQueryParam("X-Plex-Product", BuildInfo.AppName) diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index ff6fd7789..de76f9eef 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -38,20 +38,25 @@ namespace NzbDrone.Core.Notifications.Plex.Server [FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Plex")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Plex")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/plex")] + public string UrlBase { get; set; } + + [FieldDefinition(4, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)] public string AuthToken { get; set; } - [FieldDefinition(4, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)] + [FieldDefinition(5, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)] public string SignIn { get; set; } - [FieldDefinition(5, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)] + [FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(6, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")] + [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Plex")] public string MapFrom { get; set; } - [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Plex")] public string MapTo { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs index 94652a587..2d54157f2 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs @@ -37,26 +37,31 @@ namespace NzbDrone.Core.Notifications.Xbmc [FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Kodi")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Username", Privacy = PrivacyLevel.UserName)] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Kodi")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/kodi")] + public string UrlBase { get; set; } + + [FieldDefinition(4, Label = "Username", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] + [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } [DefaultValue(5)] - [FieldDefinition(5, Label = "NotificationsKodiSettingsDisplayTime", HelpText = "NotificationsKodiSettingsDisplayTimeHelpText")] + [FieldDefinition(6, Label = "NotificationsKodiSettingsDisplayTime", HelpText = "NotificationsKodiSettingsDisplayTimeHelpText")] public int DisplayTime { get; set; } - [FieldDefinition(6, Label = "NotificationsKodiSettingsGuiNotification", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "NotificationsKodiSettingsGuiNotification", Type = FieldType.Checkbox)] public bool Notify { get; set; } - [FieldDefinition(7, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsKodiSettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsKodiSettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(8, Label = "NotificationsKodiSettingsCleanLibrary", HelpText = "NotificationsKodiSettingsCleanLibraryHelpText", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "NotificationsKodiSettingsCleanLibrary", HelpText = "NotificationsKodiSettingsCleanLibraryHelpText", Type = FieldType.Checkbox)] public bool CleanLibrary { get; set; } - [FieldDefinition(9, Label = "NotificationsKodiSettingAlwaysUpdate", HelpText = "NotificationsKodiSettingAlwaysUpdateHelpText", Type = FieldType.Checkbox)] + [FieldDefinition(10, Label = "NotificationsKodiSettingAlwaysUpdate", HelpText = "NotificationsKodiSettingAlwaysUpdateHelpText", Type = FieldType.Checkbox)] public bool AlwaysUpdate { get; set; } [JsonIgnore] From c99d81e79ba5e6ecec01ddd942440d8a48a1c23b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 26 Feb 2024 20:56:59 -0800 Subject: [PATCH 152/762] New: Bypass archived history for failed downloads in SABnzbd --- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 4 ++-- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 8beaa97a2..b0a4e2495 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -205,11 +205,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd DeleteItemData(item); } - _proxy.RemoveFrom("history", item.DownloadId, deleteData, Settings); + _proxy.RemoveFromHistory(item.DownloadId, deleteData, item.Status == DownloadItemStatus.Failed, Settings); } else { - _proxy.RemoveFrom("queue", item.DownloadId, deleteData, Settings); + _proxy.RemoveFromQueue(item.DownloadId, deleteData, Settings); } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index da0f8f2aa..73736fbbc 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -14,7 +14,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { string GetBaseUrl(SabnzbdSettings settings, string relativePath = null); SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings); - void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings); + void RemoveFromQueue(string id, bool deleteData, SabnzbdSettings settings); + void RemoveFromHistory(string id, bool deleteData, bool deletePermanently, SabnzbdSettings settings); string GetVersion(SabnzbdSettings settings); SabnzbdConfig GetConfig(SabnzbdSettings settings); SabnzbdFullStatus GetFullStatus(SabnzbdSettings settings); @@ -60,9 +61,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return response; } - public void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings) + public void RemoveFromQueue(string id, bool deleteData, SabnzbdSettings settings) { - var request = BuildRequest(source, settings); + var request = BuildRequest("queue", settings); request.AddQueryParam("name", "delete"); request.AddQueryParam("del_files", deleteData ? 1 : 0); request.AddQueryParam("value", id); @@ -70,6 +71,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd ProcessRequest(request, settings); } + public void RemoveFromHistory(string id, bool deleteData, bool deletePermanently, SabnzbdSettings settings) + { + var request = BuildRequest("history", settings); + request.AddQueryParam("name", "delete"); + request.AddQueryParam("del_files", deleteData ? 1 : 0); + request.AddQueryParam("value", id); + request.AddQueryParam("archive", deletePermanently ? 0 : 1); + + ProcessRequest(request, settings); + } + public string GetVersion(SabnzbdSettings settings) { var request = BuildRequest("version", settings); From f8a0751775a696b32693cdc368aa33a7a91e954a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 28 Feb 2024 21:38:22 -0800 Subject: [PATCH 153/762] New: Release Type (Single/Multi episode and Season Pack) for Custom Formats Closes #3562 --- .../Migration/203_release_typeFixture.cs | 191 ++++++++++++++++++ .../ImportApprovedEpisodesFixture.cs | 4 +- src/NzbDrone.Core/Blocklisting/Blocklist.cs | 1 + .../Blocklisting/BlocklistService.cs | 5 + .../CustomFormatCalculationService.cs | 16 +- .../CustomFormats/CustomFormatInput.cs | 1 + .../IndexerFlagSpecification.cs | 6 +- .../Specifications/SeasonPackSpecification.cs | 43 ++++ .../Datastore/Migration/203_release_type.cs | 58 ++++++ src/NzbDrone.Core/History/HistoryService.cs | 5 + src/NzbDrone.Core/Localization/Core/en.json | 1 + src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 20 ++ .../EpisodeImport/ImportDecisionMaker.cs | 4 + .../EpisodeImport/Manual/ManualImportFile.cs | 2 + .../EpisodeImport/Manual/ManualImportItem.cs | 2 + .../Manual/ManualImportService.cs | 4 + .../Parser/Model/LocalEpisode.cs | 1 + .../Parser/Model/ParsedEpisodeInfo.cs | 23 +++ src/NzbDrone.Core/Parser/Model/ReleaseType.cs | 18 ++ .../EpisodeFiles/EpisodeFileController.cs | 5 + .../EpisodeFiles/EpisodeFileResource.cs | 32 +-- .../ManualImport/ManualImportResource.cs | 3 + 23 files changed, 408 insertions(+), 38 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/203_release_type.cs create mode 100644 src/NzbDrone.Core/Parser/Model/ReleaseType.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs new file mode 100644 index 000000000..9010ba27d --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class release_typeFixture : MigrationTest<release_type> + { + [Test] + public void should_convert_single_episode_without_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode); + } + + [Test] + public void should_convert_single_episode_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr/S01E05.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode); + } + + [Test] + public void should_convert_multi_episode_without_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode); + } + + [Test] + public void should_convert_multi_episode_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr/S01E05E06.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode); + } + + [Test] + public void should_convert_season_pack_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01.720p.HDTV.x265-Sonarr/S01E05.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SeasonPack); + } + + [Test] + public void should_not_convert_episode_without_original_file_path() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.Unknown); + } + + public class EpisodeFile203 + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int SeasonNumber { get; set; } + public string RelativePath { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string OriginalFilePath { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public long IndexerFlags { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public List<int> Languages { get; set; } + public long ReleaseType { get; set; } + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index 9020601ff..179cf5b3f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -49,6 +49,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); + _rejectedDecisions.ForEach(r => r.LocalEpisode.FileEpisodeInfo = new ParsedEpisodeInfo()); foreach (var episode in episodes) { @@ -59,7 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Episodes = new List<Episode> { episode }, Path = Path.Combine(series.Path, "30 Rock - S01E01 - Pilot.avi"), Quality = new QualityModel(Quality.Bluray720p), - ReleaseGroup = "DRONE" + ReleaseGroup = "DRONE", + FileEpisodeInfo = new ParsedEpisodeInfo() })); } diff --git a/src/NzbDrone.Core/Blocklisting/Blocklist.cs b/src/NzbDrone.Core/Blocklisting/Blocklist.cs index 4fdc4f24c..8941f42fd 100644 --- a/src/NzbDrone.Core/Blocklisting/Blocklist.cs +++ b/src/NzbDrone.Core/Blocklisting/Blocklist.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Blocklisting public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } public IndexerFlags IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } public List<Language> Languages { get; set; } diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 0015f7ea2..0ec53522c 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -194,6 +194,11 @@ namespace NzbDrone.Core.Blocklisting blocklist.IndexerFlags = flags; } + if (Enum.TryParse(message.Data.GetValueOrDefault("releaseType"), true, out ReleaseType releaseType)) + { + blocklist.ReleaseType = releaseType; + } + _blocklistRepository.Insert(blocklist); } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index c07db977e..1f0cb296b 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -41,7 +41,8 @@ namespace NzbDrone.Core.CustomFormats Series = remoteEpisode.Series, Size = size, Languages = remoteEpisode.Languages, - IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0 + IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0, + ReleaseType = remoteEpisode.ParsedEpisodeInfo.ReleaseType }; return ParseCustomFormat(input); @@ -76,7 +77,8 @@ namespace NzbDrone.Core.CustomFormats Series = series, Size = blocklist.Size ?? 0, Languages = blocklist.Languages, - IndexerFlags = blocklist.IndexerFlags + IndexerFlags = blocklist.IndexerFlags, + ReleaseType = blocklist.ReleaseType }; return ParseCustomFormat(input); @@ -88,6 +90,7 @@ namespace NzbDrone.Core.CustomFormats long.TryParse(history.Data.GetValueOrDefault("size"), out var size); Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags); + Enum.TryParse(history.Data.GetValueOrDefault("releaseType"), out ReleaseType releaseType); var episodeInfo = new ParsedEpisodeInfo { @@ -104,7 +107,8 @@ namespace NzbDrone.Core.CustomFormats Series = series, Size = size, Languages = history.Languages, - IndexerFlags = indexerFlags + IndexerFlags = indexerFlags, + ReleaseType = releaseType }; return ParseCustomFormat(input); @@ -128,6 +132,7 @@ namespace NzbDrone.Core.CustomFormats Size = localEpisode.Size, Languages = localEpisode.Languages, IndexerFlags = localEpisode.IndexerFlags, + ReleaseType = localEpisode.ReleaseType, Filename = Path.GetFileName(localEpisode.Path) }; @@ -188,7 +193,7 @@ namespace NzbDrone.Core.CustomFormats ReleaseTitle = releaseTitle, Quality = episodeFile.Quality, Languages = episodeFile.Languages, - ReleaseGroup = episodeFile.ReleaseGroup + ReleaseGroup = episodeFile.ReleaseGroup, }; var input = new CustomFormatInput @@ -198,7 +203,8 @@ namespace NzbDrone.Core.CustomFormats Size = episodeFile.Size, Languages = episodeFile.Languages, IndexerFlags = episodeFile.IndexerFlags, - Filename = Path.GetFileName(episodeFile.RelativePath) + ReleaseType = episodeFile.ReleaseType, + Filename = Path.GetFileName(episodeFile.RelativePath), }; return ParseCustomFormat(input, allCustomFormats); diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs index e202ffccf..465fbfef5 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.CustomFormats public IndexerFlags IndexerFlags { get; set; } public List<Language> Languages { get; set; } public string Filename { get; set; } + public ReleaseType ReleaseType { get; set; } public CustomFormatInput() { diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs index 56f73f8b9..3eaeeb5f6 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -11,11 +11,11 @@ namespace NzbDrone.Core.CustomFormats public IndexerFlagSpecificationValidator() { RuleFor(c => c.Value).NotEmpty(); - RuleFor(c => c.Value).Custom((qualityValue, context) => + RuleFor(c => c.Value).Custom((flag, context) => { - if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue)) + if (!Enum.IsDefined(typeof(IndexerFlags), flag)) { - context.AddFailure($"Invalid indexer flag condition value: {qualityValue}"); + context.AddFailure($"Invalid indexer flag condition value: {flag}"); } }); } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs new file mode 100644 index 000000000..acc6d9c4d --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs @@ -0,0 +1,43 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class SeasonPackSpecificationValidator : AbstractValidator<SeasonPackSpecification> + { + public SeasonPackSpecificationValidator() + { + RuleFor(c => c.Value).Custom((releaseType, context) => + { + if (!Enum.IsDefined(typeof(ReleaseType), releaseType)) + { + context.AddFailure($"Invalid release type condition value: {releaseType}"); + } + }); + } + } + + public class SeasonPackSpecification : CustomFormatSpecificationBase + { + private static readonly SeasonPackSpecificationValidator Validator = new (); + + public override int Order => 10; + public override string ImplementationName => "Release Type"; + + [FieldDefinition(1, Label = "ReleaseType", Type = FieldType.Select, SelectOptions = typeof(ReleaseType))] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + return input.ReleaseType == (ReleaseType)Value; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs b/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs new file mode 100644 index 000000000..cca7a7fa5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs @@ -0,0 +1,58 @@ +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(203)] + public class release_type : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blocklist").AddColumn("ReleaseType").AsInt32().WithDefaultValue(0); + Alter.Table("EpisodeFiles").AddColumn("ReleaseType").AsInt32().WithDefaultValue(0); + + Execute.WithConnection(UpdateEpisodeFiles); + } + + private void UpdateEpisodeFiles(IDbConnection conn, IDbTransaction tran) + { + var updates = new List<object>(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"OriginalFilePath\" FROM \"EpisodeFiles\" WHERE \"OriginalFilePath\" IS NOT NULL"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var id = reader.GetInt32(0); + var originalFilePath = reader.GetString(1); + + var folderName = Path.GetDirectoryName(originalFilePath); + var fileName = Path.GetFileNameWithoutExtension(originalFilePath); + var title = folderName.IsNullOrWhiteSpace() ? fileName : folderName; + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + + if (parsedEpisodeInfo != null && parsedEpisodeInfo.ReleaseType != ReleaseType.Unknown) + { + updates.Add(new + { + Id = id, + ReleaseType = (int)parsedEpisodeInfo.ReleaseType + }); + } + } + } + + var updateEpisodeFilesSql = "UPDATE \"EpisodeFiles\" SET \"ReleaseType\" = @ReleaseType WHERE \"Id\" = @Id"; + conn.Execute(updateEpisodeFilesSql, updates, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index df2788762..d7e76d94f 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -170,6 +170,7 @@ namespace NzbDrone.Core.History history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString()); history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString()); history.Data.Add("IndexerFlags", message.Episode.Release.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.Episode.ParsedEpisodeInfo.ReleaseType.ToString()); if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) { @@ -222,6 +223,7 @@ namespace NzbDrone.Core.History history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString()); history.Data.Add("Size", message.EpisodeInfo.Size.ToString()); history.Data.Add("IndexerFlags", message.ImportedEpisode.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.ImportedEpisode.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -283,6 +285,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.EpisodeFile.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -315,6 +318,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.EpisodeFile.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -343,6 +347,7 @@ namespace NzbDrone.Core.History history.Data.Add("Message", message.Message); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup); history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString()); + history.Data.Add("ReleaseType", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseType.ToString()); historyToAdd.Add(history); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 90de21e06..40cc2f581 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1597,6 +1597,7 @@ "ReleaseSceneIndicatorUnknownMessage": "Numbering varies for this episode and release does not match any known mappings.", "ReleaseSceneIndicatorUnknownSeries": "Unknown episode or series.", "ReleaseTitle": "Release Title", + "ReleaseType": "Release Type", "Reload": "Reload", "RemotePath": "Remote Path", "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index a02fdd44a..8dee12c2b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Core.MediaFiles public LazyLoaded<List<Episode>> Episodes { get; set; } public LazyLoaded<Series> Series { get; set; } public List<Language> Languages { get; set; } + public ReleaseType ReleaseType { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index e4650746d..74e2a71e6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -97,6 +97,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; episodeFile.Languages = localEpisode.Languages; + // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. + episodeFile.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo.ReleaseType; + if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true) { var grabHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId) @@ -107,12 +112,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { episodeFile.IndexerFlags = flags; } + + // Prefer the release type from the grabbed history + if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("releaseType"), true, out ReleaseType releaseType)) + { + episodeFile.ReleaseType = releaseType; + } } else { episodeFile.IndexerFlags = localEpisode.IndexerFlags; } + // Fall back to parsed information if history is unavailable or missing + if (episodeFile.ReleaseType == ReleaseType.Unknown) + { + // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. + episodeFile.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo.ReleaseType; + } + bool copyOnly; switch (importMode) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index d5164f1b1..c3f07a0a5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -119,6 +119,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport localEpisode.FileEpisodeInfo = fileEpisodeInfo; localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); + localEpisode.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo?.ReleaseType ?? + ReleaseType.Unknown; try { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index f4daed6e8..365d0ae31 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual @@ -17,6 +18,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List<Language> Languages { get; set; } public string ReleaseGroup { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public string DownloadId { get; set; } public bool Equals(ManualImportFile other) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index 703428535..9f690474b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -25,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List<CustomFormat> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public IEnumerable<Rejection> Rejections { get; set; } public ManualImportItem() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index c327a1418..58af3323d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -425,6 +425,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); item.Rejections = decision.Rejections; item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags; + item.ReleaseType = decision.LocalEpisode.ReleaseType; return item; } @@ -444,6 +445,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Quality = episodeFile.Quality; item.Languages = episodeFile.Languages; item.IndexerFlags = (int)episodeFile.IndexerFlags; + item.ReleaseType = episodeFile.ReleaseType; item.Size = _diskProvider.GetFileSize(item.Path); item.Rejections = Enumerable.Empty<Rejection>(); item.EpisodeFileId = episodeFile.Id; @@ -481,6 +483,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual Quality = file.Quality, Languages = file.Languages, IndexerFlags = (IndexerFlags)file.IndexerFlags, + ReleaseType = file.ReleaseType, Series = series, Size = 0 }; @@ -510,6 +513,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.Quality = file.Quality; localEpisode.Languages = file.Languages; localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags; + localEpisode.ReleaseType = file.ReleaseType; // TODO: Cleanup non-tracked downloads diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 2ca924779..af7c7347c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Parser.Model public QualityModel Quality { get; set; } public List<Language> Languages { get; set; } public IndexerFlags IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } public bool SceneSource { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 3833199bb..3f4eeee25 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -90,6 +90,29 @@ namespace NzbDrone.Core.Parser.Model } } + public ReleaseType ReleaseType + { + get + { + if (EpisodeNumbers.Length > 1 || AbsoluteEpisodeNumbers.Length > 1) + { + return Model.ReleaseType.MultiEpisode; + } + + if (EpisodeNumbers.Length == 1 || AbsoluteEpisodeNumbers.Length == 1) + { + return Model.ReleaseType.SingleEpisode; + } + + if (FullSeason) + { + return Model.ReleaseType.SeasonPack; + } + + return Model.ReleaseType.Unknown; + } + } + public override string ToString() { var episodeString = "[Unknown Episode]"; diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseType.cs b/src/NzbDrone.Core/Parser/Model/ReleaseType.cs new file mode 100644 index 000000000..75d44c424 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/ReleaseType.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Parser.Model +{ + public enum ReleaseType + { + Unknown = 0, + + [FieldOption(label: "Single Episode")] + SingleEpisode = 1, + + [FieldOption(label: "Multi-Episode")] + MultiEpisode = 2, + + [FieldOption(label: "Season Pack")] + SeasonPack = 3 + } +} diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs index a2bdbbe41..b1e9dd1fb 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs @@ -209,6 +209,11 @@ namespace Sonarr.Api.V3.EpisodeFiles { episodeFile.IndexerFlags = (IndexerFlags)resourceEpisodeFile.IndexerFlags; } + + if (resourceEpisodeFile.ReleaseType != null) + { + episodeFile.ReleaseType = (ReleaseType)resourceEpisodeFile.ReleaseType; + } } _mediaFileService.Update(episodeFiles); diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index 0b6adcdc1..d77338ea3 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -26,6 +26,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 MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -33,34 +34,6 @@ namespace Sonarr.Api.V3.EpisodeFiles public static class EpisodeFileResourceMapper { - private static EpisodeFileResource ToResource(this EpisodeFile model) - { - if (model == null) - { - return null; - } - - return new EpisodeFileResource - { - Id = model.Id, - - SeriesId = model.SeriesId, - SeasonNumber = model.SeasonNumber, - RelativePath = model.RelativePath, - - // Path - Size = model.Size, - DateAdded = model.DateAdded, - SceneName = model.SceneName, - ReleaseGroup = model.ReleaseGroup, - Languages = model.Languages, - Quality = model.Quality, - MediaInfo = model.MediaInfo.ToResource(model.SceneName) - - // QualityCutoffNotMet - }; - } - public static EpisodeFileResource ToResource(this EpisodeFile model, NzbDrone.Core.Tv.Series series, IUpgradableSpecification upgradableSpecification, ICustomFormatCalculationService formatCalculationService) { if (model == null) @@ -90,7 +63,8 @@ namespace Sonarr.Api.V3.EpisodeFiles QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality), CustomFormats = customFormats.ToResource(false), CustomFormatScore = customFormatScore, - IndexerFlags = (int)model.IndexerFlags + IndexerFlags = (int)model.IndexerFlags, + ReleaseType = (int)model.ReleaseType, }; } } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index 0e47dcd60..a65e6bdf3 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.Episodes; @@ -31,6 +32,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; } } @@ -67,6 +69,7 @@ namespace Sonarr.Api.V3.ManualImport // QualityWeight DownloadId = model.DownloadId, IndexerFlags = model.IndexerFlags, + ReleaseType = model.ReleaseType, Rejections = model.Rejections }; } From 086d3b5afaa7680d22835ca66da2afcb6dd5865e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 1 Mar 2024 17:03:05 -0800 Subject: [PATCH 154/762] Increase migration timeout to 5 minutes --- .../Datastore/Migration/Framework/MigrationController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index 8ef3b647a..992f2a26f 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework .Configure<ProcessorOptions>(opt => { opt.PreviewOnly = false; - opt.Timeout = TimeSpan.FromSeconds(60); + opt.Timeout = TimeSpan.FromMinutes(5); }) .Configure<SelectingProcessorAccessorOptions>(cfg => { From 6c8758c27a3d7858f811f287f0dce8aff9d2452d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 2 Mar 2024 01:25:14 +0000 Subject: [PATCH 155/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 135 +++++++++++++++++++- 1 file changed, 132 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 28afd80b8..fe68fe3e1 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -18,7 +18,7 @@ "Year": "Año", "Reload": "Recargar", "AbsoluteEpisodeNumber": "Número de Episodio Absoluto", - "AddAutoTagError": "", + "AddAutoTagError": "No se pudo añadir una nueva etiqueta automática, por favor inténtalo de nuevo.", "AddConditionError": "No se pudo añadir una nueva condición, inténtelo de nuevo.", "AddConnection": "Añadir Conexión", "AddCustomFormat": "Añadir Formato Personalizado", @@ -1231,6 +1231,135 @@ "MaximumSize": "Tamaño máximo", "IndexerValidationNoResultsInConfiguredCategories": "Petición con éxito, pero no se devolvió ningún resultado en las categorías configuradas de tu indexador. Esto puede ser un problema con el indexador o tus ajustes de categoría de tu indexador.", "IndexerValidationNoRssFeedQueryAvailable": "Ninguna consulta de canales RSS disponible. Esto puede ser un problema con el indexador o con los ajustes de categoría de tu indexador.", - "MappedNetworkDrivesWindowsService": "Los discos de red mapeados no están disponibles cuando se ejecutan como un servicio de Windows, consulta el [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) para más información.", - "MediaInfoFootNote": "MediaInfo Full/Idiomas de audio/Idiomas de subtítulo soporta un sufijo `:ES+DE` que te permite filtrar los idiomas incluidos en el nombre de archivo. Usa `-DE` para excluir idiomas específicos. Añadir `+` (eg `:ES+`) devolverá `[ES]`/`[ES+--]`/`[--]` dependiendo de los idiomas excluidos. Por ejemplo `{MediaInfo Full:ES+DE}`." + "MappedNetworkDrivesWindowsService": "Los discos de red mapeados no están disponibles cuando se ejecutan como un servicio de Windows, consulta el [FAQ]({url}) para más información.", + "MediaInfoFootNote": "MediaInfo Full/Idiomas de audio/Idiomas de subtítulo soporta un sufijo `:ES+DE` que te permite filtrar los idiomas incluidos en el nombre de archivo. Usa `-DE` para excluir idiomas específicos. Añadir `+` (eg `:ES+`) devolverá `[ES]`/`[ES+--]`/`[--]` dependiendo de los idiomas excluidos. Por ejemplo `{MediaInfo Full:ES+DE}`.", + "MidseasonFinale": "Final de mitad de temporada", + "Min": "Min", + "MonitorFutureEpisodesDescription": "Monitoriza episodios que no se han emitido aún", + "NoDelay": "Sin retraso", + "NoImportListsFound": "Ninguna lista de importación encontrada", + "Name": "Nombre", + "No": "No", + "NotificationTriggers": "Disparadores de notificación", + "ClickToChangeIndexerFlags": "Clic para cambiar las banderas del indexador", + "MinutesSixty": "60 minutos: {sixty}", + "MonitorMissingEpisodesDescription": "Monitoriza episodios que no tienen archivos o que no se han emitido aún", + "MoveFiles": "Mover archivos", + "MustNotContain": "No debe contener", + "NoEpisodeInformation": "No hay información de episodio disponible.", + "NoLeaveIt": "No, déjalo", + "NoLimitForAnyRuntime": "No hay límites para ningún tiempo de ejecución", + "NoMatchFound": "¡Ninguna coincidencia encontrada!", + "NoMinimumForAnyRuntime": "No hay mínimo para ningún tiempo de ejecución", + "NoMonitoredEpisodes": "No hay episodios monitorizados en esta serie", + "NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponible debido a fallos: {notificationNames}", + "CustomFormatsSpecificationFlag": "Bandera", + "Never": "Nunca", + "MinimumAge": "Edad mínima", + "Mixed": "Mezclado", + "MultiLanguages": "Multi-idiomas", + "NoEpisodesFoundForSelectedSeason": "No se encontró ningún episodio para la temporada seleccionada", + "NoEventsFound": "Ningún evento encontrado", + "IndexerFlags": "Banderas del indexador", + "CustomFilter": "Filtros personalizados", + "Filters": "Filtros", + "Label": "Etiqueta", + "MonitorExistingEpisodes": "Episodios existentes", + "MonitoringOptions": "Opciones de monitorización", + "NoIssuesWithYourConfiguration": "No hay problemas con tu configuración", + "NoSeriesHaveBeenAdded": "No has añadido ninguna serie aún. ¿Quieres importar alguna o todas tus series primero?", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Incluye elemento de guía de episodios formateados en JSON en tvshow.nfo (Requiere 'Metadatos de series')", + "MinimumAgeHelpText": "Solo Usenet: Edad mínima en minutos de NZBs antes de que sean capturados. Usa esto para dar tiempo a los nuevos lanzamientos para propagarse a tu proveedor usenet.", + "MinimumFreeSpace": "Espacio libre mínimo", + "MonitorAllEpisodesDescription": "Monitoriza todos los episodios salvo especiales", + "MonitorAllSeasonsDescription": "Monitoriza todas las nuevas temporadas automáticamente", + "MonitorExistingEpisodesDescription": "Monitoriza episodios que tienen archivos o que no se han emitido aún", + "MonitorFirstSeason": "Primera temporada", + "MonitorNewSeasonsHelpText": "Qué nuevas temporadas deberían ser monitorizadas automáticamente", + "MonitoredStatus": "Monitorizados/Estado", + "MoveSeriesFoldersDontMoveFiles": "No, moveré los archivos yo mismo", + "MultiEpisode": "Multi-episodio", + "MultiEpisodeInvalidFormat": "Multi-episodio: Formato inválido", + "MultiSeason": "Multi-temporada", + "NamingSettingsLoadError": "No se pudo cargar las opciones de nombrado", + "NoChanges": "Sin cambios", + "NoEpisodeOverview": "No hay sinopsis de episodio", + "MultiEpisodeStyle": "Estilo de multi-episodio", + "NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Clave de configuración para la Solución de almacenamiento persistente. Dejar vacío si se usan URLs sin estado.", + "NotificationsAppriseSettingsPasswordHelpText": "Autenticación básica HTTP de contraseña", + "Monitoring": "Monitorizando", + "MustContainHelpText": "El lanzamiento debe contener al menos uno de estos términos (insensible a mayúsculas)", + "NotificationTriggersHelpText": "Selecciona qué eventos deberían disparar esta notificación", + "None": "Ninguno", + "Network": "Red", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Incluye etiquetas de imagen en miniatura en <nombre de archivo>.nfo (Requiere 'Metadatos de episodio')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo con metadatos completos de series", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluye la URL de show de TheTVDB en tvshow.nfo (puede ser combinado con 'Metadatos de series')", + "MustNotContainHelpText": "El lanzamiento será rechazado si contiene uno o más de estos términos (insensible a mayúsculas)", + "NamingSettings": "Opciones de nombrado", + "MustContain": "Debe contener", + "NotificationsAppriseSettingsTags": "Etiquetas de Apprise", + "NotificationsAppriseSettingsTagsHelpText": "Opcionalmente notifica solo esas etiquetas en consecuencia.", + "NotificationsAppriseSettingsUsernameHelpText": "Autenticación básica HTTP de usuario", + "NotificationsCustomScriptSettingsArguments": "Argumentos", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Argumentos a pasar al script", + "NotificationsCustomScriptValidationFileDoesNotExist": "El archivo no existe", + "MinimumCustomFormatScoreHelpText": "Puntuación mínima de formato personalizado permitida para descargar", + "MinimumLimits": "Límites mínimos", + "Monday": "Lunes", + "MyComputer": "Mi ordenador", + "NoChange": "Sin cambio", + "NoTagsHaveBeenAddedYet": "Ninguna etiqueta ha sido añadida aún", + "NoUpdatesAreAvailable": "No hay actualizaciones disponibles", + "MoveSeriesFoldersMoveFiles": "Sí, mueve los archivos", + "MoveSeriesFoldersToNewPath": "¿Te gustaría mover los archivos de series de '{originalPath}' a '{destinationPath}'?", + "MoveSeriesFoldersToRootFolder": "¿Te gustaría mover las carpetas de series a '{destinationRootFolder}'?", + "NoMonitoredEpisodesSeason": "No hay episodios monitorizados en esta temporada", + "NoResultsFound": "Ningún resultado encontrado", + "NoSeasons": "No hay temporadas", + "MinimumFreeSpaceHelpText": "Evita importar si se quedaría menos que esta cantidad de disco disponible", + "MonitorFirstSeasonDescription": "Monitoriza todos los episodios de la primera temporada. El resto de temporadas serán ignoradas", + "MonitorLastSeasonDescription": "Monitoriza todos los episodios de la última temporada", + "MonitorFutureEpisodes": "Episodios futuros", + "MonitorNewSeasons": "Monitorizar nuevas temporadas", + "NoBackupsAreAvailable": "No hay copias de seguridad disponibles", + "Negated": "Anulado", + "NextAiring": "Siguiente emisión", + "NextExecution": "Siguiente ejecución", + "MonitorLastSeason": "Última temporada", + "MonitorNoEpisodesDescription": "Ningún episodio será monitorizado", + "MinimumCustomFormatScore": "Puntuación mínima de formato personalizado", + "MinutesThirty": "30 minutos: {thirty}", + "MinutesFortyFive": "45 minutos: {fortyFive}", + "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", + "NotificationsDiscordSettingsAvatar": "Avatar", + "NotificationsDiscordSettingsAvatarHelpText": "Cambia el avatar que es usado para mensajes desde esta integración", + "NotificationsAppriseSettingsNotificationType": "Tipo de notificación de Apprise", + "NotificationsAppriseSettingsConfigurationKey": "Clave de configuración de Apprise", + "NotificationsAppriseSettingsServerUrl": "URL de servidor de Apprise", + "NotificationsAppriseSettingsStatelessUrls": "URLs sin estado de Apprise", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Una o más URLs separadas por comas identificando a dónde debería ser enviada la notificación. Dejar vacío si se usa Almacenamiento persistente.", + "NotificationsAppriseSettingsServerUrlHelpText": "URL de servidor de Apprise, incluyendo http(s):// y puerto si es necesario", + "MonitoredOnly": "Solo monitorizados", + "NoLogFiles": "No hay archivos de registro", + "NoSeriesFoundImportOrAdd": "Ninguna serie encontrada, para comenzar querrás importar tus series existentes o añadir una nueva serie.", + "NotSeasonPack": "No hay paquete de temporada", + "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}.", + "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", + "LabelIsRequired": "Se requiere etiqueta", + "Month": "Mes", + "More": "Más", + "MoreDetails": "Más detalles", + "MoreInfo": "Más información", + "NoEpisodesInThisSeason": "No hay episodios en esta temporada", + "NoLinks": "No hay enlaces" } From de9899c60e01f53093db01523e209d607fb49d17 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Sat, 2 Mar 2024 01:28:40 +0000 Subject: [PATCH 156/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e6e89c035..e500eba23 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8220,6 +8220,11 @@ "format": "int32", "nullable": true }, + "releaseType": { + "type": "integer", + "format": "int32", + "nullable": true + }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" }, @@ -9531,6 +9536,9 @@ "type": "integer", "format": "int32" }, + "releaseType": { + "$ref": "#/components/schemas/ReleaseType" + }, "rejections": { "type": "array", "items": { @@ -9790,7 +9798,8 @@ "recent", "monitorSpecials", "unmonitorSpecials", - "none" + "none", + "skip" ], "type": "string" }, @@ -10158,6 +10167,9 @@ "isPossibleSceneSeasonSpecial": { "type": "boolean", "readOnly": true + }, + "releaseType": { + "$ref": "#/components/schemas/ReleaseType" } }, "additionalProperties": false @@ -10963,6 +10975,15 @@ }, "additionalProperties": false }, + "ReleaseType": { + "enum": [ + "unknown", + "singleEpisode", + "multiEpisode", + "seasonPack" + ], + "type": "string" + }, "RemotePathMappingResource": { "type": "object", "properties": { From 428569106499b5e3a463f1990ae2996d1ae4ab49 Mon Sep 17 00:00:00 2001 From: The Dark <12370876+CheAle14@users.noreply.github.com> Date: Sun, 3 Mar 2024 05:19:02 +0000 Subject: [PATCH 157/762] New: Import list exclusion pagination Closes #6079 --- frontend/src/App/State/AppSectionState.ts | 1 + frontend/src/App/State/SettingsAppState.ts | 11 + .../src/Helpers/Hooks/useModalOpenState.ts | 17 ++ .../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 | 234 ++++++++++++++++++ .../ImportListExclusionsConnector.js | 59 ----- .../ImportLists/ImportListSettings.js | 4 +- .../Actions/Settings/importListExclusions.js | 43 +++- .../createSettingsSectionSelector.ts | 51 ++-- frontend/src/typings/ImportListExclusion.ts | 6 + .../Exclusions/ImportListExclusionService.cs | 7 + .../ImportListExclusionController.cs | 13 + 26 files changed, 663 insertions(+), 690 deletions(-) create mode 100644 frontend/src/Helpers/Hooks/useModalOpenState.ts 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/typings/ImportListExclusion.ts diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index cabc39b1c..5bc7dfbac 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -38,6 +38,7 @@ export interface AppSectionItemState<T> { isFetching: boolean; isPopulated: boolean; error: Error; + pendingChanges: Partial<T>; 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<ImportListOptionsSettings>, AppSectionSaveState {} +export interface ImportListExclusionsSettingsAppState + extends AppSectionState<ImportListExclusion>, + AppSectionSaveState, + PagedAppSectionState, + AppSectionDeleteState { + pendingChanges: Partial<ImportListExclusion>; +} + export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>; export type LanguageSettingsAppState = AppSectionState<Language>; export type UiSettingsAppState = AppSectionItemState<UiSettings>; @@ -48,6 +58,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; 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/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 ( - <Modal - size={sizes.MEDIUM} - isOpen={isOpen} - onModalClose={onModalClose} - > - <EditImportListExclusionModalContentConnector - {...otherProps} - onModalClose={onModalClose} - /> - </Modal> - ); -} - -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 ( + <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}> + <EditImportListExclusionModalContent + {...otherProps} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +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 ( - <EditImportListExclusionModal - {...this.props} - onModalClose={this.onModalClose} - /> - ); - } -} - -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 ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {id ? translate('EditImportListExclusion') : translate('AddImportListExclusion')} - </ModalHeader> - - <ModalBody className={styles.body}> - { - isFetching && - <LoadingIndicator /> - } - - { - !isFetching && !!error && - <Alert kind={kinds.DANGER}> - {translate('AddImportListExclusionError')} - </Alert> - } - - { - !isFetching && !error && - <Form - {...otherProps} - > - <FormGroup> - <FormLabel>{translate('Title')}</FormLabel> - - <FormInputGroup - type={inputTypes.TEXT} - name="title" - helpText={translate('SeriesTitleToExcludeHelpText')} - {...title} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('TvdbId')}</FormLabel> - - <FormInputGroup - type={inputTypes.TEXT} - name="tvdbId" - helpText={translate('TvdbIdExcludeHelpText')} - {...tvdbId} - onChange={onInputChange} - /> - </FormGroup> - </Form> - } - </ModalBody> - - <ModalFooter> - { - id && - <Button - className={styles.deleteButton} - kind={kinds.DANGER} - onPress={onDeleteImportListExclusionPress} - > - {translate('Delete')} - </Button> - } - - <Button - onPress={onModalClose} - > - {translate('Cancel')} - </Button> - - <SpinnerErrorButton - isSpinning={isSaving} - error={saveError} - onPress={onSavePress} - > - {translate('Save')} - </SpinnerErrorButton> - </ModalFooter> - </ModalContent> - ); -} - -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<ImportListExclusion>, + ...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 ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader> + {id + ? translate('EditImportListExclusion') + : translate('AddImportListExclusion')} + </ModalHeader> + + <ModalBody className={styles.body}> + {isFetching && <LoadingIndicator />} + + {!isFetching && !!error && ( + <Alert kind={kinds.DANGER}> + {translate('AddImportListExclusionError')} + </Alert> + )} + + {!isFetching && !error && ( + <Form {...otherProps}> + <FormGroup> + <FormLabel>{translate('Title')}</FormLabel> + + <FormInputGroup + type={inputTypes.TEXT} + name="title" + helpText={translate('SeriesTitleToExcludeHelpText')} + {...title} + onChange={onInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('TvdbId')}</FormLabel> + + <FormInputGroup + type={inputTypes.NUMBER} + name="tvdbId" + helpText={translate('TvdbIdExcludeHelpText')} + {...tvdbId} + onChange={onInputChange} + /> + </FormGroup> + </Form> + )} + </ModalBody> + + <ModalFooter> + {id && ( + <Button + className={styles.deleteButton} + kind={kinds.DANGER} + onPress={onDeleteImportListExclusionPress} + > + {translate('Delete')} + </Button> + )} + + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <SpinnerErrorButton + isSpinning={isSaving} + error={saveError} + onPress={onSavePress} + > + {translate('Save')} + </SpinnerErrorButton> + </ModalFooter> + </ModalContent> + ); +} + +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 ( - <EditImportListExclusionModalContent - {...this.props} - onSavePress={this.onSavePress} - onInputChange={this.onInputChange} - /> - ); - } -} - -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 ( - <div - className={classNames( - styles.importListExclusion - )} - > - <div className={styles.title}>{title}</div> - <div className={styles.tvdbId}>{tvdbId}</div> - - <div className={styles.actions}> - <Link - onPress={this.onEditImportListExclusionPress} - > - <Icon name={icons.EDIT} /> - </Link> - </div> - - <EditImportListExclusionModalConnector - id={id} - isOpen={this.state.isEditImportListExclusionModalOpen} - onModalClose={this.onEditImportListExclusionModalClose} - onDeleteImportListExclusionPress={this.onDeleteImportListExclusionPress} - /> - - <ConfirmModal - isOpen={this.state.isDeleteImportListExclusionModalOpen} - kind={kinds.DANGER} - title={translate('DeleteImportListExclusion')} - message={translate('DeleteImportListExclusionMessageText')} - confirmLabel={translate('Delete')} - onConfirm={this.onConfirmDeleteImportListExclusion} - onCancel={this.onDeleteImportListExclusionModalClose} - /> - </div> - ); - } -} - -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 ( + <TableRow> + <TableRowCell>{title}</TableRowCell> + <TableRowCell>{tvdbId}</TableRowCell> + + <TableRowCell className={styles.actions}> + <IconButton + name={icons.EDIT} + onPress={setEditImportListExclusionModalOpen} + /> + </TableRowCell> + + <EditImportListExclusionModal + id={id} + isOpen={isEditImportListExclusionModalOpen} + onModalClose={setEditImportListExclusionModalClosed} + onDeleteImportListExclusionPress={setDeleteImportListExclusionModalOpen} + /> + + <ConfirmModal + isOpen={isDeleteImportListExclusionModalOpen} + kind={kinds.DANGER} + title={translate('DeleteImportListExclusion')} + message={translate('DeleteImportListExclusionMessageText')} + confirmLabel={translate('Delete')} + onConfirm={onConfirmDeleteImportListExclusionPress} + onCancel={setDeleteImportListExclusionModalClosed} + /> + </TableRow> + ); +} + +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 ( - <FieldSet legend={translate('ImportListExclusions')}> - <PageSectionContent - errorMessage={translate('ImportListExclusionsLoadError')} - {...otherProps} - > - <div className={styles.importListExclusionsHeader}> - <div className={styles.title}> - {translate('Title')} - </div> - <div className={styles.tvdbId}> - {translate('TvdbId')} - </div> - </div> - - <div> - { - items.map((item, index) => { - return ( - <ImportListExclusion - key={item.id} - {...item} - {...otherProps} - index={index} - onConfirmDeleteImportListExclusion={onConfirmDeleteImportListExclusion} - /> - ); - }) - } - </div> - - <div className={styles.addImportListExclusion}> - <Link - className={styles.addButton} - onPress={this.onAddImportListExclusionPress} - > - <Icon name={icons.ADD} /> - </Link> - </div> - - <EditImportListExclusionModalConnector - isOpen={this.state.isAddImportListExclusionModalOpen} - onModalClose={this.onModalClose} - /> - - </PageSectionContent> - </FieldSet> - ); - } -} - -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..7a15bca91 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -0,0 +1,234 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +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, + }, +]; + +interface ImportListExclusionsProps { + useCurrentPage: number; + totalRecords: number; +} + +function createImportListExlucionsSelector() { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + return { + ...importListExclusions, + }; + } + ); +} + +function ImportListExclusions(props: ImportListExclusionsProps) { + const { useCurrentPage, totalRecords } = props; + + 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, + ...otherProps + } = selected; + + const [ + isAddImportListExclusionModalOpen, + setAddImportListExclusionModalOpen, + setAddImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const isFetchingForFirstTime = isFetching && !isPopulated; + + return ( + <FieldSet legend={translate('ImportListExclusions')}> + <PageSectionContent + errorMessage={translate('ImportListExclusionsLoadError')} + isFetching={isFetchingForFirstTime} + isPopulated={isPopulated} + error={error} + > + <Table + columns={COLUMNS} + canModifyColumns={false} + pageSize={pageSize} + sortKey={sortKey} + sortDirection={sortDirection} + onSortPress={setImportListExclusionSort} + onTableOptionChange={setImportListTableOption} + > + <TableBody> + {items.map((item) => { + return ( + <ImportListExclusionRow + key={item.id} + {...item} + onConfirmDeleteImportListExclusion={ + onConfirmDeleteImportListExclusion + } + /> + ); + })} + + <TableRow> + <TableRowCell /> + <TableRowCell /> + + <TableRowCell> + <IconButton + name={icons.ADD} + onPress={setAddImportListExclusionModalOpen} + /> + </TableRowCell> + </TableRow> + </TableBody> + </Table> + + <TablePager + totalRecords={totalRecords} + pageSize={pageSize} + isFetching={isFetching} + onFirstPagePress={gotoImportListExclusionFirstPage} + onPreviousPagePress={gotoImportListExclusionPreviousPage} + onNextPagePress={gotoImportListExclusionNextPage} + onLastPagePress={gotoImportListExclusionLastPage} + onPageSelect={gotoImportListExclusionPage} + {...otherProps} + /> + + <EditImportListExclusionModal + isOpen={isAddImportListExclusionModalOpen} + onModalClose={setAddImportListExclusionModalClosed} + /> + </PageSectionContent> + </FieldSet> + ); +} + +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 ( - <ImportListExclusions - {...this.state} - {...this.props} - onConfirmDeleteImportListExclusion={this.onConfirmDeleteImportListExclusion} - /> - ); - } -} - -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} /> - <ImportListsExclusionsConnector /> + <ImportListsExclusions /> <ManageImportListsModal isOpen={isManageImportListsOpen} onModalClose={this.onManageImportListsModalClose} diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index b9b38a0ef..5bf7d37a2 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 createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; +import { createThunk, handleThunks } from 'Store/thunks'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createServerSideCollectionHandlers from '../Creators/createServerSideCollectionHandlers'; +import createSetTableOptionReducer from '../Creators/Reducers/createSetTableOptionReducer'; // // 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/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<AppState['settings'], 'advancedSettings'>; -type GetSectionState<Name extends SettingNames> = AppState['settings'][Name]; -type GetSettingsSectionItemType<Name extends SettingNames> = - GetSectionState<Name> extends AppSectionItemState<infer R> - ? R - : GetSectionState<Name> extends AppSectionState<infer R> - ? R +type SectionsWithItemNames = { + [K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState<unknown> + ? K : never; +}[keyof SettingsAppState]; -type AppStateWithPending<Name extends SettingNames> = { - item?: GetSettingsSectionItemType<Name>; - pendingChanges?: Partial<GetSettingsSectionItemType<Name>>; - saveError?: Error; -} & GetSectionState<Name>; +type GetSectionState<Name extends SectionsWithItemNames> = + SettingsAppState[Name]; +type GetSettingsSectionItemType<Name extends SectionsWithItemNames> = + GetSectionState<Name> extends AppSectionItemState<infer R> ? R : never; -function createSettingsSectionSelector<Name extends SettingNames>( - section: Name -) { +function createSettingsSectionSelector< + Name extends SectionsWithItemNames, + T extends GetSettingsSectionItemType<Name> +>(section: Name) { return createSelector( (state: AppState) => state.settings[section], (sectionSettings) => { - const { item, pendingChanges, saveError, ...other } = - sectionSettings as AppStateWithPending<Name>; + 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<GetSettingsSectionItemType<Name>>, + settings: settings as PendingSection<T>, + pendingChanges: selectedPendingChanges as Partial<T>, ...rest, }; } 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/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<ImportListExclusion> All(); + PagingSpec<ImportListExclusion> Paged(PagingSpec<ImportListExclusion> 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<ImportListExclusion> Paged(PagingSpec<ImportListExclusion> pagingSpec) + { + return _repo.GetPaged(pagingSpec); + } + public void HandleAsync(SeriesDeletedEvent message) { if (!message.AddImportListExclusion) 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) From 7f061a9583870d9b60427c8be2d55f695cc545d7 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 3 Mar 2024 00:24:11 +0000 Subject: [PATCH 158/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Nicolò Castagnola <nipica@outlook.it> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 18 +--- src/NzbDrone.Core/Localization/Core/it.json | 1 - .../Localization/Core/pt_BR.json | 4 +- src/NzbDrone.Core/Localization/Core/ro.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 1 - src/NzbDrone.Core/Localization/Core/tr.json | 82 ++++++++++++++++++- .../Localization/Core/zh_CN.json | 1 - 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 40ea83ad4..fd571509c 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -6,7 +6,7 @@ "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.", "DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {downloadClientName} legt Downloads im Stammordner {rootFolderPath} ab. Sie sollten nicht in einen Stammordner herunterladen.", @@ -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", @@ -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", @@ -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", @@ -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,7 +524,6 @@ "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}", "Version": "Version", @@ -572,7 +560,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 +593,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 +606,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", @@ -770,7 +755,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", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index d1e93e9d3..2b98de2c4 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -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}", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8f5e7e63a..940cb956f 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2050,5 +2050,7 @@ "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" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 68252092b..0a28d0f4a 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -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.", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index f5271efdd..95e74778b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -197,7 +197,6 @@ "AlreadyInYourLibrary": "Уже в вашей библиотеке", "Always": "Всегда", "Conditions": "Условия", - "AddAutoTag": "", "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index c1633f7ce..787097f4e 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -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/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index f95f32f6b..0c6733450 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1762,7 +1762,6 @@ "NotificationsNtfySettingsServerUrl": "服务器 URL", "NotificationsNtfySettingsPasswordHelpText": "密码,可选", "NotificationsPushBulletSettingSenderIdHelpText": "发送通知的设备 ID,使用 pushbullet.com 设备 URL 中的 device_iden 参数值,或者留空来自行发送", - "NotificationsPushBulletSettingsAccessToken": "", "NotificationsPushBulletSettingsChannelTags": "频道标签", "NotificationsPushBulletSettingsChannelTagsHelpText": "通知的目标频道标签列表", "NotificationsPushBulletSettingsDeviceIds": "设备 ID", From 64c6a8879beb1b17122c8f6f74bf7b3cf4dd1570 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 1 Mar 2024 23:24:47 -0800 Subject: [PATCH 159/762] Queue Manual Import commands at high priority --- src/Sonarr.Api.V3/Commands/CommandController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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); } } From 71c2c0570b45c0b7613e61b453cce23ef6e34980 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 2 Mar 2024 21:19:44 -0800 Subject: [PATCH 160/762] Renamed SeasonPackSpecification to ReleaseTypeSpecification --- ...pecification.cs => ReleaseTypeSpecification.cs} | 4 ++-- .../Migration/205_rename_season_pack_spec.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) rename src/NzbDrone.Core/CustomFormats/Specifications/{SeasonPackSpecification.cs => ReleaseTypeSpecification.cs} (92%) create mode 100644 src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs 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<SeasonPackSpecification> + public class SeasonPackSpecificationValidator : AbstractValidator<ReleaseTypeSpecification> { 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/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')"); + } + } +} From 13af6f57796e54c3949cf340e03f020e6f8575c4 Mon Sep 17 00:00:00 2001 From: Louis R <covert8@users.noreply.github.com> Date: Sun, 3 Mar 2024 06:20:36 +0100 Subject: [PATCH 161/762] Fixed: Don't disable IPv6 in IPv6-only Environment Closes #6545 --- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 9eb4cc1e4..2215a953f 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,7 +1,9 @@ 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; @@ -247,6 +249,18 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } + private static bool HasRoutableIPv4Address() + { + // Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses + 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))); + } + private static async ValueTask<Stream> 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. @@ -270,10 +284,8 @@ 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. + useIPv6 = !HasRoutableIPv4Address(); } finally { From e5f19f01fa657325cc981421dc44e2f5eb14b6d3 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 2 Mar 2024 23:21:16 -0600 Subject: [PATCH 162/762] Update AddSeries Messaging and Logging --- src/NzbDrone.Core/Tv/AddSeriesService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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> { From 20273b07ad0cd1ba7d9797a91c50980054bd41ca Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 2 Mar 2024 14:00:58 -0800 Subject: [PATCH 163/762] Properly type validation errors/warnings --- frontend/src/typings/pending.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) 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<T> { value: T; - errors: any[]; - warnings: any[]; + errors: ValidationError[]; + warnings: ValidationWarning[]; } export type PendingSection<T> = { From 07bd159436935a7adb87ae1b6924a4d42d719b0f Mon Sep 17 00:00:00 2001 From: nopoz <bill.lowney@gmail.com> Date: Sat, 2 Mar 2024 21:22:03 -0800 Subject: [PATCH 164/762] New: Add download directory & move completed for Deluge Closes #4575 --- .../Download/Clients/Deluge/DelugeProxy.cs | 42 ++++++++++++++----- .../Download/Clients/Deluge/DelugeSettings.cs | 6 +++ src/NzbDrone.Core/Localization/Core/en.json | 4 ++ 3 files changed, 42 insertions(+), 10 deletions(-) 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<string>(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<string>(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/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 40cc2f581..ede7a0ca3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -413,6 +413,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}", From 32c32e2f884adf136cc5c42ab8f32a8058ea7707 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 2 Mar 2024 17:18:55 -0800 Subject: [PATCH 165/762] Fixed: Issue extracting subtitle information for unknown episodes --- .../Extras/Subtitles/ExistingSubtitleImporter.cs | 9 +++++---- .../Aggregation/Aggregators/AggregateSubtitleInfo.cs | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index 6c5a5481e..05fdf5770 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; @@ -78,11 +79,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<string>(), + Title = localEpisode.SubtitleInfo?.Title, Extension = extension, - Copy = localEpisode.SubtitleInfo.Copy + Copy = localEpisode.SubtitleInfo?.Copy ?? 0 }; subtitleFiles.Add(subtitleFile); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs index 5beabf7d5..65eaf0cca 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,6 +31,11 @@ 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); From 653963a2478924fa5ec54ba5de1fc87861062dd7 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Sun, 3 Mar 2024 05:21:40 +0000 Subject: [PATCH 166/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e500eba23..54c1bdd7b 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": [ @@ -8889,6 +8943,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": { From fa4c11a943bc685a260180f0647f86449c359a8b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 2 Mar 2024 17:10:32 -0800 Subject: [PATCH 167/762] New: Do not automatically unmonitor episodes renamed outside of Sonarr Closes #6584 --- .../HandleEpisodeFileDeletedFixture.cs | 36 +++++- src/NzbDrone.Core/Tv/EpisodeService.cs | 104 +++++++++++++----- 2 files changed, 108 insertions(+), 32 deletions(-) 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<EpisodeService> { + private Series _series; private EpisodeFile _episodeFile; private List<Episode> _episodes; [SetUp] public void Setup() { + _series = Builder<Series> + .CreateNew() + .Build(); + _episodeFile = Builder<EpisodeFile> .CreateNew() + .With(e => e.SeriesId = _series.Id) .Build(); } @@ -30,6 +38,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests _episodes = Builder<Episode> .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<Episode> .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<string>())); Mocker.GetMock<IEpisodeRepository>() - .Verify(v => v.ClearFileId(It.IsAny<Episode>(), true), Times.Once()); + .Verify(v => v.SetMonitored(It.IsAny<IEnumerable<int>>(), 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<List<Episode>>(_episodes); + + Mocker.GetMock<IConfigService>() + .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<string>())); + + Mocker.GetMock<IEpisodeRepository>() + .Verify(v => v.SetMonitored(It.IsAny<IEnumerable<int>>(), false), Times.Never()); } [Test] 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()); + } + } } } From 7f09903a06d0d5657d20fba9eb5e286d455140be Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 23 Feb 2024 04:51:35 +0200 Subject: [PATCH 168/762] New: Episode Requested filter for Interactive Search --- frontend/src/Store/Actions/releaseActions.js | 6 ++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 7 insertions(+) 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/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ede7a0ca3..15674ddc7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -632,6 +632,7 @@ "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", "EpisodeTitleRequired": "Episode Title Required", From 0183812cc58dad0e555125ddd8b33a85cbdecbf2 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Mar 2024 09:34:02 -0800 Subject: [PATCH 169/762] Fixed: Overly aggressive exception release group parsing Closes #6591 --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 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/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 421ca70cf..693eaa36d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -533,7 +533,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); From 2068c5393e130d51d7bf2ae948b1fcb254c7106f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:44:47 +0200 Subject: [PATCH 170/762] Fixed: URL Base setting for Kodi connections --- src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs | 2 +- src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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() { From f211433b778a4ccd5d575b721ba0528e040c5770 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:45:17 +0200 Subject: [PATCH 171/762] Remove debugger from metadata source and rearrange some imports --- frontend/src/Settings/MetadataSource/TheTvdb.js | 1 - frontend/src/Store/Actions/Settings/importListExclusions.js | 4 ++-- frontend/src/Store/Actions/settingsActions.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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 ( <div className={styles.container}> <img diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index 5bf7d37a2..d6371946f 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -1,11 +1,11 @@ import { createAction } from 'redux-actions'; 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 createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import { createThunk, handleThunks } from 'Store/thunks'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import createServerSideCollectionHandlers from '../Creators/createServerSideCollectionHandlers'; -import createSetTableOptionReducer from '../Creators/Reducers/createSetTableOptionReducer'; // // Variables diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index e7b5e40f6..a030c028a 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'; From e81bb3b993adac705fd61dc9e281b040ca2338f5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:37:24 +0200 Subject: [PATCH 172/762] Persist page size for Import List Exclusions --- frontend/src/Store/Actions/settingsActions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index a030c028a..440f20000 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -91,7 +91,8 @@ export const defaultState = { }; export const persistState = [ - 'settings.advancedSettings' + 'settings.advancedSettings', + 'settings.importListExclusions.pageSize' ]; // From d0e9504af0d88391a74e04b90638e4b2d99fb476 Mon Sep 17 00:00:00 2001 From: CheAle14 <12370876+CheAle14@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:37:20 +0000 Subject: [PATCH 173/762] Fix import list exclusion props --- frontend/src/App/State/AppSectionState.ts | 1 + .../ImportListExclusions/ImportListExclusions.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 5bc7dfbac..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<T> { diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index 7a15bca91..8c7033686 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -1,5 +1,6 @@ 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'; @@ -41,11 +42,6 @@ const COLUMNS = [ }, ]; -interface ImportListExclusionsProps { - useCurrentPage: number; - totalRecords: number; -} - function createImportListExlucionsSelector() { return createSelector( (state: AppState) => state.settings.importListExclusions, @@ -57,8 +53,9 @@ function createImportListExlucionsSelector() { ); } -function ImportListExclusions(props: ImportListExclusionsProps) { - const { useCurrentPage, totalRecords } = props; +function ImportListExclusions() { + const history = useHistory(); + const useCurrentPage = history.action === 'POP'; const dispatch = useDispatch(); @@ -155,6 +152,7 @@ function ImportListExclusions(props: ImportListExclusionsProps) { sortKey, error, sortDirection, + totalRecords, ...otherProps } = selected; From c7dd7abf892eead7796fcc482aa2f2aabaf88712 Mon Sep 17 00:00:00 2001 From: Helvio Pedreschi <helvio88@gmail.com> Date: Thu, 7 Mar 2024 20:29:50 -0500 Subject: [PATCH 174/762] Fixed: WebApp functionality on Apple devices --- frontend/src/index.ejs | 7 +++++-- frontend/src/login.html | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-capable" content="yes" /> <!-- Chrome, Opera, and Firefox OS --> <meta name="theme-color" content="#3a3f51" /> <!-- Windows Phone --> <meta name="msapplication-navbutton-color" content="#3a3f51" /> + <!-- Android/Apple Phone --> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> + <meta name="format-detection" content="telephone=no"> <meta name="description" content="Sonarr" /> 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 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-capable" content="yes" /> <!-- Chrome, Opera, and Firefox OS --> <meta name="theme-color" content="#3a3f51" /> <!-- Windows Phone --> <meta name="msapplication-navbutton-color" content="#3a3f51" /> + <!-- Android/Apple Phone --> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> + <meta name="format-detection" content="telephone=no"> <meta name="description" content="Sonarr" /> From 18aadb544e2567b26eecbf8dec70f7bd7c47f3e9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 8 Mar 2024 03:30:20 +0200 Subject: [PATCH 175/762] Fixed: Maintain release type for items in Manual Import --- frontend/src/InteractiveImport/InteractiveImport.ts | 2 ++ frontend/src/InteractiveImport/ReleaseType.ts | 3 +++ .../src/Store/Actions/interactiveImportActions.js | 1 + .../EpisodeImport/Manual/ManualImportService.cs | 11 +++++++---- .../ManualImport/ManualImportController.cs | 2 +- .../ManualImport/ManualImportReprocessResource.cs | 2 ++ 6 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 frontend/src/InteractiveImport/ReleaseType.ts diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 9ec91a4aa..1feea60c0 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -1,5 +1,6 @@ import ModelBase from 'App/ModelBase'; import Episode from 'Episode/Episode'; +import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import Series from 'Series/Series'; @@ -33,6 +34,7 @@ interface InteractiveImport extends ModelBase { qualityWeight: number; customFormats: object[]; indexerFlags: number; + releaseType: ReleaseType; rejections: Rejection[]; episodeFileId?: number; } 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/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/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<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags, ReleaseType releaseType); } public class ManualImportService : IExecute<ManualImportCommand>, 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<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags) + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> 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/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index f537f2e2f..eb6787c5b 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -39,7 +39,7 @@ 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(); 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; } } } From 2c252458609dd8cc2a7ee193e6f043cdb5356f80 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 8 Mar 2024 01:27:05 +0000 Subject: [PATCH 176/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: Mark Martines <mark-martines@hotmail.com> Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com> Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: linkin931 <931linkin@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/el/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ko/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 8 +- src/NzbDrone.Core/Localization/Core/cs.json | 6 +- src/NzbDrone.Core/Localization/Core/de.json | 21 +- src/NzbDrone.Core/Localization/Core/el.json | 4 +- src/NzbDrone.Core/Localization/Core/es.json | 726 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/fi.json | 37 +- src/NzbDrone.Core/Localization/Core/fr.json | 472 ++++++++---- src/NzbDrone.Core/Localization/Core/hu.json | 17 +- src/NzbDrone.Core/Localization/Core/it.json | 2 +- src/NzbDrone.Core/Localization/Core/ko.json | 2 +- src/NzbDrone.Core/Localization/Core/nl.json | 41 +- src/NzbDrone.Core/Localization/Core/pt.json | 8 +- .../Localization/Core/pt_BR.json | 15 +- src/NzbDrone.Core/Localization/Core/ro.json | 2 +- src/NzbDrone.Core/Localization/Core/ru.json | 10 +- src/NzbDrone.Core/Localization/Core/tr.json | 4 +- .../Localization/Core/zh_CN.json | 18 +- 17 files changed, 1133 insertions(+), 260 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 0bba6b2a8..08c1aba65 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", @@ -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.", 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/de.json b/src/NzbDrone.Core/Localization/Core/de.json index fd571509c..0ee4eb808 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -8,7 +8,7 @@ "AutomaticAdd": "Automatisch hinzufügen", "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}", @@ -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", @@ -202,7 +202,7 @@ "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.", @@ -288,15 +288,15 @@ "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", @@ -350,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", @@ -372,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", @@ -446,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", @@ -525,7 +525,7 @@ "UsenetDelayTime": "Usenet-Verzögerung: {usenetDelay}", "UsenetDelayHelpText": "Verzögerung in Minuten, bevor Sie eine Veröffentlichung aus dem Usenet erhalten", "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", @@ -549,7 +549,6 @@ "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", "Daily": "Täglich", 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/es.json b/src/NzbDrone.Core/Localization/Core/es.json index fe68fe3e1..0093deca1 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", @@ -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", @@ -325,10 +325,10 @@ "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.", + "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", @@ -616,7 +616,7 @@ "DownloadClientRTorrentSettingsUrlPath": "Ruta de la 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}", @@ -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", @@ -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", @@ -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,701 @@ "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", + "ReleaseProfileIndexerHelpTextWarning": "Usar un indexador específico con perfiles de lanzamientos puede conllevar que lanzamientos duplicados sean capturados", + "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')" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index af390db5a..c00cd1c8a 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", @@ -1619,7 +1618,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..438e7cafe 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,7 +243,7 @@ "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", "Delete": "Supprimer", @@ -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,10 @@ "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", + "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications 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 +934,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 +988,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 +1054,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 +1067,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", @@ -1108,7 +1108,7 @@ "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 +1152,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 +1174,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 +1195,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 +1206,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é", @@ -1251,7 +1251,7 @@ "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", + "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 +1263,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,7 +1277,7 @@ "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é", @@ -1292,7 +1292,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,17 +1340,17 @@ "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", @@ -1390,7 +1390,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 +1401,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", @@ -1444,15 +1444,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 +1464,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 +1486,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 +1506,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 +1532,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 +1555,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 +1641,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 +1792,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 +1872,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 +1883,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 +1923,139 @@ "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": "Cliquer pour changer les attributs de l'indexer", + "CustomFormatsSpecificationFlag": "Attribut", + "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 <filename>.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" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index f8ca3b381..c3213f302 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,7 +891,7 @@ "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", @@ -975,7 +975,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 +1106,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 +1379,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 2b98de2c4..3cb19a328 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -227,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", 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/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 940cb956f..0773f1dac 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}", @@ -1327,7 +1327,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 +1615,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 +1756,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", @@ -2052,5 +2052,10 @@ "Label": "Rótulo", "LabelIsRequired": "Rótulo é requerido", "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo a URL {connectionName}, como {url}", - "ReleaseType": "Tipo de Lançamento" + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 0a28d0f4a..d6a639da8 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", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 95e74778b..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": "Включить обработку завершенной загрузки, если это возможно", @@ -210,5 +210,11 @@ "CustomFormatsSpecificationRegularExpression": "Регулярное выражение", "CustomFormatsSpecificationReleaseGroup": "Релиз группа", "CustomFormatsSpecificationResolution": "Разрешение", - "CustomFormatsSpecificationSource": "Источник" + "CustomFormatsSpecificationSource": "Источник", + "AddAutoTag": "Добавить автоматический тег", + "AddAutoTagError": "Не удалось добавить новый авто тег, пожалуйста повторите попытку.", + "AddListExclusionError": "Не удалось добавить новое исключение из списка. Повторите попытку.", + "AddImportListExclusionError": "Не удалось добавить новое исключение из списка импорта. Повторите попытку.", + "AddListExclusion": "Добавить исключение из списка", + "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку." } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 787097f4e..32a93393a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -119,7 +119,7 @@ "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.", + "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ı", @@ -136,7 +136,7 @@ "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.", + "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.", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 0c6733450..2083d9c24 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}无法查看此目录。您可能需要调整文件夹的权限。", @@ -825,7 +825,7 @@ "SelectEpisodes": "选择剧集", "SelectSeason": "选择季", "LibraryImportTipsQualityInEpisodeFilename": "确保您的文件在其文件名中包含质量。例如:`episode.s02e15.bluray.mkv`", - "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExamp}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", + "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExample}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", "ListQualityProfileHelpText": "质量配置列表项将添加", "SeriesIndexFooterMissingMonitored": "缺失集(剧集已监控)", "SeriesIsMonitored": "剧集被监控", @@ -907,7 +907,7 @@ "TorrentDelay": "Torrent延时", "ToggleUnmonitoredToMonitored": "未监控,单击进行监控", "Upcoming": "即将播出", - "ProgressBarProgress": "进度栏位于{Progress}%", + "ProgressBarProgress": "进度栏位于{progress}%", "Usenet": "Usenet", "Week": "周", "Standard": "标准", @@ -966,7 +966,7 @@ "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答]({url})获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", - "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", + "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", "Max": "最大的", "MaximumSingleEpisodeAgeHelpText": "在整季搜索期间,当该季的最后一集比此设置旧时,只允许获取整季包。仅限标准剧集。填写 0 可禁用此设置。", "MaximumSize": "最大文件体积", @@ -994,7 +994,7 @@ "MoreDetails": "更多详细信息", "More": "更多", "MoveAutomatically": "自动移动", - "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{originalPath}' ?", + "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{destinationPath}' ?", "MoveSeriesFoldersMoveFiles": "是,移动文件", "MultiEpisode": "多集", "MultiEpisodeStyle": "多集风格", @@ -1585,7 +1585,7 @@ "IndexerSettingsSeedRatioHelpText": "种子在停止之前应达到的比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", "IndexerValidationTestAbortedDueToError": "测试因错误而中止:{exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "索引器不支持所需的搜索参数", - "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {异常消息}。", + "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {exceptionMessage}。", "IndexerValidationUnableToConnectInvalidCredentials": "无法连接到索引器,凭据无效。{exceptionMessage}。", "IndexerValidationUnableToConnectResolutionFailure": "与索引器连接失败。 请检查与索引器服务器和 DNS 的连接。{exceptionMessage}。", "IndexerValidationUnableToConnectServerUnavailable": "无法连接到索引器,索引器的服务器不可用。 请稍后再试。{exceptionMessage}。", @@ -1601,7 +1601,7 @@ "DownloadClientFloodSettingsPostImportTagsHelpText": "导入下载后附加标签。", "DownloadClientFloodSettingsStartOnAdd": "添加并开始", "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。 要被识别,下载必须具有所有初始标签。 这可以避免与不相关的下载发生冲突。", - "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因:{错误描述}", + "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因 {errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "API 地址", "DownloadClientFreeboxSettingsAppToken": "App Token", "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查“主机名”、“端口”或“使用 SSL”的设置(错误: {exceptionMessage})", From 61a7515041b75fa7809b0d55c051ce707d62aca0 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Fri, 8 Mar 2024 01:32:30 +0000 Subject: [PATCH 177/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 54c1bdd7b..cd2552f4d 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -9528,6 +9528,9 @@ "type": "integer", "format": "int32" }, + "releaseType": { + "$ref": "#/components/schemas/ReleaseType" + }, "rejections": { "type": "array", "items": { From 13e29bd257ccfccb09e66c940ffabeb6503c05b5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 7 Mar 2024 21:34:57 +0200 Subject: [PATCH 178/762] Prevent NullRef in naming when truncating a null Release Group --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f527dd334..e8abe01c4 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -622,7 +622,7 @@ 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); } private void AddQualityTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series, EpisodeFile episodeFile) @@ -1168,6 +1168,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)) From a12cdb34bc0ab78937e3c3677012bf030923aebf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Mar 2024 11:48:49 -0800 Subject: [PATCH 179/762] Fixed: Error sending Manual Interaction Required notification --- .../Notifications/NotificationService.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) 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 }; From 89bef4af99da90df608dbdaad42d87544eb27038 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:50:45 +0200 Subject: [PATCH 180/762] New: Wider modal for Interactive Search and Manual Import --- frontend/src/Components/Modal/Modal.css | 10 +++++++++- frontend/src/Components/Modal/Modal.css.d.ts | 1 + frontend/src/Episode/EpisodeDetailsModal.js | 2 +- frontend/src/Helpers/Props/sizes.js | 4 ++-- .../src/InteractiveImport/InteractiveImportModal.tsx | 2 +- .../src/Series/Search/SeasonInteractiveSearchModal.js | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) 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/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 ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={this.state.closeOnBackgroundClick} onModalClose={onModalClose} > 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/InteractiveImportModal.tsx b/frontend/src/InteractiveImport/InteractiveImportModal.tsx index 37b26012e..11dc8e6ae 100644 --- a/frontend/src/InteractiveImport/InteractiveImportModal.tsx +++ b/frontend/src/InteractiveImport/InteractiveImportModal.tsx @@ -47,7 +47,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) { return ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={false} onModalClose={onModalClose} > 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 ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={false} onModalClose={onModalClose} > From a0329adeba1ccda49d24b12f9ef9281588c9fc89 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 9 Mar 2024 22:51:29 -0600 Subject: [PATCH 181/762] Improve single file detected as full season messaging --- .../EpisodeImport/Specifications/FullSeasonSpecification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(); From 48cb5d227187a06930aad5ee1b4e7b76422d8421 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Sat, 9 Mar 2024 22:53:02 -0600 Subject: [PATCH 182/762] New: 'Custom Format: Format Name' rename token --- .../MediaManagement/Naming/NamingModal.js | 3 +- .../CustomFormatsFixture.cs | 122 ++++++++++++++++++ .../CustomFormats/CustomFormat.cs | 2 +- .../Organizer/FileNameBuilder.cs | 11 +- 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 509c5d940..9af6a1160 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -150,7 +150,8 @@ const mediaInfoTokens = [ const otherTokens = [ { token: '{Release Group}', example: 'Rls Grp' }, - { token: '{Custom Formats}', example: 'iNTERNAL' } + { token: '{Custom Formats}', example: 'iNTERNAL' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' } ]; const originalTokens = [ 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..fb644e104 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs @@ -0,0 +1,122 @@ +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<FileNameBuilder> + { + private Series _series; + private Episode _episode1; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + private List<CustomFormat> _customFormats; + + [SetUp] + public void Setup() + { + _series = Builder<Series> + .CreateNew() + .With(s => s.Title = "South Park") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock<INamingConfigService>() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _episode1 = Builder<Episode>.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<CustomFormat>() + { + 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<IQualityDefinitionService>() + .Setup(v => v.Get(Moq.It.IsAny<Quality>())) + .Returns<Quality>(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<Episode> { _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<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>()) + .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<Episode> { _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<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>()) + .Should().Be(expected); + } + } +} 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/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index e8abe01c4..947d5f555 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached<bool> _patternHasEpisodeIdentifierCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9+-]+(?<!-)))?(?<suffix>[- ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", @@ -698,6 +698,15 @@ namespace NzbDrone.Core.Organizer } tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); + tokenHandlers["{Custom Format}"] = m => + { + if (m.CustomFormat.IsNullOrWhiteSpace()) + { + return string.Empty; + } + + return customFormats.Where(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat).FirstOrDefault()?.ToString() ?? string.Empty; + }; } private void AddIdTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series) From d86aeb7472f7f514c6793172e3858af299472bf8 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Sat, 9 Mar 2024 22:54:06 -0600 Subject: [PATCH 183/762] New: Release Hash renaming token Closes #6570 --- .../MediaManagement/Naming/NamingModal.js | 22 +++++ .../AggregateReleaseHashFixture.cs | 83 +++++++++++++++++++ .../FileNameBuilderFixture.cs | 22 +++++ .../ParserTests/AnimeMetadataParserFixture.cs | 32 ++++++- .../Migration/204_add_release_hash.cs | 76 +++++++++++++++++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../Aggregators/AggregateReleaseHash.cs | 41 +++++++++ .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../Organizer/FileNameBuilder.cs | 1 + .../Parser/Model/LocalEpisode.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 48 +++++------ 11 files changed, 303 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 9af6a1160..f873ec1d9 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -154,6 +154,10 @@ const otherTokens = [ { token: '{Custom Format:FormatName}', example: 'AMZN' } ]; +const otherAnimeTokens = [ + { token: '{Release Hash}', example: 'ABCDEFGH' } +]; + const originalTokens = [ { token: '{Original Title}', example: 'The.Series.Title\'s!.S01E01.WEBDL.1080p.x264-EVOLVE' }, { token: '{Original Filename}', example: 'the.series.title\'s!.s01e01.webdl.1080p.x264-EVOLVE' } @@ -535,6 +539,24 @@ class NamingModal extends Component { } ) } + + { + anime && otherAnimeTokens.map(({ token, example }) => { + return ( + <NamingOption + key={token} + name={name} + value={value} + token={token} + example={example} + tokenSeparator={tokenSeparator} + tokenCase={tokenCase} + onPress={this.onOptionPress} + /> + ); + } + ) + } </div> </FieldSet> 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<AggregateReleaseHash> + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder<Series>.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/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<Episode> { _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<Episode> { _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/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/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<object>(); + + 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/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/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/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 74e2a71e6..d591a068d 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. diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 947d5f555..c09978bfa 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -623,6 +623,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); 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<string, Func<TokenMatch, string>> tokenHandlers, Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index af7c7347c..65f6e84f8 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -37,6 +37,7 @@ 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; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 693eaa36d..aa2a121b9 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 @@ -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,27 @@ 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 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 +492,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); @@ -1197,7 +1197,7 @@ namespace NzbDrone.Core.Parser if (hash.Success) { - var hashValue = hash.Value.Trim('[', ']'); + var hashValue = hash.Value.Trim('[', ']', '(', ')'); if (hashValue.Equals("1280x720")) { From 2ec071a5ecab8f5056d179feaaef0147abb944ca Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 10 Mar 2024 04:54:21 +0000 Subject: [PATCH 184/762] Update release profile download client warning --- .../HealthCheck/Checks/DownloadClientRootFolderCheck.cs | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 1 - src/NzbDrone.Core/Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 8 files changed, 2 insertions(+), 8 deletions(-) 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<string, object> { { "downloadClientName", client.Definition.Name }, - { "path", folder.FullPath } + { "rootFolderPath", folder.FullPath } }), "#downloads-in-root-folder"); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 15674ddc7..ce503dd49 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1590,7 +1590,7 @@ "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", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 0093deca1..c6ab4d35a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1644,7 +1644,6 @@ "Pending": "Pendiente", "QualityDefinitions": "Definiciones de calidad", "RecyclingBin": "Papelera de reciclaje", - "ReleaseProfileIndexerHelpTextWarning": "Usar un indexador específico con perfiles de lanzamientos puede conllevar que lanzamientos duplicados sean capturados", "ReleaseTitle": "Título de lanzamiento", "RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente", "Remove": "Eliminar", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index c00cd1c8a..adbb36ef8 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1395,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", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 438e7cafe..55a004379 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -654,7 +654,6 @@ "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 entraîner la saisie de publications 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.", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index c3213f302..f3a6bc1c2 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -894,7 +894,6 @@ "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", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 0773f1dac..d21604d83 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -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", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 2083d9c24..925e731ca 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1082,7 +1082,6 @@ "RefreshAndScan": "刷新并扫描", "RefreshAndScanTooltip": "刷新信息并扫描磁盘", "ReleaseProfileIndexerHelpText": "指定配置文件应用于哪个索引器", - "ReleaseProfileIndexerHelpTextWarning": "使用有发布配置的特定索引器可能会导致重复获取发布", "ReleaseRejected": "发布被拒绝", "ReleaseSceneIndicatorAssumingTvdb": "推测TVDB编号。", "ReleaseSceneIndicatorMappedNotRequested": "在此搜索中未包含已映射的剧集。", From 4aa56e3f910d597d1b83609bd5715b51e38e6bfc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 9 Mar 2024 23:35:50 -0800 Subject: [PATCH 185/762] Fixed: Parsing of some French and Spanish anime releases --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 37c54215c..427a3e480 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -132,6 +132,8 @@ 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("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index aa2a121b9..2989353ad 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Parser 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+)))+.*?(?<hash>[(\[]\w{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 @@ -401,6 +401,10 @@ namespace NzbDrone.Core.Parser 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 new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 86034beccd14c873ce550ad9d3c32a5b4e1b4ab7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 10 Mar 2024 11:31:15 +0200 Subject: [PATCH 186/762] Bump ImageSharp, Polly, DryIoc, STJson, WindowsServices --- src/NzbDrone.Common/Sonarr.Common.csproj | 6 +++--- src/NzbDrone.Core/Sonarr.Core.csproj | 6 +++--- src/NzbDrone.Host/Sonarr.Host.csproj | 4 ++-- src/NzbDrone.Update/Sonarr.Update.csproj | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 831798a2e..afd914994 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -4,16 +4,16 @@ <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> </PropertyGroup> <ItemGroup> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> - <PackageReference Include="System.Text.Json" Version="6.0.8" /> + <PackageReference Include="System.Text.Json" Version="6.0.9" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" /> diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index d7a4c9fa5..269005cfb 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -7,7 +7,7 @@ <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="Polly" Version="8.3.1" /> <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" /> @@ -18,12 +18,12 @@ <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="SixLabors.ImageSharp" Version="3.1.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <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="System.Text.Json" Version="6.0.9" /> <PackageReference Include="Npgsql" Version="7.0.4" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index e11144bcf..0ccf5c4a2 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -8,8 +8,8 @@ <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <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="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index ab05b2ca4..624151093 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -4,7 +4,7 @@ <TargetFrameworks>net6.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" /> </ItemGroup> From 6584d95331d0e0763e1688a397a3ccaf5fa6ca38 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Wed, 13 Mar 2024 23:46:33 -0500 Subject: [PATCH 187/762] New: Update Custom Format renaming token to allow excluding specific formats Closes #6615 --- .../CustomFormatsFixture.cs | 24 +++++++++++++++++ .../Organizer/FileNameBuilder.cs | 27 +++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs index fb644e104..b07fbedbe 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs @@ -93,6 +93,30 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .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<Episode> { _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<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + [TestCase("{Custom Format}", "")] [TestCase("{Custom Format:INTERNAL}", "INTERNAL")] [TestCase("{Custom Format:AMZN}", "AMZN")] diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c09978bfa..7e03172ef 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached<bool> _patternHasEpisodeIdentifierCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", @@ -698,7 +698,7 @@ 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()) @@ -717,6 +717,29 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty; } + private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter) + { + var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming); + + 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<string> mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { var tokens = new List<string>(); From 4d4d63921b45477b8f3783f4094c30ff3b85db7a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:47:01 -0700 Subject: [PATCH 188/762] Add notification for build success/failures --- .github/workflows/build.yml | 24 ++++++++++++++++++- .../MediaManagement/Naming/NamingOption.css | 4 ++-- package.json | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c65b49f2..3834c7d35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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, unit_test, unit_test_postgres, integration_test] + 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/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/package.json b/package.json index 267e78f5f..05bac8b34 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", From 6d552f2a60f44052079b5e8944f5e1bbabac56e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 12 Mar 2024 22:34:47 -0700 Subject: [PATCH 189/762] New: Show Series title and season number after task name when applicable Closes #6601 --- frontend/src/Commands/Command.ts | 2 + .../Selectors/createMultiSeriesSelector.ts | 14 + .../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 | 54 ++++ .../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 +- 14 files changed, 400 insertions(+), 459 deletions(-) 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 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/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts new file mode 100644 index 000000000..119ccd1ee --- /dev/null +++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createMultiSeriesSelector(seriesIds: number[]) { + return createSelector( + (state: AppState) => state.series.itemMap, + (state: AppState) => state.series.items, + (itemMap, allSeries) => { + return seriesIds.map((seriesId) => allSeries[itemMap[seriesId]]); + } + ); +} + +export default createMultiSeriesSelector; 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 ( - <TableRow> - <TableRowCell className={styles.trigger}> - <span className={styles.triggerContent}> - <Icon - name={triggerIcon} - title={titleCase(trigger)} - /> - - <Icon - {...getStatusIconProps(status, message)} - /> - </span> - </TableRowCell> - - <TableRowCell> - <span className={styles.commandName}> - {commandName} - </span> - { - clientUserAgent ? - <span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}> - {translate('From')}: {clientUserAgent} - </span> : - null - } - </TableRowCell> - - <TableRowCell - className={styles.queued} - title={formatDateTime(queued, longDateFormat, timeFormat)} - > - {queuedAt} - </TableRowCell> - - <TableRowCell - className={styles.started} - title={formatDateTime(started, longDateFormat, timeFormat)} - > - {startedAt} - </TableRowCell> - - <TableRowCell - className={styles.ended} - title={formatDateTime(ended, longDateFormat, timeFormat)} - > - {endedAt} - </TableRowCell> - - <TableRowCell className={styles.duration}> - {formatTimeSpan(duration)} - </TableRowCell> - - <TableRowCell - className={styles.actions} - > - { - status === 'queued' && - <IconButton - title={translate('RemovedFromTaskQueue')} - name={icons.REMOVE} - onPress={this.onCancelPress} - /> - } - </TableRowCell> - - <ConfirmModal - isOpen={isCancelConfirmModalOpen} - kind={kinds.DANGER} - title={translate('Cancel')} - message={translate('CancelPendingTask')} - confirmLabel={translate('YesCancel')} - cancelLabel={translate('NoLeaveIt')} - onConfirm={onCancelPress} - onCancel={this.onAbortCancel} - /> - </TableRow> - ); - } -} - -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<ReturnType<typeof setTimeout> | null>( + null + ); + const [times, setTimes] = useState<QueuedTimes>( + 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 ( + <TableRow> + <TableRowCell className={styles.trigger}> + <span className={styles.triggerContent}> + <Icon name={triggerIcon} title={titleCase(trigger)} /> + + <Icon {...getStatusIconProps(status, message)} /> + </span> + </TableRowCell> + + <QueuedTaskRowNameCell + commandName={commandName} + body={body} + clientUserAgent={clientUserAgent} + /> + + <TableRowCell + className={styles.queued} + title={formatDateTime(queued, longDateFormat, timeFormat)} + > + {queuedAt} + </TableRowCell> + + <TableRowCell + className={styles.started} + title={formatDateTime(started, longDateFormat, timeFormat)} + > + {startedAt} + </TableRowCell> + + <TableRowCell + className={styles.ended} + title={formatDateTime(ended, longDateFormat, timeFormat)} + > + {endedAt} + </TableRowCell> + + <TableRowCell className={styles.duration}> + {formatTimeSpan(duration)} + </TableRowCell> + + <TableRowCell className={styles.actions}> + {status === 'queued' && ( + <IconButton + title={translate('RemovedFromTaskQueue')} + name={icons.REMOVE} + onPress={openCancelConfirmModal} + /> + )} + </TableRowCell> + + <ConfirmModal + isOpen={isCancelConfirmModalOpen} + kind={kinds.DANGER} + title={translate('Cancel')} + message={translate('CancelPendingTask')} + confirmLabel={translate('YesCancel')} + cancelLabel={translate('NoLeaveIt')} + onConfirm={handleCancelPress} + onCancel={closeCancelConfirmModal} + /> + </TableRow> + ); +} 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..193c78afc --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -0,0 +1,54 @@ +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)); + + return ( + <TableRowCell> + <span className={styles.commandName}> + {commandName} + {series.length ? ( + <span> - {series.map((s) => s.title).join(', ')}</span> + ) : null} + {body.seasonNumber ? ( + <span> + {' '} + {translate('SeasonNumberToken', { + seasonNumber: body.seasonNumber, + })} + </span> + ) : null} + </span> + + {clientUserAgent ? ( + <span + className={styles.userAgent} + title={translate('TaskUserAgentTooltip')} + > + {translate('From')}: {clientUserAgent} + </span> + ) : null} + </TableRowCell> + ); +} 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 ( - <FieldSet legend={translate('Queue')}> - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - isPopulated && - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - return ( - <QueuedTaskRowConnector - key={item.id} - {...item} - /> - ); - }) - } - </TableBody> - </Table> - } - </FieldSet> - ); -} - -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 ( + <FieldSet legend={translate('Queue')}> + {isFetching && !isPopulated && <LoadingIndicator />} + + {isPopulated && ( + <Table columns={columns}> + <TableBody> + {items.map((item) => { + return <QueuedTaskRow key={item.id} {...item} />; + })} + </TableBody> + </Table> + )} + </FieldSet> + ); +} 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 ( - <QueuedTasks - {...this.props} - /> - ); - } -} - -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() { <PageContent title={translate('Tasks')}> <PageContentBody> <ScheduledTasksConnector /> - <QueuedTasksConnector /> + <QueuedTasks /> </PageContentBody> </PageContent> ); From 063dba22a803295adee4fdcbe42718af3e85ca78 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:05:15 -0700 Subject: [PATCH 190/762] Fixed: Disabled select option still selectable --- .../src/Components/Form/MonitorEpisodesSelectInput.js | 8 ++++---- .../src/Components/Form/MonitorNewItemsSelectInput.js | 8 ++++---- .../Components/Form/QualityProfileSelectInputConnector.js | 4 ++-- frontend/src/Components/Form/SeriesTypeSelectInput.tsx | 6 +++--- .../Series/Index/Select/Edit/EditSeriesModalContent.tsx | 4 ++-- .../Manage/Edit/ManageDownloadClientsEditModalContent.tsx | 2 +- .../Manage/Edit/ManageImportListsEditModalContent.tsx | 2 +- .../Manage/Edit/ManageIndexersEditModalContent.tsx | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) 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 ( - <SelectInput + <EnhancedSelectInput values={values} {...otherProps} /> 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 ( - <SelectInput + <EnhancedSelectInput values={values} {...otherProps} /> 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/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/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/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/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', From 9f705e4161af3f4dd55b399d56b0b9c5a36e181b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:15:36 -0700 Subject: [PATCH 191/762] Fixed: Release push with only Magnet URL Closes #6622 --- src/Sonarr.Api.V3/Indexers/ReleasePushController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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); From 381ce61aefd9ea3e48fbef2c13ac9c81b4442a07 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 14 Mar 2024 04:44:14 +0000 Subject: [PATCH 192/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dennis Langthjem <dennis@langthjem.dk> Co-authored-by: DimitriDR <dimitridroeck@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Ihor Mudryi <mudryy33@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 19 +++++- src/NzbDrone.Core/Localization/Core/es.json | 3 +- src/NzbDrone.Core/Localization/Core/fr.json | 3 +- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/uk.json | 58 ++++++++++++++++++- 5 files changed, 81 insertions(+), 5 deletions(-) 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/es.json b/src/NzbDrone.Core/Localization/Core/es.json index c6ab4d35a..b53c02002 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2056,5 +2056,6 @@ "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')" + "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." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 55a004379..1f66dc409 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -2056,5 +2056,6 @@ "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" + "Filters": "Filtres", + "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index d21604d83..65b22e2ad 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2056,5 +2056,6 @@ "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" + "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." } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 0f30d9911..e6b2d5d39 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -38,5 +38,61 @@ "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": "Метод автентифікації" } From e14568adef8c73eae3a93ac0c28d2ed8baff72b9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:33:43 +0200 Subject: [PATCH 193/762] Ensure not allowed cursor is shown for disabled select inputs --- frontend/src/Components/Form/EnhancedSelectInput.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 172b1a82d1d061b1b44e2a561391d6742a3d3820 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:35:22 +0200 Subject: [PATCH 194/762] Sort series by title in task name --- frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx index 193c78afc..a3e327e01 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -23,13 +23,16 @@ export default function QueuedTaskRowNameCell( } const series = useSelector(createMultiSeriesSelector(seriesIds)); + const sortedSeries = series.sort((a, b) => + a.sortTitle.localeCompare(b.sortTitle) + ); return ( <TableRowCell> <span className={styles.commandName}> {commandName} - {series.length ? ( - <span> - {series.map((s) => s.title).join(', ')}</span> + {sortedSeries.length ? ( + <span> - {sortedSeries.map((s) => s.title).join(', ')}</span> ) : null} {body.seasonNumber ? ( <span> From 58de0310fd1376d74eb100a53ecea75b38b7f47c Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 17 Mar 2024 17:58:50 +0000 Subject: [PATCH 195/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: infoaitek24 <info@aitekph.com> Co-authored-by: reloxx <reloxx@interia.pl> Co-authored-by: vfaergestad <vgf@hotmail.no> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 31 +++++++++++++------ src/NzbDrone.Core/Localization/Core/it.json | 5 +-- .../Localization/Core/nb_NO.json | 9 +++++- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 0ee4eb808..901b52856 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -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.", @@ -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.", @@ -171,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", @@ -205,7 +205,7 @@ "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", @@ -280,8 +280,8 @@ "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", @@ -540,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.", @@ -550,7 +550,7 @@ "CountIndexersSelected": "{count} Indexer ausgewählt", "CountSelectedFiles": "{selectedCount} ausgewählte Dateien", "CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“", - "CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen", + "CustomFormatsSettings": "Einstellungen für eigene Formate", "Daily": "Täglich", "Dash": "Bindestrich", "Debug": "Debuggen", @@ -709,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?", @@ -776,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/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 3cb19a328..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.", @@ -248,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/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" } From c6417337812f3578a27f9dc1e44fdad80f557271 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 16:48:35 -0700 Subject: [PATCH 196/762] Fixed: Task progress messages in the UI Closes #6632 --- .../IndexerSearch/ReleaseSearchService.cs | 2 +- src/NzbDrone.Core/Indexers/RssSyncCommand.cs | 1 - src/NzbDrone.Core/Messaging/Commands/Command.cs | 2 +- .../ProgressMessaging/ProgressMessageContext.cs | 16 +++++++++++++--- .../Tv/Commands/RefreshSeriesCommand.cs | 2 ++ .../Update/Commands/ApplicationUpdateCommand.cs | 2 -- 6 files changed, 17 insertions(+), 8 deletions(-) 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/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/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/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/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; } } From 29204c93a37881d9066d8a18a338edbbe4c0d807 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 17:15:23 -0700 Subject: [PATCH 197/762] New: Parsing multi-episode file with two and three digit episode numbers Closes #6631 --- .../ParserTests/MultiEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 4 ++++ 2 files changed, 5 insertions(+) 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/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 2989353ad..3c15ca812 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -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), From 88de9274358d7005fa9c677bb8c86f046a2a23a9 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 17:24:28 -0700 Subject: [PATCH 198/762] Fixed: Plex Watchlist import list --- src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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) From 40bac236985c682f1da06d56221ecb2ba807931e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 20 Mar 2024 16:48:01 -0700 Subject: [PATCH 199/762] New: Support parsing season number from season folder when importing Closes #903 --- .../ParserTests/PathParserFixture.cs | 3 +++ .../Organizer/FileNameBuilder.cs | 4 ++-- .../Organizer/FileNameValidation.cs | 3 +++ .../Organizer/FileNameValidationService.cs | 15 ++++++++---- src/NzbDrone.Core/Parser/Parser.cs | 24 +++++++++++++++++++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 03bc64fa5..8ef926316 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -29,6 +29,8 @@ 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:\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 +47,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/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7e03172ef..ab532f0a5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -50,10 +50,10 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", + public static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", + public static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})", 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/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3c15ca812..9104af5ed 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -554,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+)))?(?:[_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); @@ -563,6 +565,28 @@ namespace NzbDrone.Core.Parser { var fileInfo = new FileInfo(path); + // Parse using the folder and file separately, but combine if they both parse correctly. + var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name); + + if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null) + { + var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name); + + if (parsedFileInfo != null) + { + var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name); + + if (seasonMatch.Success && seasonMatch.Groups["season"].Success) + { + parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value); + + Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo); + + return parsedFileInfo; + } + } + } + var result = ParseTitle(fileInfo.Name); if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number)) From dec3fc68897a17b8b7b1e51b043bc0c6c03eec33 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 21 Mar 2024 21:21:04 -0700 Subject: [PATCH 200/762] Fixed: Don't add series from import list with no matched TVDB ID --- .../ImportListSyncServiceFixture.cs | 32 +++++++++++++++++++ .../ImportLists/ImportListSyncService.cs | 8 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) 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<ISearchForNewSeries>() + .Setup(s => s.SearchForNewSeriesByImdbId(_list1Series.First().ImdbId)) + .Returns( + Builder<Series> + .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<ISearchForNewSeries>() .Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once()); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1), It.IsAny<bool>())); } [Test] @@ -498,5 +517,18 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock<IImportListExclusionService>() .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<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>())); + } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 2c51ea6c0..eccbed341 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -190,6 +190,12 @@ namespace NzbDrone.Core.ImportLists 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 +208,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; } From cf3d51bab27e99ec719f882ff0b58291b18f5f10 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 22 Mar 2024 04:19:30 +0000 Subject: [PATCH 201/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Casselluu <jack10193@163.com> Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: infoaitek24 <info@aitekph.com> Co-authored-by: reloxx <reloxx@interia.pl> Co-authored-by: shimmyx <shimmygodx@gmail.com> Co-authored-by: vfaergestad <vgf@hotmail.no> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 10 +++++++++- src/NzbDrone.Core/Localization/Core/fr.json | 14 +++++++------- src/NzbDrone.Core/Localization/Core/zh_CN.json | 5 +++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 08c1aba65..32c883eb9 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -665,5 +665,13 @@ "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" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 1f66dc409..b6b0239e5 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -245,7 +245,7 @@ "SubtitleLanguages": "Langues des sous-titres", "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", @@ -1281,8 +1281,8 @@ "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", @@ -1355,7 +1355,7 @@ "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", @@ -1415,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}", @@ -1931,8 +1931,8 @@ "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": "Cliquer pour changer les attributs de l'indexer", - "CustomFormatsSpecificationFlag": "Attribut", + "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", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 925e731ca..78129b628 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1786,7 +1786,7 @@ "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", - "ChangeCategory": "改变分类", + "ChangeCategory": "修改分类", "IgnoreDownload": "忽略下载", "IgnoreDownloads": "忽略下载", "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", @@ -1808,5 +1808,6 @@ "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", - "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。" + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", + "AutoTaggingSpecificationOriginalLanguage": "语言" } From c403b2cdd5fa89c95a32c06dd6c4d0d60c5040be Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 27 Mar 2024 16:57:16 +0000 Subject: [PATCH 202/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Altair <villagermd@outlook.com> Co-authored-by: Dani Talens <databio@gmail.com> Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Stanislav <prekop3@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/sk/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 44 +++++++++++++++++++-- src/NzbDrone.Core/Localization/Core/es.json | 16 ++++---- src/NzbDrone.Core/Localization/Core/sk.json | 42 +++++++++++++++++++- src/NzbDrone.Core/Localization/Core/tr.json | 2 +- 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 32c883eb9..4deede861 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -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", @@ -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", @@ -673,5 +673,43 @@ "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" + "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/es.json b/src/NzbDrone.Core/Localization/Core/es.json index b53c02002..8b9fbedcf 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -537,8 +537,8 @@ "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", @@ -570,15 +570,15 @@ "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')", + "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, por defecto es '{port}'", "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{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.", @@ -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", 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 32a93393a..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", From fc6494c569324c839debdb1d08dde23b8f1b8d76 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Mar 2024 21:42:54 -0700 Subject: [PATCH 203/762] Fixed: Task with removed series causing error --- .../src/Store/Selectors/createMultiSeriesSelector.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts index 119ccd1ee..fa8235c45 100644 --- a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts +++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts @@ -1,12 +1,21 @@ 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.map((seriesId) => allSeries[itemMap[seriesId]]); + return seriesIds.reduce((acc: Series[], seriesId) => { + const series = allSeries[itemMap[seriesId]]; + + if (series) { + acc.push(series); + } + + return acc; + }, []); } ); } From d338425951af50a710c6c4411a72f05d14737ddd Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Mar 2024 21:44:23 -0700 Subject: [PATCH 204/762] Fixed: Use custom formats from import during rename --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 1f2f3cb9c..dde75161a 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); From 1335efd487a02ab66f075064781c35634494f2f1 Mon Sep 17 00:00:00 2001 From: iceypotato <nickyjedi@gmail.com> Date: Mon, 17 Jul 2023 10:54:09 -0700 Subject: [PATCH 205/762] New: My Anime List import list Closes #5148 --- .../ImportLists/ImportListSyncService.cs | 17 +++ .../MyAnimeList/MyAnimeListImport.cs | 121 ++++++++++++++++++ .../MyAnimeList/MyAnimeListParser.cs | 31 +++++ .../MyAnimeListRequestGenerator.cs | 50 ++++++++ .../MyAnimeList/MyAnimeListResponses.cs | 55 ++++++++ .../MyAnimeList/MyAnimeListSettings.cs | 58 +++++++++ .../MyAnimeList/MyAnimeListStatus.cs | 25 ++++ src/NzbDrone.Core/Localization/Core/en.json | 3 + .../MetadataSource/ISearchForNewSeries.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 7 + 10 files changed, 368 insertions(+) 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 diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index eccbed341..291ecba27 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -190,6 +190,23 @@ 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); 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<MyAnimeListSettings> + { + 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<string, string> 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<MyAnimeListAuthToken>(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<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse) + { + var jsonResponse = Json.Deserialize<MyAnimeListResponse>(importListResponse.Content); + var series = new List<ImportListItemInfo>(); + + 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<MyAnimeListStatus, string> StatusMapping = new Dictionary<MyAnimeListStatus, string> + { + { 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<ImportListRequest> 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<MyAnimeListItem> 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<MyAnimeListSettings> + { + 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<MyAnimeListSettings> 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/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ce503dd49..b5d0cb157 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -838,6 +838,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", 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<Series> SearchForNewSeriesByImdbId(string imdbId); List<Series> SearchForNewSeriesByAniListId(int aniListId); List<Series> SearchForNewSeriesByTmdbId(int tmdbId); + List<Series> 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<Series> SearchForNewSeriesByMyAnimeListId(int malId) + { + var results = SearchForNewSeries($"mal:{malId}"); + + return results; + } + public List<Series> SearchForNewSeriesByTmdbId(int tmdbId) { var results = SearchForNewSeries($"tmdb:{tmdbId}"); From 13c925b3418d1d48ec041e3d97ab51aaf2b8977a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:24:49 +0200 Subject: [PATCH 206/762] New: Advanced settings toggle in import list, notification and download client modals --- .../EditDownloadClientModalContent.js | 9 +++++++++ .../EditDownloadClientModalContentConnector.js | 17 +++++++++++++++-- .../ImportLists/EditImportListModalContent.js | 9 +++++++++ .../EditImportListModalContentConnector.js | 17 +++++++++++++++-- .../EditNotificationModalContent.js | 9 +++++++++ .../EditNotificationModalContentConnector.js | 17 +++++++++++++++-- 6 files changed, 72 insertions(+), 6 deletions(-) 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 { </Button> } + <AdvancedSettingsButton + advancedSettings={advancedSettings} + onAdvancedSettingsPress={onAdvancedSettingsPress} + showLabel={false} + /> + <SpinnerErrorButton isSpinning={isTesting} error={saveError} @@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = { onModalClose: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired, + onAdvancedSettingsPress: PropTypes.func.isRequired, onDeleteDownloadClientPress: PropTypes.func }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js index f2d4ad6ff..3c9289763 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -2,7 +2,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions'; +import { + saveDownloadClient, + setDownloadClientFieldValue, + setDownloadClientValue, + testDownloadClient, + toggleAdvancedSettings +} from 'Store/Actions/settingsActions'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import EditDownloadClientModalContent from './EditDownloadClientModalContent'; @@ -23,7 +29,8 @@ const mapDispatchToProps = { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, - testDownloadClient + testDownloadClient, + toggleAdvancedSettings }; class EditDownloadClientModalContentConnector extends Component { @@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component { this.props.testDownloadClient({ id: this.props.id }); }; + onAdvancedSettingsPress = () => { + 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/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index 2c1ab4bb0..e5a0dcd10 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -19,6 +19,7 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Popover from 'Components/Tooltip/Popover'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; +import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import translate from 'Utilities/String/translate'; import styles from './EditImportListModalContent.css'; @@ -38,6 +39,7 @@ function EditImportListModalContent(props) { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteImportListPress, ...otherProps } = props; @@ -288,6 +290,12 @@ function EditImportListModalContent(props) { </Button> } + <AdvancedSettingsButton + advancedSettings={advancedSettings} + onAdvancedSettingsPress={onAdvancedSettingsPress} + showLabel={false} + /> + <SpinnerErrorButton isSpinning={isTesting} error={saveError} @@ -327,6 +335,7 @@ EditImportListModalContent.propTypes = { onModalClose: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired, + onAdvancedSettingsPress: PropTypes.func.isRequired, onDeleteImportListPress: PropTypes.func }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js index 3a7ad8b1f..ebcbd4ae1 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js @@ -2,7 +2,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions'; +import { + saveImportList, + setImportListFieldValue, + setImportListValue, + testImportList, + toggleAdvancedSettings +} from 'Store/Actions/settingsActions'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import EditImportListModalContent from './EditImportListModalContent'; @@ -23,7 +29,8 @@ const mapDispatchToProps = { setImportListValue, setImportListFieldValue, saveImportList, - testImportList + testImportList, + toggleAdvancedSettings }; class EditImportListModalContentConnector extends Component { @@ -56,6 +63,10 @@ class EditImportListModalContentConnector extends Component { this.props.testImportList({ id: this.props.id }); }; + onAdvancedSettingsPress = () => { + 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/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index 83f5d257d..f52655289 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import NotificationEventItems from './NotificationEventItems'; import styles from './EditNotificationModalContent.css'; @@ -32,6 +33,7 @@ function EditNotificationModalContent(props) { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteNotificationPress, ...otherProps } = props; @@ -136,6 +138,12 @@ function EditNotificationModalContent(props) { </Button> } + <AdvancedSettingsButton + advancedSettings={advancedSettings} + onAdvancedSettingsPress={onAdvancedSettingsPress} + showLabel={false} + /> + <SpinnerErrorButton isSpinning={isTesting} error={saveError} @@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = { onModalClose: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired, + onAdvancedSettingsPress: PropTypes.func.isRequired, onDeleteNotificationPress: PropTypes.func }; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js index 3d6e9378e..658d72da8 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -2,7 +2,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions'; +import { + saveNotification, + setNotificationFieldValue, + setNotificationValue, + testNotification, + toggleAdvancedSettings +} from 'Store/Actions/settingsActions'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import EditNotificationModalContent from './EditNotificationModalContent'; @@ -23,7 +29,8 @@ const mapDispatchToProps = { setNotificationValue, setNotificationFieldValue, saveNotification, - testNotification + testNotification, + toggleAdvancedSettings }; class EditNotificationModalContentConnector extends Component { @@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component { this.props.testNotification({ id: this.props.id }); }; + onAdvancedSettingsPress = () => { + 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 }; From 588372fd950fc85f5e9a4275fbcb423b247ed0ee Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento <carlos5678@gmail.com> Date: Thu, 28 Mar 2024 06:28:41 +0100 Subject: [PATCH 207/762] Fixed: qBittorrent not correctly handling retention during testing --- .../Download/Clients/QBittorrent/QBittorrent.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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<OsPath> { _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<ValidationFailure> 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")) { From 35d0e6a6f806c68756450a7d199600d7fb49d6c5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 28 Mar 2024 06:29:15 +0100 Subject: [PATCH 208/762] Fixed: Handling torrents with relative path in rTorrent --- src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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(); From 1ec1ce58e9f095222e7fe4a8c74a0720fed71558 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:30:21 -0700 Subject: [PATCH 209/762] New: Add additional fields to Webhook Manual Interaction Required events --- .../Notifications/Webhook/WebhookBase.cs | 2 ++ .../Webhook/WebhookDownloadStatusMessage.cs | 18 ++++++++++++++++++ .../Webhook/WebhookManualInteractionPayload.cs | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs 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<string> 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<WebhookDownloadStatusMessage> DownloadStatusMessages { get; set; } public WebhookCustomFormatInfo CustomFormatInfo { get; set; } public WebhookGrabbedRelease Release { get; set; } } From 7353fe479dbb8d0dab76993ebed92d48e1b05524 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 28 Mar 2024 07:30:45 +0200 Subject: [PATCH 210/762] New: Allow HEAD requests to ping endpoint Closes #6656 --- src/Sonarr.Http/Ping/PingController.cs | 1 + 1 file changed, 1 insertion(+) 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() { From 060b789bc6f10f667795697eb536d4bd3851da49 Mon Sep 17 00:00:00 2001 From: Louis R <covert8@users.noreply.github.com> Date: Thu, 28 Mar 2024 06:31:28 +0100 Subject: [PATCH 211/762] Fixed: Exceptions when checking for routable IPv4 addresses --- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 36 +++++++++++++------ src/NzbDrone.Core.Test/Framework/CoreTest.cs | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 2215a953f..678e16548 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -9,6 +9,7 @@ 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; @@ -30,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers private readonly ICached<System.Net.Http.HttpClient> _httpClientCache; private readonly ICached<CredentialCache> _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; @@ -43,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers _httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher)); _credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache"); + + _logger = logger; } public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies) @@ -249,19 +255,27 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } - private static bool HasRoutableIPv4Address() + private bool HasRoutableIPv4Address() { // Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses - var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + 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))); + 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 static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + private async ValueTask<Stream> 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. @@ -285,7 +299,9 @@ namespace NzbDrone.Common.Http.Dispatchers catch { // Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections. - useIPv6 = !HasRoutableIPv4Address(); + 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.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<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger)); - Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>())); + Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger)); Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder()); } From f010f56290bd7df301391f917928d2edefc4509d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 30 Mar 2024 18:46:47 +0000 Subject: [PATCH 212/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: 王锋 <17611382361@163.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 63 ++++++++++--------- .../Localization/Core/pt_BR.json | 5 +- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 8b9fbedcf..777377345 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -324,7 +324,7 @@ "ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados", "ConnectSettings": "Conectar Ajustes", "CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'", - "XmlRpcPath": "Ruta XML RPC", + "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", @@ -530,7 +530,7 @@ "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", @@ -543,7 +543,7 @@ "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", @@ -579,8 +579,8 @@ "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, por defecto es '{port}'", - "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)", + "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.", "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", @@ -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", @@ -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", @@ -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", @@ -2057,5 +2057,8 @@ "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." + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 65b22e2ad..44c9c1038 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2057,5 +2057,8 @@ "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." + "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" } From 5a66b949cf5a08483feb1d5b24e936d3a6004892 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 1 Apr 2024 00:14:26 +0000 Subject: [PATCH 213/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index cd2552f4d..f6befb85f 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -5143,6 +5143,23 @@ } } } + }, + "head": { + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResource" + } + } + } + } + } } }, "/api/v3/qualitydefinition/{id}": { From 4e83820511b35f67478d6084fef15d547d793035 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 31 Mar 2024 21:01:14 -0700 Subject: [PATCH 214/762] Bump version to 4.0.3 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3834c7d35..74a9b33df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.2 + VERSION: 4.0.3 jobs: backend: From 60ee7cc716d344fc904fa6fb28f7be0386ae710d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:06:35 -0700 Subject: [PATCH 215/762] Fixed: Cleanse BHD RSS key in log files Closes #6666 --- .../InstrumentationTests/CleanseLogMessageFixture.cs | 2 ++ src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs | 1 + 2 files changed, 3 insertions(+) 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/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}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[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)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase), From 0937ee6fefce628b1641f87ca77a626d483f1e79 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 2 Apr 2024 17:09:07 -0700 Subject: [PATCH 216/762] Fixed: Path parsing incorrectly treating series title as episode number --- .../ParserTests/PathParserFixture.cs | 2 + .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 38 ++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 8ef926316..f2c8d2a84 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests [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) 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 9104af5ed..53dba36e3 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -555,7 +555,7 @@ namespace NzbDrone.Core.Parser 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+)))?(?:[_. ]|$)", 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); @@ -564,31 +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.Matches(fileInfo.Name); + var episodeNumberMatch = SimpleEpisodeNumberRegex.Match(fileInfo.Name); - if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null) + if (episodeNumberMatch.Success && fileInfo.Directory?.Name != null && (result == null || result.IsMiniSeries || result.AbsoluteEpisodeNumbers.Any())) { - var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name); + var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name); - if (parsedFileInfo != null) + if (seasonMatch.Success && seasonMatch.Groups["season"].Success) { - var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name); + 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 (seasonMatch.Success && seasonMatch.Groups["season"].Success) + if (first != last) { - parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value); - - Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo); - - return parsedFileInfo; + 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; } } - var result = ParseTitle(fileInfo.Name); - 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); @@ -1107,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 From 6003ca1696adde0dafb695e383007d17e274fa73 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 2 Apr 2024 20:59:08 -0700 Subject: [PATCH 217/762] Fixed: Deleted episodes not being unmonitored when series folder has been deleted Closes #6678 --- .../MediaFiles/DiskScanService.cs | 12 +++++-- .../MediaFiles/MediaFileDeletionService.cs | 34 ++++++++++--------- 2 files changed, 27 insertions(+), 19 deletions(-) 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<string>(); + + 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/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)) + var series = message.EpisodeFile.Series.Value; + var seriesPath = series.Path; + var folder = message.EpisodeFile.Path.GetParentPath(); + + while (seriesPath.IsParentPath(folder)) + { + if (_diskProvider.FolderExists(folder)) { - if (_diskProvider.FolderExists(folder)) - { - _diskProvider.RemoveEmptySubfolders(folder); - } - - folder = folder.GetParentPath(); + _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); } } } From 2ef46e5b902b42cdbafbd80b3154a5d22b090f29 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Thu, 21 Mar 2024 01:22:05 +0100 Subject: [PATCH 218/762] Fix incorrect subtitle copy regex --- src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs | 1 + src/NzbDrone.Core/Parser/LanguageParser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/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(@".+?(\.((?<tags1>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*[-_. ](?<title>[^.]*)(\.((?<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) { From 0a7f3a12c2be783c6374864e1e8f49ff5e969166 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Thu, 21 Mar 2024 02:04:17 +0100 Subject: [PATCH 219/762] Do not remove all extras when script importing --- src/NzbDrone.Core/Extras/ExistingExtraFileService.cs | 8 ++++---- src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs | 2 +- src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs | 9 +++++++-- .../Extras/Metadata/ExistingMetadataImporter.cs | 4 ++-- .../Extras/Others/ExistingOtherExtraImporter.cs | 4 ++-- .../Extras/Subtitles/ExistingSubtitleImporter.cs | 4 ++-- .../MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index ae17b44c6..14aba7b31 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<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles); + List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, bool keepExistingEntries); } public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent> @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras _logger = logger; } - public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles) + public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, bool keepExistingEntries) { _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, keepExistingEntries); 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, false); _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..d2dd17265 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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); + IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index a2dddaa69..c45f2dbc8 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,10 +19,15 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); + public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries); - public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles) + public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) { + if (keepExistingEntries) + { + return Filter(series, filesOnDisk, importedFiles, new List<TExtraFile>()); + } + var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); Clean(series, filesOnDisk, importedFiles, seriesFiles); diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 8ceb31a7f..42c1dc1ae 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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) { _logger.Debug("Looking for existing metadata in {0}", series.Path); var metadataFiles = new List<MetadataFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); 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..1ac074394 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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) { _logger.Debug("Looking for existing extra files in {0}", series.Path); var extraFiles = new List<OtherExtraFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); 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 05fdf5770..9ca3f3e67 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -29,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles public override int Order => 1; - public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) { _logger.Debug("Looking for existing subtitle files in {0}", series.Path); var subtitleFiles = new List<SubtitleFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index d591a068d..030079081 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -176,7 +176,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (localEpisode.ScriptImported) { - _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles); + _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, true); if (localEpisode.FileRenamedAfterScriptImport) { From af5a681ab7edf7f72544647c490216907853d77d Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Sun, 24 Mar 2024 15:08:59 +0100 Subject: [PATCH 220/762] Fix ignoring title based on pre-rename episodefile --- .../AggregateSubtitleInfoFixture.cs | 19 ++++++++++--------- .../Extras/ExistingExtraFileService.cs | 8 ++++---- .../Extras/IImportExistingExtraFiles.cs | 2 +- .../Extras/ImportExistingExtraFilesBase.cs | 2 +- .../Metadata/ExistingMetadataImporter.cs | 4 ++-- .../Others/ExistingOtherExtraImporter.cs | 4 ++-- .../Subtitles/ExistingSubtitleImporter.cs | 7 ++++--- .../MediaFiles/EpisodeFileMovingService.cs | 2 +- .../Aggregators/AggregateSubtitleInfo.cs | 6 +++--- .../EpisodeImport/ImportApprovedEpisodes.cs | 4 ++-- .../Parser/Model/LocalEpisode.cs | 2 +- 11 files changed, 31 insertions(+), 29 deletions(-) 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<AggregateSubtitleInfo> { - [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/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 14aba7b31..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<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, bool keepExistingEntries); + List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename); } public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent> @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras _logger = logger; } - public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, bool keepExistingEntries) + public List<string> ImportExtraFiles(Series series, List<string> 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, keepExistingEntries); + 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, false); + 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 d2dd17265..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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries); + IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index c45f2dbc8..c3ae44cba 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries); + public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename); public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) { diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 42c1dc1ae..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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing metadata in {0}", series.Path); var metadataFiles = new List<MetadataFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); + 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 1ac074394..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<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing extra files in {0}", series.Path); var extraFiles = new List<OtherExtraFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); + 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 9ca3f3e67..631c92be3 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -29,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles public override int Order => 1; - public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries) + public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing subtitle files in {0}", series.Path); var subtitleFiles = new List<SubtitleFile>(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, keepExistingEntries); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) { @@ -46,7 +46,8 @@ namespace NzbDrone.Core.Extras.Subtitles { FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile), Series = series, - Path = possibleSubtitleFile + Path = possibleSubtitleFile, + FileNameBeforeRename = fileNameBeforeRename }; try diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index dde75161a..e186246ba 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -122,6 +122,7 @@ namespace NzbDrone.Core.MediaFiles } episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); + localEpisode.FileNameBeforeRename = episodeFile.RelativePath; if (localEpisode is not null && _scriptImportDecider.TryImport(episodeFilePath, destinationFilePath, localEpisode, episodeFile, mode) is var scriptImportDecision && scriptImportDecision != ScriptImportDecision.DeferMove) { @@ -130,7 +131,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/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs index 65eaf0cca..53418a6ff 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs @@ -38,16 +38,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators 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 030079081..4cdf288fd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -176,9 +176,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (localEpisode.ScriptImported) { - _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, true); + _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/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 65f6e84f8..0129c5d0c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Parser.Model 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; } From 7776ec995571a6bc3ff1a35bbede02c05b943063 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Tue, 26 Mar 2024 15:25:23 +0100 Subject: [PATCH 221/762] Reimport files imported prematurely during script import --- src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index c3ae44cba..fdfc7e67e 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -23,13 +23,17 @@ namespace NzbDrone.Core.Extras public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> 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<TExtraFile>()); } - var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); - Clean(series, filesOnDisk, importedFiles, seriesFiles); return Filter(series, filesOnDisk, importedFiles, seriesFiles); From 1562d3bae3002947f9e428321d2b162ad69c3309 Mon Sep 17 00:00:00 2001 From: Cuki <2996147+cuki@users.noreply.github.com> Date: Sat, 6 Apr 2024 08:08:08 +0200 Subject: [PATCH 222/762] Fixed: Use widely supported display mode for PWA --- frontend/src/Content/Images/Icons/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } From 238ba85f0a2639608d9890292dfe0b96c0212f10 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 6 Apr 2024 08:08:57 +0200 Subject: [PATCH 223/762] New: Informational text on Custom Formats modal --- .../CustomFormats/EditCustomFormatModalContent.js | 5 +++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 6 insertions(+) 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 { </Form> <FieldSet legend={translate('Conditions')}> + <Alert kind={kinds.INFO}> + <div> + {translate('CustomFormatsSettingsTriggerInfo')} + </div> + </Alert> <div className={styles.customFormats}> { specifications.map((tag) => { diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b5d0cb157..5e614e84c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -278,6 +278,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", From e672996dbb5f15226f505b080f2c038c0c964300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kr=C3=BCss?= <tillkruss@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:09:55 -0700 Subject: [PATCH 224/762] Improve text for file deleted through UI/API --- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5e614e84c..983284b25 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -379,7 +379,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", From 7fc3bebc91db217a1c24ab2d01ebbc5bf03c918e Mon Sep 17 00:00:00 2001 From: fireph <443370+fireph@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:10:42 -0700 Subject: [PATCH 225/762] New: Footnote to indicate some renaming tokens support truncation --- .../MediaManagement/Naming/NamingModal.js | 55 ++++++++++++------- src/NzbDrone.Core/Localization/Core/en.json | 3 + 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index f873ec1d9..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,7 +149,7 @@ const mediaInfoTokens = [ ]; const otherTokens = [ - { token: '{Release Group}', example: 'Rls Grp' }, + { token: '{Release Group}', example: 'Rls Grp', footNote: 1 }, { token: '{Custom Formats}', example: 'iNTERNAL' }, { token: '{Custom Format:FormatName}', example: 'AMZN' } ]; @@ -305,7 +305,7 @@ class NamingModal extends Component { <FieldSet legend={translate('Series')}> <div className={styles.groups}> { - seriesTokens.map(({ token, example }) => { + seriesTokens.map(({ token, example, footNote }) => { return ( <NamingOption key={token} @@ -313,6 +313,7 @@ class NamingModal extends Component { value={value} token={token} example={example} + footNote={footNote} tokenSeparator={tokenSeparator} tokenCase={tokenCase} onPress={this.onOptionPress} @@ -322,6 +323,11 @@ class NamingModal extends Component { ) } </div> + + <div className={styles.footNote}> + <Icon className={styles.icon} name={icons.FOOTNOTE} /> + <InlineMarkdown data={translate('SeriesFootNote')} /> + </div> </FieldSet> <FieldSet legend={translate('SeriesID')}> @@ -451,7 +457,7 @@ class NamingModal extends Component { <FieldSet legend={translate('EpisodeTitle')}> <div className={styles.groups}> { - episodeTitleTokens.map(({ token, example }) => { + episodeTitleTokens.map(({ token, example, footNote }) => { return ( <NamingOption key={token} @@ -459,6 +465,7 @@ class NamingModal extends Component { value={value} token={token} example={example} + footNote={footNote} tokenSeparator={tokenSeparator} tokenCase={tokenCase} onPress={this.onOptionPress} @@ -468,6 +475,10 @@ class NamingModal extends Component { ) } </div> + <div className={styles.footNote}> + <Icon className={styles.icon} name={icons.FOOTNOTE} /> + <InlineMarkdown data={translate('EpisodeTitleFootNote')} /> + </div> </FieldSet> <FieldSet legend={translate('Quality')}> @@ -523,7 +534,7 @@ class NamingModal extends Component { <FieldSet legend={translate('Other')}> <div className={styles.groups}> { - otherTokens.map(({ token, example }) => { + otherTokens.map(({ token, example, footNote }) => { return ( <NamingOption key={token} @@ -531,6 +542,7 @@ class NamingModal extends Component { value={value} token={token} example={example} + footNote={footNote} tokenSeparator={tokenSeparator} tokenCase={tokenCase} onPress={this.onOptionPress} @@ -558,6 +570,11 @@ class NamingModal extends Component { ) } </div> + + <div className={styles.footNote}> + <Icon className={styles.icon} name={icons.FOOTNOTE} /> + <InlineMarkdown data={translate('ReleaseGroupFootNote')} /> + </div> </FieldSet> <FieldSet legend={translate('Original')}> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 983284b25..b41d10d93 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -636,6 +636,7 @@ "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", @@ -1590,6 +1591,7 @@ "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", @@ -1780,6 +1782,7 @@ "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", From a169ebff2adda5c8585c6aae6249b1c1f7c12264 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:11:03 -0700 Subject: [PATCH 226/762] Fixed: Sending ntfy.sh notifications with unicode characters Closes #6679 --- src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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()) From 74cdf01e49565c5bc63b0e90890a9e170caa3db4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:11:17 -0700 Subject: [PATCH 227/762] New: Set 'Release Type' during Manual Import Closes #6681 --- frontend/src/Episode/getReleaseTypeName.ts | 17 ++++ frontend/src/EpisodeFile/EpisodeFile.ts | 2 + .../InteractiveImportModalContent.tsx | 41 +++++++- .../Interactive/InteractiveImportRow.tsx | 44 ++++++++- .../InteractiveImport/InteractiveImport.ts | 1 + .../ReleaseType/SelectReleaseTypeModal.tsx | 30 ++++++ .../SelectReleaseTypeModalContent.tsx | 99 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../ManualImport/ManualImportController.cs | 1 + 10 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 frontend/src/Episode/getReleaseTypeName.ts create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx 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/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} /> + <SelectReleaseTypeModal + isOpen={selectModalOpen === 'releaseType'} + releaseType="unknown" + modalTitle={modalTitle} + onReleaseTypeSelect={onReleaseTypeSelect} + onModalClose={onSelectModalClose} + /> + <ConfirmModal isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 2f6f11af4..3a2b81874 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -12,6 +12,7 @@ import Episode from 'Episode/Episode'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import getReleaseTypeName from 'Episode/getReleaseTypeName'; import IndexerFlags from 'Episode/IndexerFlags'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; @@ -20,6 +21,8 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; +import ReleaseType from 'InteractiveImport/ReleaseType'; +import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; import Language from 'Language/Language'; @@ -44,7 +47,8 @@ type SelectType = | 'releaseGroup' | 'quality' | 'language' - | 'indexerFlags'; + | 'indexerFlags' + | 'releaseType'; type SelectedChangeProps = SelectStateInputProps & { hasEpisodeFileId: boolean; @@ -61,6 +65,7 @@ interface InteractiveImportRowProps { quality?: QualityModel; languages?: Language[]; size: number; + releaseType: ReleaseType; customFormats?: object[]; customFormatScore?: number; indexerFlags: number; @@ -86,6 +91,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { languages, releaseGroup, size, + releaseType, customFormats, customFormatScore, indexerFlags, @@ -315,6 +321,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { [id, dispatch, setSelectModalOpen, selectRowAfterChange] ); + const onSelectReleaseTypePress = useCallback(() => { + 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) { <TableRowCell>{formatBytes(size)}</TableRowCell> + <TableRowCellButton + title={translate('ClickToChangeReleaseType')} + onPress={onSelectReleaseTypePress} + > + {getReleaseTypeName(releaseType)} + </TableRowCellButton> + <TableRowCell> {customFormats?.length ? ( <Popover @@ -572,6 +606,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { onModalClose={onSelectModalClose} /> + <SelectReleaseTypeModal + isOpen={selectModalOpen === 'releaseType'} + releaseType={releaseType ?? 'unknown'} + modalTitle={modalTitle} + onReleaseTypeSelect={onReleaseTypeSelect} + onModalClose={onSelectModalClose} + /> + <SelectIndexerFlagsModal isOpen={selectModalOpen === 'indexerFlags'} indexerFlags={indexerFlags ?? 0} diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 1feea60c0..d9e0b1b04 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -15,6 +15,7 @@ export interface InteractiveImportCommandOptions { quality: QualityModel; languages: Language[]; indexerFlags: number; + releaseType: ReleaseType; downloadId?: string; episodeFileId?: number; } 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 ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <SelectReleaseTypeModalContent + releaseType={releaseType} + modalTitle={modalTitle} + onReleaseTypeSelect={onReleaseTypeSelect} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +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 ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader> + {modalTitle} - {translate('SelectReleaseType')} + </ModalHeader> + + <ModalBody> + <Form> + <FormGroup> + <FormLabel>{translate('ReleaseType')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="releaseType" + value={releaseType} + values={options} + onChange={handleReleaseTypeChange} + /> + </FormGroup> + </Form> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <Button kind={kinds.SUCCESS} onPress={handleReleaseTypeSelect}> + {translate('SelectReleaseType')} + </Button> + </ModalFooter> + </ModalContent> + ); +} + +export default SelectReleaseTypeModalContent; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b41d10d93..1e47f15ea 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -218,6 +218,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", @@ -1777,6 +1778,7 @@ "SelectLanguages": "Select Languages", "SelectQuality": "Select Quality", "SelectReleaseGroup": "Select Release Group", + "SelectReleaseType": "Select Release Type", "SelectSeason": "Select Season", "SelectSeasonModalTitle": "{modalTitle} - Select Season", "SelectSeries": "Select Series", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 4cdf288fd..39c3c849f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -123,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 diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index eb6787c5b..46ab91a95 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -43,6 +43,7 @@ namespace Sonarr.Api.V3.ManualImport 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); From aca10f6f4f379366edf28af1e1a4f4f9529d915b Mon Sep 17 00:00:00 2001 From: Qstick <qstick@gmail.com> Date: Sun, 9 Jul 2023 23:08:05 -0500 Subject: [PATCH 228/762] Fixed: Skip move when source and destination are the same ignore-downstream Co-Authored-By: Colin Hebert <makkhdyn@gmail.com> (cherry picked from commit 7a5ae56a96700f401726ac80b3031a25207d8f75) --- src/NzbDrone.Core/Tv/MoveSeriesService.cs | 7 +++++++ 1 file changed, 7 insertions(+) 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 From dac69445e4ab77fbce093b8dd859390e2e8fef2d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 4 Apr 2024 11:58:59 +0000 Subject: [PATCH 229/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: Michael5564445 <michaelvelosk@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fr.json | 5 ++++- src/NzbDrone.Core/Localization/Core/ro.json | 4 +++- src/NzbDrone.Core/Localization/Core/uk.json | 12 +++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index b6b0239e5..01c5df548 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -2057,5 +2057,8 @@ "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." + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index d6a639da8..183427ba3 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -198,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/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index e6b2d5d39..a514c1313 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -94,5 +94,15 @@ "AudioLanguages": "Мови аудіо", "AuthForm": "Форми (сторінка входу)", "Authentication": "Автентифікація", - "AuthenticationMethod": "Метод автентифікації" + "AuthenticationMethod": "Метод автентифікації", + "Yes": "Так", + "AuthenticationRequired": "Потрібна Автентифікація", + "UpdateAll": "Оновити все", + "WhatsNew": "Що нового ?", + "Yesterday": "Вчора", + "AddedToDownloadQueue": "Додано в чергу на завантаження", + "AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.", + "AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм", + "AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль", + "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача" } From 5c42935eb3add91051aab84b6a19cdd10e007546 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 6 Apr 2024 12:34:28 -0700 Subject: [PATCH 230/762] Fixed: Improve AniList testing with Media filters --- .../ImportLists/AniList/List/AniListImport.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) 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; + } } } From 37863a8deb339ef730b2dd5be61e1da1311fdd23 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Apr 2024 16:12:20 -0700 Subject: [PATCH 231/762] New: Option to prefix app name on Telegram notification titles --- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ .../Notifications/Telegram/Telegram.cs | 36 ++++++++++++++----- .../Notifications/Telegram/TelegramProxy.cs | 3 +- .../Telegram/TelegramSettings.cs | 3 ++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 1e47f15ea..c751eefe9 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1409,6 +1409,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", 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)); From 5061dc4b5e5ea9925740496a5939a1762788b793 Mon Sep 17 00:00:00 2001 From: Josh McKinney <joshka@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:12:58 -0700 Subject: [PATCH 232/762] Add DevContainer, VSCode config and extensions.json --- .devcontainer/devcontainer.json | 19 ++++++++++++++ .github/dependabot.yml | 12 +++++++++ .gitignore | 1 + .vscode/extensions.json | 7 ++++++ .vscode/launch.json | 26 +++++++++++++++++++ .vscode/tasks.json | 44 +++++++++++++++++++++++++++++++++ src/Directory.Build.props | 34 +++++++++++++++++++++++-- 7 files changed, 141 insertions(+), 2 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 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/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/.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/src/Directory.Build.props b/src/Directory.Build.props index ef0944ea9..239a98d73 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -175,16 +175,46 @@ </Otherwise> </Choose> + <!-- + Set architecture to RuntimeInformation.ProcessArchitecture if not specified --> + <Choose> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'"> + <PropertyGroup> + <Architecture>x64</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'"> + <PropertyGroup> + <Architecture>x86</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'"> + <PropertyGroup> + <Architecture>arm64</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'"> + <PropertyGroup> + <Architecture>arm</Architecture> + </PropertyGroup> + </When> + <Otherwise> + <PropertyGroup> + <Architecture></Architecture> + </PropertyGroup> + </Otherwise> + </Choose> + <PropertyGroup Condition="'$(IsWindows)' == 'true' and '$(RuntimeIdentifier)' == ''"> <_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier> - <RuntimeIdentifier>win-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier> </PropertyGroup> <PropertyGroup Condition="'$(IsLinux)' == 'true' and '$(RuntimeIdentifier)' == ''"> <_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier> - <RuntimeIdentifier>linux-x64</RuntimeIdentifier> + <RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier> </PropertyGroup> <PropertyGroup Condition="'$(IsOSX)' == 'true' and From f4c19a384bd9bb4e35c9fa0ca5d9a448c04e409e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 7 Apr 2024 16:22:21 -0700 Subject: [PATCH 233/762] New: Auto tag series based on tags present/absent on series Closes #6236 --- .../src/Components/Form/FormInputGroup.js | 4 ++ .../Components/Form/ProviderFieldFormGroup.js | 2 + .../src/Components/Form/SeriesTagInput.tsx | 53 +++++++++++++++++++ frontend/src/Helpers/Props/inputTypes.js | 2 + frontend/src/Settings/Tags/TagInUse.js | 2 +- .../Housekeepers/CleanupUnusedTagsFixture.cs | 33 ++++++++++++ .../Annotations/FieldDefinitionAttribute.cs | 3 +- .../Specifications/TagSpecification.cs | 36 +++++++++++++ .../Housekeepers/CleanupUnusedTags.cs | 36 +++++++++++-- src/NzbDrone.Core/Localization/Core/en.json | 1 + src/NzbDrone.Core/Tags/TagService.cs | 23 +++++++- 11 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 frontend/src/Components/Form/SeriesTagInput.tsx create mode 100644 src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs 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/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/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 + <TagInputConnector + {...otherProps} + value={finalValue} + onChange={handleChange} + /> + ); +} 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/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 ( <div> {count} {labelPlural.toLowerCase()} 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<Tag> + .CreateListOfSize(2) + .All() + .With(x => x.Id = 0) + .BuildList(); + Db.InsertMany(tags); + + var autoTags = Builder<AutoTag>.CreateListOfSize(1) + .All() + .With(x => x.Id = 0) + .With(x => x.Specifications = new List<IAutoTaggingSpecification> + { + new TagSpecification + { + Name = "Test", + Value = tags[0].Id + } + }) + .BuildList(); + + Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All()) + .Returns(autoTags); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } } } 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<TagSpecification> + { + 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/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<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") + return mapper + .Query<List<int>>( + $"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") .SelectMany(x => x) .Distinct() .ToArray(); } + + private List<int> GetAutoTaggingTagSpecificationTags(IDbConnection mapper) + { + var tags = new List<int>(); + 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/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index c751eefe9..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", 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(); + } } } From fc06e5135213f218648c8b36747d3bdf361f08b4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:13:59 +0300 Subject: [PATCH 234/762] Fixed: Renaming episodes for a series Closes #6640 --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index e186246ba..2518df234 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -122,7 +122,11 @@ namespace NzbDrone.Core.MediaFiles } episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); - localEpisode.FileNameBeforeRename = episodeFile.RelativePath; + + 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) { From 1aef91041e404f76f278f430e4e53140fb125792 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:44:07 +0300 Subject: [PATCH 235/762] New: Detect shfs mounts in disk space --- src/NzbDrone.Mono/Disk/FindDriveType.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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) { From 1fcd2b492c9c02713f708f529bcf090ca48d8523 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:09:12 +0300 Subject: [PATCH 236/762] Prevent multiple enumerations in Custom Formats token --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index ab532f0a5..29cab2edb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -706,7 +706,7 @@ namespace NzbDrone.Core.Organizer return string.Empty; } - return customFormats.Where(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat).FirstOrDefault()?.ToString() ?? string.Empty; + return customFormats.FirstOrDefault(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat)?.ToString() ?? string.Empty; }; } @@ -719,7 +719,7 @@ namespace NzbDrone.Core.Organizer private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter) { - var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming); + var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming).ToList(); var filteredTokens = tokens; From 476e7a7b94406c8424a2939939df87d9614e693f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:14:56 +0300 Subject: [PATCH 237/762] Fixed: Changing Release Type in Manage Episodes Closes #6706 --- src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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, }; } } From 4b8afe3d33ffcd311918c38f62858483bbd8265d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 8 Apr 2024 10:59:00 +0000 Subject: [PATCH 238/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: myrad2267 <myrad2267@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 10 ++++++++-- src/NzbDrone.Core/Localization/Core/fr.json | 12 +++++++++--- src/NzbDrone.Core/Localization/Core/pt_BR.json | 7 ++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 777377345..cd074ec2a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -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", @@ -2060,5 +2060,11 @@ "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" + "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." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 01c5df548..bfcedc8d1 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1102,7 +1102,7 @@ "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", @@ -1249,7 +1249,7 @@ "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", + "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", @@ -2060,5 +2060,11 @@ "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" + "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/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 44c9c1038..4e6bd8e3c 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -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", @@ -2060,5 +2060,6 @@ "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" + "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." } From 8a7b67c593c349d04bccd4270663d3671ba920d2 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Tue, 9 Apr 2024 23:17:30 +0000 Subject: [PATCH 239/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index f6befb85f..14444bdba 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8292,9 +8292,7 @@ "nullable": true }, "releaseType": { - "type": "integer", - "format": "int32", - "nullable": true + "$ref": "#/components/schemas/ReleaseType" }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" @@ -10234,6 +10232,9 @@ "isSplitEpisode": { "type": "boolean" }, + "isMiniSeries": { + "type": "boolean" + }, "special": { "type": "boolean" }, From 0fdbbd018cb56640e1a5e1f5b48b8066bc80d222 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Apr 2024 16:58:01 -0700 Subject: [PATCH 240/762] New: Parse absolute episode numbers within square brackets Closes #6694 --- .../AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 427a3e480..30c2907f2 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -134,6 +134,7 @@ namespace NzbDrone.Core.Test.ParserTests [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) @@ -179,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/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 53dba36e3..c4f61fbce 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -294,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) @@ -328,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), @@ -409,6 +405,10 @@ namespace NzbDrone.Core.Parser 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])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 9afe1c4b3fb01ebe81bf4ecbc1bd4d64ab00502b Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 12 Apr 2024 02:59:00 +0000 Subject: [PATCH 241/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: YSLG <1451164040@qq.com> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 5 ++++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 11 +++++++++-- src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index cd074ec2a..ee29b93a7 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2066,5 +2066,8 @@ "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." + "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/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 4e6bd8e3c..4797f8499 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -989,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", @@ -2061,5 +2061,12 @@ "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." + "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/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 78129b628..d04a2d4b3 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -588,7 +588,6 @@ "DownloadIgnored": "忽略下载", "DownloadIgnoredEpisodeTooltip": "集下载被忽略", "EditAutoTag": "编辑自动标签", - "AddAutoTagError": "无法添加新的自动标签,请重试。", "AddImportListExclusionError": "无法添加新排除列表,请再试一次。", "AddIndexer": "添加索引器", "AddImportList": "添加导入列表", From 6b08117d7d1502c1e9cc38949efdf364d3b5f3d4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 11 Apr 2024 16:32:28 -0700 Subject: [PATCH 242/762] Improve release notes for main releases --- .github/workflows/deploy.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 From 10daf97d81ad97e828741ae157eb6fa228320512 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 11 Apr 2024 16:32:48 -0700 Subject: [PATCH 243/762] Improve build step dependencies --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74a9b33df..12c770a7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -228,7 +228,7 @@ jobs: notify: name: Discord Notification - needs: [backend, unit_test, unit_test_postgres, integration_test] + 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' }} From 941985f65b0c5c810294a6462028698e4972b170 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 13 Apr 2024 09:29:35 -0700 Subject: [PATCH 244/762] Bump version to 4.0.4 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12c770a7b..736d5d8ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.3 + VERSION: 4.0.4 jobs: backend: From 317ce39aa26fa05d48c3a827601b71f06ea0b41a Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 15 Apr 2024 20:59:14 +0000 Subject: [PATCH 245/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Altair <villagermd@outlook.com> Co-authored-by: Fonkio <maxime.fabre10@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Jacopo Luca Maria Latrofa <jacopo.latrofa@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fr.json | 11 ++- src/NzbDrone.Core/Localization/Core/it.json | 3 +- src/NzbDrone.Core/Localization/Core/tr.json | 82 ++++++++++++++++++++- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index bfcedc8d1..975286610 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -68,11 +68,11 @@ "CancelPendingTask": "Êtes-vous sur de vouloir annuler cette tâche en attente ?", "Clear": "Effacer", "AddAutoTagError": "Impossible d'ajouter un nouveau tag automatique, veuillez réessayer.", - "AddConditionError": "Impossible d'ajouter une nouvelle condition, Réessayer.", + "AddConditionError": "Impossible d'ajouter une nouvelle condition, veuillez réessayer.", "AddCondition": "Ajouter une condition", "AddAutoTag": "Ajouter un tag automatique", "AddCustomFormatError": "Impossible d'ajouter un nouveau format personnalisé, veuillez réessayer.", - "AddIndexerError": "Impossible d'ajouter un nouvelle indexeur, veuillez réessayer.", + "AddIndexerError": "Impossible d'ajouter un nouvel indexeur, veuillez réessayer.", "AddNewRestriction": "Ajouter une nouvelle restriction", "AddListError": "Impossible d'ajouter une nouvelle liste, veuillez réessayer.", "AddDownloadClientError": "Impossible d'ajouter un nouveau client de téléchargement, veuillez réessayer.", @@ -534,7 +534,7 @@ "IndexerSearchNoInteractiveHealthCheckMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée. {appName} ne fournira aucun résultat de recherche interactif", "IndexerStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs : {indexerNames}", "Info": "Information", - "InstallLatest": "Installer le dernier", + "InstallLatest": "Installer la dernière", "InteractiveImportNoLanguage": "La ou les langues doivent être choisies pour chaque fichier sélectionné", "InteractiveImportNoQuality": "La qualité doit être choisie pour chaque fichier sélectionné", "InteractiveSearchModalHeader": "Recherche interactive", @@ -2066,5 +2066,8 @@ "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.`)." + "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.`).", + "AutoTaggingSpecificationTag": "Étiquette", + "NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 8b6adc4bf..d8113dd53 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -249,5 +249,6 @@ "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", "AutoRedownloadFailed": "Download fallito", "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", - "Cutoff": "Taglio" + "Cutoff": "Taglio", + "AddListExclusion": "Aggiungi elenco esclusioni" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 3a45b393b..adaa32070 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -136,12 +136,90 @@ "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", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter sayısı kadar güncelleyiniz. Bunu ayarlar veya yapılandırma dosyası üzerinden 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" + "CloneAutoTag": "Otomatik Etiketi Klonla", + "Dash": "Çizgi", + "DeleteReleaseProfileMessageText": "'{name}' bu sürüm profilini silmek istediğinizden emin misiniz?", + "DownloadClientFreeboxApiError": "Freebox API'si şu hatayı döndürdü: {errorDescription}", + "DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil", + "DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?", + "DeleteRootFolderMessageText": "'{path}' kök klasörünü silmek istediğinizden emin misiniz?", + "DeleteSpecificationHelpText": "'{name}' spesifikasyonunu silmek istediğinizden emin misiniz?", + "DeletedReasonUpgrade": "Bir yükseltmeyi içe aktarmak için dosya silindi", + "DelayMinutes": "{delay} Dakika", + "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", + "DeleteReleaseProfile": "Sürüm Profilini Sil", + "DeleteSelectedIndexers": "Dizin Oluşturucuları Sil", + "Directory": "Rehber", + "Donate": "Bağış yap", + "DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil", + "DownloadClientFloodSettingsAdditionalTags": "Ek Etiketler", + "DownloadClientFloodSettingsPostImportTags": "İçe Aktarma Sonrası Etiketler", + "DownloadClientFloodSettingsStartOnAdd": "Eklemeye Başla", + "DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.", + "DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın", + "DefaultNameCopiedProfile": "{name} - Kopyala", + "DeleteAutoTag": "Etiketi Otomatik Sil", + "DeleteCondition": "Koşulu Sil", + "DeleteDelayProfileMessageText": "Bu gecikme profilini silmek istediğinizden emin misiniz?", + "DeleteRootFolder": "Kök Klasörü Sil", + "DeleteSpecification": "Spesifikasyonu Sil", + "DeletedReasonManual": "Dosya, {appName} kullanılarak manuel olarak veya API aracılığıyla başka bir araçla silindi", + "DeleteCustomFormatMessageText": "'{name}' özel biçimini silmek istediğinizden emin misiniz?", + "DefaultNameCopiedSpecification": "{name} - Kopyala", + "DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?", + "DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", + "DeleteQualityProfileMessageText": "'{name}' kalite profilini silmek istediğinizden emin misiniz?", + "DeleteSelectedIndexersMessageText": "Seçilen {count} dizin oluşturucuyu silmek istediğinizden emin misiniz?", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName}, etiketi {clientName} uygulamasına ekleyemedi.", + "DownloadClientDownloadStationProviderMessage": "DSM hesabınızda 2 Faktörlü Kimlik Doğrulama etkinleştirilmişse {appName}, Download Station'a bağlanamaz", + "DownloadClientDownloadStationValidationApiVersion": "Download Station API sürümü desteklenmiyor; en az {requiredVersion} olmalıdır. {minVersion}'dan {maxVersion}'a kadar destekler", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Varsayılan hedef yok", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Medyanın özelliklerini etiket olarak ekler. İpuçları örnektir.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "İndirmelere içe aktarıldıktan sonra etiket ekler.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Flood API'sine {url} gibi bir önek ekler", + "ReplaceIllegalCharactersHelpText": "Geçersiz karakterleri değiştirin. İşaretlenmezse bunun yerine {appName} bunları kaldıracak", + "ConnectionSettingsUrlBaseHelpText": "{connectionName} URL'sine {url} gibi bir önek ekler", + "DeleteSelectedImportLists": "İçe Aktarma Listelerini Sil", + "DelayingDownloadUntil": "İndirme işlemi {date} tarihine, {time} tarihine kadar erteleniyor", + "Destination": "Hedef", + "DoNotBlocklist": "Engelleme Listesine Eklemeyin", + "DoNotBlocklistHint": "Engellenenler listesine eklemeden kaldır", + "DownloadClientDelugeTorrentStateError": "Deluge bir hata bildiriyor", + "DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", + "DeleteImportList": "İçe Aktarma Listesini Sil", + "IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan sürümler için eşitlik bozucu olarak sürümler alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", + "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", + "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation'da '{sharedFolder}' adında bir Paylaşımlı Klasör yok, bunu doğru belirttiğinizden emin misiniz?", + "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizin Oluşturucular'daki mevcut tohum kriterlerine göre torrentlerin otomatik olarak kaldırılmasını gerçekleştirecek", + "Database": "Veri tabanı", + "DelayProfileProtocol": "Protokol: {preferredProtocol}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "'{downloadDir}' klasörü mevcut değil, '{sharedFolder}' Paylaşımlı Klasöründe manuel olarak oluşturulması gerekiyor.", + "DeleteAutoTagHelpText": "'{name}' etiketini otomatik silmek istediğinizden emin misiniz?", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir", + "DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {istisnaMessage})", + "CustomFormatsSettingsTriggerInfo": "Bir yayına veya dosyaya, seçilen farklı koşul türlerinden en az biriyle eşleştiğinde Özel Format uygulanacaktır.", + "Default": "Varsayılan", + "DeleteSelectedImportListsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?", + "DownloadClientDelugeSettingsDirectory": "İndirme Dizini", + "DownloadClientDelugeSettingsDirectoryCompleted": "Tamamlandığında Dizini Taşı", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Tamamlanan indirmelerin taşınacağı isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", + "DownloadClientDelugeSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", + "ApiKey": "API Anahtarı", + "Analytics": "Analiz", + "All": "Herşey", + "AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak", + "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} Web arayüz sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız." } From d6278fced49b26be975c3a6039b38a94f700864b Mon Sep 17 00:00:00 2001 From: Josh McKinney <joshka@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:16:43 +0000 Subject: [PATCH 246/762] Add dev container workspace Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace --- .devcontainer/Sonarr.code-workspace | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .devcontainer/Sonarr.code-workspace diff --git a/.devcontainer/Sonarr.code-workspace b/.devcontainer/Sonarr.code-workspace new file mode 100644 index 000000000..a46158e44 --- /dev/null +++ b/.devcontainer/Sonarr.code-workspace @@ -0,0 +1,13 @@ +// This file is used to open the backend and frontend in the same workspace, which is necessary as +// the frontend has vscode settings that are distinct from the backend +{ + "folders": [ + { + "path": ".." + }, + { + "path": "../frontend" + } + ], + "settings": {} +} From 6c232b062c5c11b76a2f205fcd949619e4346d16 Mon Sep 17 00:00:00 2001 From: Gauthier <mail@gauthierth.fr> Date: Tue, 16 Apr 2024 05:24:05 +0200 Subject: [PATCH 247/762] New: Multi Language selection per indexer Closes #2854 --- .../IndexerTests/TestIndexerSettings.cs | 3 +++ .../BroadcastheNet/BroadcastheNetSettings.cs | 7 +++++++ .../Indexers/Fanzub/FanzubSettings.cs | 7 +++++++ .../Indexers/FileList/FileListSettings.cs | 5 +++++ .../Indexers/HDBits/HDBitsSettings.cs | 5 +++++ src/NzbDrone.Core/Indexers/IIndexerSettings.cs | 5 ++++- .../Indexers/IPTorrents/IPTorrentsSettings.cs | 7 +++++++ src/NzbDrone.Core/Indexers/IndexerBase.cs | 12 ++++++++++++ .../Indexers/Newznab/NewznabSettings.cs | 8 +++++++- .../Indexers/Nyaa/NyaaSettings.cs | 7 +++++++ .../TorrentRss/TorrentRssIndexerSettings.cs | 7 +++++++ .../Torrentleech/TorrentleechSettings.cs | 7 +++++++ .../Indexers/Torznab/TorznabSettings.cs | 6 +++--- .../Languages/RealLanguageFieldConverter.cs | 18 ++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 15 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs index 0ee1716fa..948867108 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Indexers; using NzbDrone.Core.Validation; @@ -12,5 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests } public string BaseUrl { get; set; } + + public IEnumerable<int> MultiLanguages { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index 7f4491a2b..e424a46f8 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.BroadcastheNet @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet { BaseUrl = "https://api.broadcasthe.net/"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -40,6 +44,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index 51ff19fa1..57abc672e 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Fanzub @@ -19,6 +22,7 @@ namespace NzbDrone.Core.Indexers.Fanzub public FanzubSettings() { BaseUrl = "http://fanzub.com/rss/"; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl", HelpText = "IndexerSettingsRssUrlHelpText")] @@ -28,6 +32,9 @@ namespace NzbDrone.Core.Indexers.Fanzub [FieldDefinition(1, Label = "IndexerSettingsAnimeStandardFormatSearch", Type = FieldType.Checkbox, HelpText = "IndexerSettingsAnimeStandardFormatSearchHelpText")] public bool AnimeStandardFormatSearch { get; set; } + [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 2ef02c7de..13846a25f 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.FileList @@ -35,6 +36,7 @@ namespace NzbDrone.Core.Indexers.FileList }; AnimeCategories = Array.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)] @@ -43,6 +45,9 @@ namespace NzbDrone.Core.Indexers.FileList [FieldDefinition(1, Label = "IndexerSettingsPasskey", Privacy = PrivacyLevel.ApiKey)] public string Passkey { get; set; } + [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(3, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] public string BaseUrl { get; set; } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index b5833789f..7d90cfa40 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.HDBits @@ -29,6 +30,7 @@ namespace NzbDrone.Core.Indexers.HDBits Categories = new[] { (int)HdBitsCategory.Tv, (int)HdBitsCategory.Documentary }; Codecs = Array.Empty<int>(); Mediums = Array.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -58,6 +60,9 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs index 87e7f03d2..5491b7c52 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs @@ -1,9 +1,12 @@ -using NzbDrone.Core.ThingiProvider; +using System.Collections.Generic; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers { public interface IIndexerSettings : IProviderConfig { string BaseUrl { get; set; } + + IEnumerable<int> MultiLanguages { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index 5c1271459..841c98ebf 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -1,7 +1,10 @@ +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.IPTorrents @@ -29,6 +32,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents public IPTorrentsSettings() { MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerIPTorrentsSettingsFeedUrl", HelpText = "IndexerIPTorrentsSettingsFeedUrlHelpText")] @@ -43,6 +47,9 @@ namespace NzbDrone.Core.Indexers.IPTorrents [FieldDefinition(3, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(4, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index dbb9916c0..4696bea3c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Languages; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -17,6 +20,8 @@ namespace NzbDrone.Core.Indexers public abstract class IndexerBase<TSettings> : IIndexer where TSettings : IIndexerSettings, new() { + private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + protected readonly IIndexerStatusService _indexerStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; @@ -84,9 +89,16 @@ namespace NzbDrone.Core.Indexers protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases) { var result = releases.DistinctBy(v => v.Guid).ToList(); + var settings = Definition.Settings as IIndexerSettings; result.ForEach(c => { + // Use multi languages from setting if ReleaseInfo languages is empty + if (c.Languages.Empty() && MultiRegex.IsMatch(c.Title) && settings.MultiLanguages.Any()) + { + c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + } + c.IndexerId = Definition.Id; c.Indexer = Definition.Name; c.DownloadProtocol = Protocol; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index a38229560..b329140ea 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Newznab @@ -55,6 +57,7 @@ namespace NzbDrone.Core.Indexers.Newznab ApiPath = "/api"; Categories = new[] { 5030, 5040 }; AnimeCategories = Enumerable.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "URL")] @@ -79,7 +82,10 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(6, Label = "IndexerSettingsAdditionalParameters", HelpText = "IndexerSettingsAdditionalNewznabParametersHelpText", Advanced = true)] public string AdditionalParameters { get; set; } - // Field 7 is used by TorznabSettings MinimumSeeders + [FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + + // Field 8 is used by TorznabSettings MinimumSeeders // If you need to add another field here, update TorznabSettings as well and this comment public virtual NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 6983b2d67..516c34604 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Nyaa @@ -25,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Nyaa BaseUrl = ""; AdditionalParameters = "&cats=1_0&filter=1"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -45,6 +49,9 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 898442bf7..5b3d4f3ef 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.TorrentRss @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss BaseUrl = string.Empty; AllowZeroSize = false; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl")] @@ -43,6 +47,9 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index d999d84ba..47713230d 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Torrentleech @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech { BaseUrl = "http://rss.torrentleech.org"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -40,6 +44,9 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 6ed534bb2..6a84b59cd 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -49,13 +49,13 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(7, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] + [FieldDefinition(8, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(8)] + [FieldDefinition(9)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(9, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + [FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs b/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs new file mode 100644 index 000000000..daca95472 --- /dev/null +++ b/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Languages +{ + public class RealLanguageFieldConverter : ISelectOptionsConverter + { + public List<SelectOption> GetSelectOptions() + { + return Language.All + .Where(l => l != Language.Unknown) + .OrderBy(l => l.Id > 0).ThenBy(l => l.Name) + .ToList() + .ConvertAll(v => new SelectOption { Value = v.Id, Name = v.Name }); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f6b312928..78b8557e7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -978,6 +978,8 @@ "IndexerSettingsPasskey": "Passkey", "IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", + "IndexerSettingsMultiLanguageRelease": "Multi Languages", + "IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", From d71c619f1a22825ca02f458c9217d9b32601d4be Mon Sep 17 00:00:00 2001 From: Uruk <uruknarb20@gmail.com> Date: Sun, 14 Apr 2024 12:03:16 +0200 Subject: [PATCH 248/762] Update CI dependencies --- .github/workflows/build.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/lock.yml | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 736d5d8ee..6cddbc438 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: steps: - name: Notify - uses: tsickert/discord-webhook@v5.3.0 + uses: tsickert/discord-webhook@v6.0.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} username: 'GitHub Actions' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 857cfb4a7..ab2292824 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 0435b1c71..03ec90954 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,13 +9,13 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '90' - issue-exclude-created-before: '' - issue-exclude-labels: 'one-day-maybe' - issue-lock-labels: '' - issue-lock-comment: '' + issue-inactive-days: '90' + exclude-issue-created-before: '' + exclude-any-issue-labels: 'one-day-maybe' + add-issue-labels: '' + issue-comment: '' issue-lock-reason: 'resolved' process-only: '' From 016c4b353b64a8ea3c6e1d6e8e7b4cf71901d011 Mon Sep 17 00:00:00 2001 From: Gauvino <68083474+Gauvino@users.noreply.github.com> Date: Tue, 16 Apr 2024 05:25:13 +0200 Subject: [PATCH 249/762] Add merge conflict labeler --- .github/workflows/conflict_labeler.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/conflict_labeler.yml diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml new file mode 100644 index 000000000..a19496985 --- /dev/null +++ b/.github/workflows/conflict_labeler.yml @@ -0,0 +1,24 @@ +name: Merge Conflict Labeler + +on: + push: + branches: + - develop + pull_request_target: + issue_comment: + +permissions: {} + +jobs: + label: + name: Labeling + runs-on: ubuntu-latest + if: ${{ github.repository == 'Sonarr/Sonarr' }} + steps: + - name: Apply label + uses: eps1lon/actions-label-merge-conflict@v3 + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} + with: + dirtyLabel: 'merge-conflict' + repoToken: "${{ secrets.GITHUB_TOKEN }}" + \ No newline at end of file From e9662544621b2d1fb133ff9d96d0eb20b8198725 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:43:52 +0300 Subject: [PATCH 250/762] Fixed: Re-testing edited providers will forcibly test them --- .../Creators/createTestProviderHandler.js | 24 +++++++++++++++++-- src/Sonarr.Api.V3/ProviderControllerBase.cs | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index ca26883fb..e35157dbd 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,8 +1,11 @@ +import $ from 'jquery'; +import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; +let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const testData = getProviderState(payload, getState, section); + const { + queryParams = {}, + ...otherPayload + } = payload; + + const testData = getProviderState({ ...otherPayload }, getState, section); + const params = { ...queryParams }; + + // If the user is re-testing the same provider without changes + // force it to be tested. + + if (_.isEqual(testData, lastTestData)) { + params.forceTest = true; + } + + lastTestData = testData; const ajaxOptions = { - url: `${url}/test`, + url: `${url}/test?${$.param(params, true)}`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { + lastTestData = null; + dispatch(set({ section, isTesting: false, diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index ca1082609..2622b9b02 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -205,10 +205,10 @@ namespace Sonarr.Api.V3 [SkipValidation(true, false)] [HttpPost("test")] [Consumes("application/json")] - public object Test([FromBody] TProviderResource providerResource) + public object Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false) { var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; - var providerDefinition = GetDefinition(providerResource, existingDefinition, true, true, true); + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true); Test(providerDefinition, true); From f9b013a8bfa3ea65590e4a3c34f31b2c847daeaf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 17:12:26 -0700 Subject: [PATCH 251/762] New: Parse releases with multiple Ukranian audio tracks Closes #6714 --- .../ParserTests/LanguageParserFixture.cs | 20 +++++++++++++++++++ src/NzbDrone.Core/Parser/LanguageParser.cs | 7 ++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index dce7fafc6..a7a363c9f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -429,6 +429,26 @@ namespace NzbDrone.Core.Test.ParserTests result.Languages.Should().Contain(Language.English); } + [TestCase("Остання серія (Сезон 1) / The Last Series (Season 1) (2024) WEB-DLRip-AVC 2xUkr/Eng | Sub Ukr/Eng")] + [TestCase("Справжня серія (Сезон 1-3) / True Series (Season 1-3) (2014-2019) BDRip-AVC 3xUkr/Eng | Ukr/Eng")] + [TestCase("Серія (Сезон 1-3) / The Series (Seasons 1-3) (2019-2022) BDRip-AVC 4xUkr/Eng | Sub 2xUkr/Eng")] + public void should_parse_english_and_ukranian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Languages.Count.Should().Be(2); + result.Languages.Should().Contain(Language.Ukrainian); + result.Languages.Should().Contain(Language.English); + } + + [TestCase("Серія (Сезон 1, серії 01-26 із 51) / Seri (Season 1, episodes 01-26) (2018) WEBRip-AVC 2Ukr/Tur")] + public void should_parse_turkish_and_ukranian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Languages.Count.Should().Be(2); + result.Languages.Should().Contain(Language.Ukrainian); + result.Languages.Should().Contain(Language.Turkish); + } + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 4071539c0..1548e4f82 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; - private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:ukr)\b)|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b))(?:(?i)(?![\W|_|^]SUB))", @@ -470,6 +470,11 @@ namespace NzbDrone.Core.Parser { languages.Add(Language.Latvian); } + + if (match.Groups["turkish"].Success) + { + languages.Add(Language.Turkish); + } } return languages; From ef6cc7fa3aa0c34b3b830fdf22dc3015a5039d4d Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Tue, 16 Apr 2024 03:27:46 +0000 Subject: [PATCH 252/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 14444bdba..4492f1d6a 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -1654,6 +1654,16 @@ "tags": [ "DownloadClient" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -2897,6 +2907,16 @@ "tags": [ "ImportList" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -3487,6 +3507,16 @@ "tags": [ "Indexer" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -4470,6 +4500,16 @@ "tags": [ "Metadata" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -5032,6 +5072,16 @@ "tags": [ "Notification" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { From cf6748a80ce7039eef2612d9f7720f5f391c4523 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 20:40:39 -0700 Subject: [PATCH 253/762] Fix merge conflict labeling --- .github/workflows/conflict_labeler.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml index a19496985..e9afb71a3 100644 --- a/.github/workflows/conflict_labeler.yml +++ b/.github/workflows/conflict_labeler.yml @@ -5,20 +5,22 @@ on: branches: - develop pull_request_target: - issue_comment: - -permissions: {} + branches: + - develop + types: [synchronize] jobs: label: name: Labeling runs-on: ubuntu-latest if: ${{ github.repository == 'Sonarr/Sonarr' }} + permissions: + contents: read + pull-requests: write steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@v3 - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: dirtyLabel: 'merge-conflict' - repoToken: "${{ secrets.GITHUB_TOKEN }}" + repoToken: '${{ secrets.GITHUB_TOKEN }}' \ No newline at end of file From b81c3ee4a8114f4271a517425d5ac3b81e4efeaa Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 21:13:53 -0700 Subject: [PATCH 254/762] Fix labeling config --- .github/labeler.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 3b42128d4..fdd66d11a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,17 +1,23 @@ 'connection': - - src/NzbDrone.Core/Notifications/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Notifications/**/* 'db-migration': - - src/NzbDrone.Core/Datastore/Migration/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Datastore/Migration/* 'download-client': - - src/NzbDrone.Core/Download/Clients/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Download/Clients/**/* 'indexer': - - src/NzbDrone.Core/Indexers/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Indexers/**/* 'parsing': - - src/NzbDrone.Core/Parser/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Parser/**/* 'ui-only': - - all: ['frontend/**/*'] + - changed-files: + - any-glob-to-all-files: frontend/**/* From aded9d95f7bb66cbf1768694da64403f537bcfb9 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 27 Apr 2024 18:12:21 +0000 Subject: [PATCH 255/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ano10 <arnaudthommeray+github@ik.me> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: aghus <aghus.m@outlook.com> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: maodun96 <435795439@qq.com> Co-authored-by: toeiazarothis <patrickdealmeida89000@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 49 +-- src/NzbDrone.Core/Localization/Core/es.json | 256 +++++++-------- src/NzbDrone.Core/Localization/Core/fi.json | 20 +- src/NzbDrone.Core/Localization/Core/fr.json | 26 +- .../Localization/Core/pt_BR.json | 5 +- src/NzbDrone.Core/Localization/Core/tr.json | 295 +++++++++++++++++- .../Localization/Core/zh_CN.json | 21 +- 7 files changed, 498 insertions(+), 174 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 901b52856..6bb656d91 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -23,7 +23,7 @@ "CloneCondition": "Bedingung klonen", "DeleteCondition": "Bedingung löschen", "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?", - "DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{name}' löschen willst?", + "DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?", "RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?", "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?", "DeleteSelectedDownloadClients": "Lösche Download Client(s)", @@ -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": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.", + "AutoTaggingNegateHelpText": "Falls aktiviert wird die Auto Tagging Regel nicht angewendet, solange diese Bedingung {implementationName} 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.", @@ -172,7 +172,7 @@ "BuiltIn": "Eingebaut", "ChangeFileDate": "Ändern Sie das Dateidatum", "CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden", - "DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?", + "DeleteQualityProfileMessageText": "Bist du sicher, dass du das Qualitätsprofil '{name}' wirklich löschen willst?", "DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren", "DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen", "Seeders": "Seeders", @@ -185,7 +185,7 @@ "DeleteSelectedIndexersMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte(n) Indexer löschen möchten?", "DeleteSelectedSeries": "Ausgewählte Serie löschen", "DeleteSpecification": "Spezifikation löschen", - "DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag „{label}“ löschen möchten?", + "DeleteTagMessageText": "Bist du sicher, dass du den Tag '{label}' wirklich löschen willst?", "DeletedSeriesDescription": "Die Serie wurde aus TheTVDB gelöscht", "DetailedProgressBar": "Detaillierter Fortschrittsbalken", "DetailedProgressBarHelpText": "Text auf Fortschrittsbalken anzeigen", @@ -238,9 +238,9 @@ "AddingTag": "Tag hinzufügen", "Apply": "Anwenden", "Disabled": "Deaktiviert", - "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", + "ApplyTagsHelpTextHowToApplyImportLists": "Wie Tags den selektierten Importlisten hinzugefügt werden können", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Wie Tags zu den selektierten Downloadclients hinzugefügt werden können", + "ApplyTagsHelpTextHowToApplyIndexers": "Wie Tags zu den selektierten Indexern hinzugefügt werden können", "RestrictionsLoadError": "Einschränkungen können nicht geladen werden", "SslCertPath": "SSL-Zertifikatpfad", "TheTvdb": "TheTVDB", @@ -269,7 +269,7 @@ "Connection": "Verbindung", "ConnectionLost": "Verbindung unterbrochen", "Connections": "Verbindungen", - "ContinuingOnly": "Nur Fortsetzung", + "ContinuingOnly": "Nur fortlaufend", "ContinuingSeriesDescription": "Weitere Episoden/eine weitere Staffel werden erwartet", "CopyToClipboard": "In die Zwischenablage kopieren", "CouldNotFindResults": "Es konnten keine Ergebnisse für „{term}“ gefunden werden.", @@ -300,18 +300,18 @@ "DeleteAutoTag": "Auto-Tag löschen", "DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?", "DeleteBackup": "Sicherung löschen", - "DeleteBackupMessageText": "Sind Sie sicher, dass Sie die Sicherung „{name}“ löschen möchten?", + "DeleteBackupMessageText": "Soll das Backup '{name}' wirklich gelöscht werden?", "DeleteCustomFormat": "Benutzerdefiniertes Format löschen", "DeleteDelayProfileMessageText": "Sind Sie sicher, dass Sie dieses Verzögerungsprofil löschen möchten?", "DeleteDownloadClient": "Download-Client löschen", - "DeleteDownloadClientMessageText": "Sind Sie sicher, dass Sie den Download-Client „{name}“ löschen möchten?", + "DeleteDownloadClientMessageText": "Bist du sicher, dass du den Download Client '{name}' wirklich löschen willst?", "DeleteEmptyFolders": "Leere Ordner löschen", "DeleteEpisodeFile": "Episodendatei löschen", "DeleteEpisodeFileMessage": "Sind Sie sicher, dass Sie „{path}“ löschen möchten?", "DeleteEpisodeFromDisk": "Episode von der Festplatte löschen", "DeleteEpisodesFilesHelpText": "Löschen Sie die Episodendateien und den Serienordner", "DeleteImportList": "Importliste löschen", - "DeleteIndexerMessageText": "Sind Sie sicher, dass Sie den Indexer „{name}“ löschen möchten?", + "DeleteIndexerMessageText": "Bist du sicher, dass du den Indexer '{name}' wirklich löschen willst?", "DeleteQualityProfile": "Qualitätsprofil löschen", "DeleteReleaseProfile": "Release-Profil löschen", "DeleteReleaseProfileMessageText": "Sind Sie sicher, dass Sie dieses Release-Profil „{name}“ löschen möchten?", @@ -416,12 +416,12 @@ "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deaktivieren Sie die Datumssortierung", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sie müssen die Datumssortierung für die von {appName} verwendete Kategorie deaktivieren, um Importprobleme zu vermeiden. Gehen Sie zu Sabnzbd, um das Problem zu beheben.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deaktivieren Sie die Filmsortierung", - "AllResultsAreHiddenByTheAppliedFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet", + "AllResultsAreHiddenByTheAppliedFilter": "Alle Resultate werden wegen des angewandten Filters nicht angezeigt", "RegularExpressionsCanBeTested": "Reguläre Ausdrücke können [hier] getestet werden ({url}).", "ReleaseSceneIndicatorUnknownSeries": "Unbekannte Folge oder Serie.", "RemoveFilter": "Filter entfernen", "RemoveFailedDownloadsHelpText": "Entfernen Sie fehlgeschlagene Downloads aus dem Download-Client-Verlauf", - "RemoveFromDownloadClient": "Vom Download-Client entfernen", + "RemoveFromDownloadClient": "Aus dem Download Client entfernen", "RemoveFromBlocklist": "Aus der Sperrliste entfernen", "Age": "Alter", "All": "Alle", @@ -535,11 +535,11 @@ "WeekColumnHeader": "Spaltenüberschrift „Woche“.", "Week": "Woche", "XmlRpcPath": "XML-RPC-Pfad", - "WouldYouLikeToRestoreBackup": "Möchten Sie die Sicherung „{name}“ wiederherstellen?", + "WouldYouLikeToRestoreBackup": "Willst du das Backup '{name}' wiederherstellen?", "WithFiles": "Mit Dateien", - "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).", + "ApplyTagsHelpTextAdd": "Hinzufügen: Füge Tags zu den bestehenden Tags hinzu", + "ApplyTagsHelpTextRemove": "Entfernen: Entferne die hinterlegten Tags", + "ApplyTagsHelpTextReplace": "Ersetzen: Ersetze die Tags mit den eingegebenen Tags (keine Tags eingeben um alle Tags zu löschen)", "Wanted": "Gesucht", "ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.", "Continuing": "Fortsetzung", @@ -585,7 +585,7 @@ "CollapseMultipleEpisodesHelpText": "Reduzieren Sie mehrere Episoden, die am selben Tag ausgestrahlt werden", "Connect": "Verbinden", "CreateEmptySeriesFolders": "Erstellen Sie leere Serienordner", - "DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung „{name}“ löschen möchten?", + "DeleteNotificationMessageText": "Bist du sicher, dass du die Benachrichtigung '{name}' wirklich löschen willst?", "Started": "Gestartet", "AuthenticationMethod": "Authentifizierungsmethode", "RemoveTagsAutomatically": "Tags automatisch entfernen", @@ -597,7 +597,7 @@ "CustomFormatJson": "Benutzerdefiniertes JSON-Format", "DeleteDelayProfile": "Verzögerungsprofil löschen", "DeleteIndexer": "Indexer löschen", - "DeleteImportListMessageText": "Sind Sie sicher, dass Sie die Liste „{name}“ löschen möchten?", + "DeleteImportListMessageText": "Bist du sicher, dass du die Liste '{name}' wirklich löschen willst?", "Deleted": "Gelöscht", "System": "System", "RemoveFailed": "Entferne fehlgeschlagene", @@ -642,8 +642,8 @@ "CollapseAll": "Alles reduzieren", "DeleteSeriesFolder": "Serienordner löschen", "DeleteSeriesFolderConfirmation": "Der Serienordner „{path}“ und sein gesamter Inhalt werden gelöscht.", - "AddToDownloadQueue": "Zur Download-Warteschlange hinzufügen", - "AddedToDownloadQueue": "Zur Download-Warteschlange hinzugefügt", + "AddToDownloadQueue": "Zur Download Warteschlange hinzufügen", + "AddedToDownloadQueue": "Zur Download Warteschlange hinzugefügt", "AirsDateAtTimeOn": "{date} um {time} auf {networkLabel}", "AirsTbaOn": "TBA auf {networkLabel}", "AirsTimeOn": "{time} auf {networkLabel}", @@ -787,5 +787,10 @@ "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" + "ChangeCategory": "Kategorie wechseln", + "ReplaceIllegalCharactersHelpText": "Ersetze illegale Zeichen. Wenn nicht ausgewählt, werden sie stattdessen von {appName} entfernt", + "MediaManagement": "Medienverwaltung", + "StartupDirectory": "Start-Verzeichnis", + "OnRename": "Bei Umbenennung", + "MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index ee29b93a7..8a7af7034 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -29,7 +29,7 @@ "AddImportList": "Añadir Lista de Importación", "AddImportListExclusion": "Añadir Exclusión de Lista de Importación", "AddImportListExclusionError": "No se pudo añadir una nueva exclusión de lista de importación, inténtelo de nuevo.", - "AddIndexerError": "No se pudo añadir un nuevo indexador, inténtelo de nuevo.", + "AddIndexerError": "No se pudo añadir un nuevo indexador, por favor inténtalo de nuevo.", "AddList": "Añadir Lista", "AddListExclusionError": "No se pudo añadir una nueva exclusión de lista, inténtelo de nuevo.", "AddNotificationError": "No se pudo añadir una nueva notificación, inténtelo de nuevo.", @@ -53,7 +53,7 @@ "BindAddressHelpText": "Dirección IP4 válida, localhost o '*' para todas las interfaces", "BindAddress": "Dirección de Ligado", "Branch": "Rama", - "BuiltIn": "Incorporado", + "BuiltIn": "Integrado", "Condition": "Condición", "Component": "Componente", "Custom": "Personalizado", @@ -76,9 +76,9 @@ "Trace": "Rastro", "TvdbId": "TVDB ID", "Torrents": "Torrents", - "Ui": "UI", + "Ui": "Interfaz", "Underscore": "Guion bajo", - "UpdateMechanismHelpText": "Usar el actualizador incorporado de {appName} o un script", + "UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script", "Warn": "Advertencia", "AutoTagging": "Etiquetado Automático", "AddAutoTag": "Añadir Etiqueta Automática", @@ -173,7 +173,7 @@ "Season": "Temporada", "Clone": "Clonar", "Connections": "Conexiones", - "Dash": "Guión", + "Dash": "Guion", "AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", "BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas", "BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente", @@ -185,7 +185,7 @@ "AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes", "AddQualityProfile": "Añadir Perfil de Calidad", "AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.", - "AddReleaseProfile": "Añadir Perfil de Lanzamiento", + "AddReleaseProfile": "Añadir perfil de lanzamiento", "AddSeriesWithTitle": "Añadir {title}", "AfterManualRefresh": "Tras Refrescar Manualmente", "AllSeriesInRootFolderHaveBeenImported": "Todas las series en {path} han sido importadas", @@ -216,7 +216,7 @@ "ImportScriptPath": "Importar Ruta de Script", "Absolute": "Absoluto", "AddANewPath": "Añadir una nueva ruta", - "AddConditionImplementation": "Añadir Condición - {implementationName}", + "AddConditionImplementation": "Añadir condición - {implementationName}", "AppUpdated": "{appName} Actualizado", "AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script", "AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.", @@ -233,8 +233,8 @@ "CountIndexersSelected": "{count} indexador(es) seleccionado(s)", "CouldNotFindResults": "No se pudieron encontrar resultados para '{term}'", "CountImportListsSelected": "{count} lista(s) de importación seleccionada(s)", - "DelayingDownloadUntil": "Retrasar la descarga hasta {date} a {time}", - "DeleteIndexerMessageText": "Seguro que quieres eliminar el indexer '{name}'?", + "DelayingDownloadUntil": "Retrasar la descarga hasta el {date} a las {time}", + "DeleteIndexerMessageText": "¿Estás seguro que quieres eliminar el indexador '{name}'?", "BlocklistLoadError": "No se ha podido cargar la lista de bloqueos", "BypassDelayIfAboveCustomFormatScore": "Omitir si está por encima de la puntuación del formato personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para evitar el retraso del protocolo preferido", @@ -245,8 +245,8 @@ "DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{name}'?", "DeleteQualityProfileMessageText": "¿Seguro que quieres eliminar el perfil de calidad {name}?", "DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{path}'?", - "DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {count} cliente(s) de descarga seleccionado(s)?", - "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.", + "DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?", + "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.", "CalendarOptions": "Opciones de Calendario", "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", "Default": "Por defecto", @@ -255,15 +255,15 @@ "AddImportListImplementation": "Añadir lista de importación - {implementationName}", "AddIndexerImplementation": "Agregar Indexador - {implementationName}", "AutoRedownloadFailed": "Descarga fallida", - "ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puede hacer clic en recargar abajo.", + "ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puedes pulsar en recargar abajo.", "CustomFormatJson": "Formato JSON personalizado", "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", - "DeleteImportList": "Eliminar Lista(s) de Importación", + "DeleteImportList": "Eliminar lista de importación", "DeleteImportListMessageText": "Seguro que quieres eliminar la lista '{name}'?", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Búsqueda automática e intento de descarga de una versión diferente cuando se obtiene una versión fallida de la búsqueda interactiva", "AutoRedownloadFailedFromInteractiveSearch": "Fallo al volver a descargar desde la búsqueda interactiva", - "DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?", - "DeleteSelectedImportListsMessageText": "Seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", + "DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?", + "DeleteSelectedImportListsMessageText": "¿Estás seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", "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", @@ -311,7 +311,7 @@ "CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles", "ChooseAnotherFolder": "Elige otra Carpeta", "ClientPriority": "Prioridad del Cliente", - "CloneIndexer": "Clonar Indexer", + "CloneIndexer": "Clonar indexador", "BranchUpdateMechanism": "La rama se uso por un mecanisco de actualizacion externo", "BrowserReloadRequired": "Se requiere recargar el explorador", "CalendarLoadError": "Incapaz de cargar el calendario", @@ -332,7 +332,7 @@ "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}'?", - "ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento", + "ClickToChangeReleaseGroup": "Pulsa para cambiar el grupo de lanzamiento", "ClickToChangeSeries": "Click para cambiar la serie", "ColonReplacement": "Reemplazar dos puntos", "CollapseMultipleEpisodes": "Colapsar episodios multiples", @@ -344,10 +344,10 @@ "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, los archivos bloqueados impiden renombrar los archivos que están siendo sembrados. Puedes desactivar temporalmente la siembra y usar la función de renombrado de {appName} como alternativa.", "CurrentlyInstalled": "Actualmente instalado", "CustomFilters": "Filtros Personalizados", - "CustomFormat": "Formatos Personalizados", + "CustomFormat": "Formato personalizado", "CustomFormatHelpText": "{appName} puntúa cada lanzamiento utilizando la suma de puntuaciones para hacer coincidir formatos personalizados. Si una nueva versión mejorara la puntuación, con la misma o mejor calidad, {appName} la tomará.", "CustomFormatUnknownConditionOption": "Opción Desconocida '{key}' para condición '{implementation}'", - "CustomFormatScore": "Puntuación de Formato personalizado", + "CustomFormatScore": "Puntuación de formato personalizado", "Connection": "Conexiones", "CancelPendingTask": "Estas seguro de que deseas cancelar esta tarea pendiente?", "Clear": "Borrar", @@ -356,15 +356,15 @@ "Cancel": "Cancelar", "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", + "AddListExclusion": "Añadir lista de exclusión", "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)", - "AddedToDownloadQueue": "Agregado a la cola de descarga", + "AddedToDownloadQueue": "Añadido a la cola de descarga", "AirsTimeOn": "{time} en{networkLabel}", "AirsDateAtTimeOn": "{date} en {time} en{networkLabel}", - "AddToDownloadQueue": "Agregar a la cola de descarga", + "AddToDownloadQueue": "Añadir a la cola de descarga", "Airs": "Emision", "AirsTbaOn": "A anunciar en {networkLabel}", "AllFiles": "Todos los archivos", @@ -380,17 +380,17 @@ "CalendarLegendEpisodeUnmonitoredTooltip": "El episodio esta sin monitorizar", "AnimeEpisodeTypeFormat": "Numero de episodio absoluto ({format})", "BypassDelayIfHighestQualityHelpText": "Evitar el retardo cuando el lanzamiento tiene habilitada la máxima calidad en el perfil de calidad con el protocolo preferido", - "CalendarFeed": "Feed de calendario de {appName}", + "CalendarFeed": "Canal de calendario de {appName}", "ChooseImportMode": "Elegir Modo de Importación", "ClickToChangeLanguage": "Clic para cambiar el idioma", "ClickToChangeQuality": "Clic para cambiar la calidad", "ClickToChangeSeason": "Click para cambiar la temporada", "ChmodFolderHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga establezca los permisos correctamente.", - "BlackholeWatchFolderHelpText": "Carpeta desde donde {appName} debera importar las descargas completas", + "BlackholeWatchFolderHelpText": "Carpeta desde la que {appName} debería importar las descargas completadas", "BlackholeFolderHelpText": "La carpeta en donde {appName} se almacenaran los {extension} file", "CancelProcessing": "Procesando cancelacion", - "Category": "Categoria", - "WhatsNew": "Que es lo nuevo?", + "Category": "Categoría", + "WhatsNew": "¿Qué hay nuevo?", "BlocklistReleases": "Lista de bloqueos de lanzamientos", "BypassDelayIfHighestQuality": "Pasar sí es la calidad más alta", "ChownGroupHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga use el mismo grupo que {appName}.", @@ -427,7 +427,7 @@ "Daily": "Diario", "CollapseMultipleEpisodesHelpText": "Contraer varios episodios que se emiten el mismo día", "ContinuingOnly": "Solo continuando", - "ConditionUsingRegularExpressions": "Esta condición coincide con el uso de expresiones regulares. Tenga en cuenta que los caracteres `\\^$.|?*+()[{` tienen significados especiales y deben escaparse con un `\\`", + "ConditionUsingRegularExpressions": "Esta condición coincide usando expresiones regulares. Ten en cuenta que los caracteres `\\^$.|?*+()[{` tienen significados especiales y necesitan ser escapados con un `\\`", "CountSelectedFiles": "{selectedCount} archivos seleccionados", "CreateEmptySeriesFolders": "Crear carpetas de series vacías", "CountSelectedFile": "{selectedCount} archivo seleccionado", @@ -439,7 +439,7 @@ "CutoffUnmet": "Umbrales no alcanzados", "DailyEpisodeFormat": "Formato de episodio diario", "Database": "Base de datos", - "DelayMinutes": "{delay} Minutos", + "DelayMinutes": "{delay} minutos", "Continuing": "Continua", "CustomFormats": "Formatos personalizados", "AddRootFolderError": "No se pudoagregar la carpeta raíz", @@ -465,7 +465,7 @@ "InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado", "InteractiveSearchModalHeader": "Búsqueda Interactiva", "InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración", - "ChownGroup": "Cambiar grupo propietario", + "ChownGroup": "chown grupo", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo", "ContinuingSeriesDescription": "Se esperan más episodios u otra temporada", @@ -476,16 +476,16 @@ "DeleteDelayProfile": "Eliminar Perfil de Retardo", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)", "DelayProfiles": "Perfiles de retardo", - "DeleteCustomFormatMessageText": "¿Estás seguro de que quieres eliminar el formato personalizado '{name}'?", + "DeleteCustomFormatMessageText": "¿Estás seguro que quieres eliminar el formato personalizado '{name}'?", "DeleteBackup": "Eliminar copia de seguridad", "CopyUsingHardlinksSeriesHelpText": "Los hardlinks permiten a {appName} a importar los torrents que se estén compartiendo a la carpeta de la serie sin usar espacio adicional en el disco o sin copiar el contenido completo del archivo. Los hardlinks solo funcionarán si el origen y el destino están en el mismo volumen", "DefaultDelayProfileSeries": "Este es el perfil por defecto. Aplica a todas las series que no tienen un perfil explícito.", "DelayProfileSeriesTagsHelpText": "Aplica a series con al menos una etiqueta coincidente", - "DeleteCustomFormat": "Eliminar Formato Personalizado", - "BlackholeWatchFolder": "Monitorizar Carpeta", + "DeleteCustomFormat": "Eliminar formato personalizado", + "BlackholeWatchFolder": "Monitorizar carpeta", "DeleteEmptyFolders": "Eliminar directorios vacíos", "DeleteNotification": "Borrar Notificacion", - "DeleteReleaseProfile": "Borrar perfil de estreno", + "DeleteReleaseProfile": "Eliminar perfil de lanzamiento", "Details": "Detalles", "DeleteDownloadClient": "Borrar cliente de descarga", "DeleteSelectedSeries": "Eliminar serie seleccionada", @@ -496,7 +496,7 @@ "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", "DeleteImportListExclusionMessageText": "¿Está seguro de que desea eliminar esta exclusión de la lista de importación?", "DeleteQualityProfile": "Borrar perfil de calidad", - "DeleteReleaseProfileMessageText": "Esta seguro que quiere borrar este perfil de estreno? '{name}'?", + "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar este perfil de lanzamiento '{name}'?", "DeleteRemotePathMapping": "Borrar mapeo de ruta remota", "DeleteSelectedEpisodeFiles": "Borrar los archivos de episodios seleccionados", "DeleteSelectedEpisodeFilesHelpText": "Esta seguro que desea borrar los archivos de episodios seleccionados?", @@ -506,15 +506,15 @@ "DeleteSeriesFolderHelpText": "Eliminar el directorio de series y sus contenidos", "DeleteSeriesFoldersHelpText": "Eliminar los directorios de series y sus contenidos", "DeleteSeriesModalHeader": "Borrar - {title}", - "DeleteSpecification": "Borrar especificacion", + "DeleteSpecification": "Eliminar especificación", "Directory": "Directorio", "Disabled": "Deshabilitado", "Discord": "Discord", "DiskSpace": "Espacio en Disco", - "DeleteSpecificationHelpText": "Esta seguro que desea borrar la especificacion '{name}'?", - "DeleteSelectedIndexers": "Borrar indexer(s)", - "DeleteIndexer": "Borrar Indexer", - "DeleteSelectedDownloadClients": "Borrar Cliente de Descarga(s)", + "DeleteSpecificationHelpText": "¿Estás seguro que quieres eliminar la especificación '{name}'?", + "DeleteSelectedIndexers": "Borrar indexador(es)", + "DeleteIndexer": "Borrar indexador", + "DeleteSelectedDownloadClients": "Borrar cliente(s) de descarga", "DeleteSeriesFolderCountConfirmation": "Esta seguro que desea eliminar '{count}' series seleccionadas?", "DeleteSeriesFolders": "Eliminar directorios de series", "DeletedSeriesDescription": "Serie fue eliminada de TheTVDB", @@ -533,14 +533,14 @@ "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.", + "DownloadClientDelugeValidationLabelPluginFailure": "La configuración de etiqueta falló", + "DownloadClientDelugeTorrentStateError": "Deluge está reportando un error", + "DownloadClientDownloadStationValidationFolderMissing": "La carpeta no existe", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en las opciones de DownloadStation en BT/HTTP/FTP/NZB -> Ubicació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}", + "DownloadClientFreeboxUnableToReachFreebox": "No se pudo alcanzar la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})", + "DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene múltiples 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 necesitará ser alcanzable desde XBMC", @@ -571,33 +571,33 @@ "DownloadClientCheckNoneAvailableHealthCheckMessage": "Ningún cliente de descarga disponible", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es posible comunicarse con {downloadClientName}. {errorMessage}", "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}'.", + "DownloadClientDownloadStationValidationApiVersion": "Versión API de estación de descarga no soportada, debería ser al menos {requiredVersion}. Soporta desde {minVersion} hasta {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "La carpeta '{downloadDir}' no existe, debe ser creada manualmente dentro de la carpeta compartida '{sharedFolder}'.", "DownloadClientDownloadStationValidationNoDefaultDestination": "Sin destino predeterminado", - "DownloadClientFreeboxNotLoggedIn": "No ha iniciado sesión", + "DownloadClientFreeboxNotLoggedIn": "Sin sesión iniciada", "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.", + "DownloadClientFreeboxUnableToReachFreeboxApi": "No se pudo alcanzar la API de Freebox. Verifica la opción 'URL de la API' para la URL base y la versión.", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "La configuración KeepHistory de NzbGet debería ser menor de 25000", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La configuración KeepHistory de NzbGet está establecida demasiado alta.", "UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", "DownloadClientDownloadStationProviderMessage": "{appName} no pudo conectarse a la Estación de Descarga si la Autenticación de 2 factores está habilitada en su cuenta de DSM", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Por defecto: 1. Se usa Round-Robin para clientes con la misma prioridad.", - "DownloadClientDelugeValidationLabelPluginInactive": "Extensión de etiqueta no activada", - "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de Ratio de Compartición", + "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de etiqueta no activado", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de ratio de compartición", "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Una vez alcanzada esta puntuación de formato personalizada {appName} no capturará más lanzamientos de episodios", "IndexerValidationRequestLimitReached": "Límite de petición alcanzado: {exceptionMessage}", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Debes tener la Extensión de etiqueta habilitada en {clientName} para usar categorías.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Debes tener el plugir de etiqueta habilitado en {clientName} para usar categorías.", "DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada", - "DownloadClientNzbgetValidationKeepHistoryZero": "La opción KeepHistory de NzbGet debería ser mayor que 0", + "DownloadClientNzbgetValidationKeepHistoryZero": "La configuración KeepHistory de NzbGet debería ser mayor de 0", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.", - "DownloadClientDownloadStationValidationSharedFolderMissing": "No existe la carpeta compartida", + "DownloadClientDownloadStationValidationSharedFolderMissing": "La carpeta compartida no existe", "DownloadPropersAndRepacksHelpText": "Decidir si automáticamente actualizar a Propers/Repacks", "EditListExclusion": "Editar exclusión de lista", "EnableAutomaticAdd": "Habilitar añadido automático", @@ -612,28 +612,28 @@ "EnableAutomaticAddSeriesHelpText": "Añade series de esta lista a {appName} cuando las sincronizaciones se llevan a cabo vía interfaz de usuario o por {appName}", "EditReleaseProfile": "Editar perfil de lanzamiento", "DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm", - "DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría", + "DownloadClientQbittorrentValidationCategoryAddFailure": "La configuración de categoría falló", "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.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} para rastrear nuevas descargas. Sabnzbd recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que es más efectivo.", "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.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debes deshabilitar el ordenamiento de fecha para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.", "DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo", "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", + "DownloadClientValidationSslConnectFailure": "No se pudo conectar a través de SSL", "DownloadClientsSettingsSummary": "Clientes de descarga, manejo de descarga y mapeo de rutas remotas", "EditSelectedSeries": "Editar series seleccionadas", "EnableAutomaticSearchHelpTextWarning": "Será usado cuando se use la búsqueda interactiva", "EnableColorImpairedMode": "Habilitar Modo de dificultad con los colores", "EnableMetadataHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas hasta qBittorrent versión 3.3.0. Por favor, actualice o inténtelo de nuevo con una categoría vacía.", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas por debajo de la versión 3.3.0 de qBittorrent. Por favor actualiza o inténtalo de nuevo con una categoría vacía.", "DownloadClientValidationCategoryMissing": "La categoría no existe", - "DownloadClientValidationGroupMissingDetail": "El grupo que introdujo no existe en {clientName}. Créelo primero en {clientName}", + "DownloadClientValidationGroupMissingDetail": "El grupo que introdujiste no existe en {clientName}. Créalo en {clientName} primero.", "DownloadClientValidationTestNzbs": "Fallo al obtener la lista de NZBs: {exceptionMessage}", - "DownloadClientValidationUnableToConnectDetail": "Por favor, verifique el nombre de host y el puerto.", - "DownloadClientValidationUnableToConnect": "No es posible conectarse a {clientName}", + "DownloadClientValidationUnableToConnectDetail": "Por favor verifica el nombre de host y el puerto.", + "DownloadClientValidationUnableToConnect": "No se pudo conectar a {clientName}", "DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para actualizaciones automáticas a propers/repacks", "DownloadStationStatusExtracting": "Extrayendo: {progress}%", "EditConnectionImplementation": "Editar Conexión - {implementationName}", @@ -642,49 +642,49 @@ "DoneEditingGroups": "Terminado de editar grupos", "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales", "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", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Añadir etiquetas una vez una descarga sea importada.", + "DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basados en los criterios de sembrado actuales en Opciones -> Indexadores", + "DownloadClientFloodSettingsPostImportTags": "Etiquetas post-importación", "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciales de una descarga. Para ser reconocida, una descarga debe tener todas las etiquetas iniciales. Esto evita conflictos con descargas no relacionadas.", - "DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir", + "DownloadClientFloodSettingsStartOnAdd": "Iniciar 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}", + "DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. Motivo: {errorDescription}", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone", - "DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent está reportando un error", "DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial", - "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver el enlace magnet con DHT deshabilitado", "DownloadClientQbittorrentTorrentStateStalled": "La descarga está parada sin conexiones", - "DownloadClientQbittorrentValidationCategoryRecommended": "Se recomienda una categoría", - "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.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Una categoría es recomendada", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar las descargas completadas sin una categoría.", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "El encolado de torrent no está habilitado en las opciones de tu qBittorrent. Habilítalo en qBittorrent o selecciona 'Ú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 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.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar ordenamiento de fecha", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debes deshabilitar el ordenamiento de película para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.", + "DownloadClientSettingsCategorySubFolderHelpText": "Añade una categoría específica para que {appName} evite conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero altamente recomendado. Crea un subdirectorio [categoría] en el directorio de salida.", "DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado", "DownloadClientSettingsInitialState": "Estado inicial", "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.", + "DownloadClientSettingsPostImportCategoryHelpText": "Categoría para que {appName} establezca una vez se haya importado la descarga. {appName} no eliminará torrents en esa categoría incluso si finalizó el sembrado. Dejar 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 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, 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", + "DownloadClientUTorrentTorrentStateError": "uTorrent está reportando un error", "DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta", "DownloadClientValidationApiKeyRequired": "Clave API requerida", "DownloadClientValidationAuthenticationFailure": "Fallo de autenticación", - "DownloadClientValidationCategoryMissingDetail": "La categoría que ha introducido no existe en {clientName}. Créela primero en {clientName}", + "DownloadClientValidationCategoryMissingDetail": "La categoría que has introducido no existe en {clientName}. Créala en {clientName} primero.", "DownloadClientValidationUnknownException": "Excepción desconocida: {exception}", - "DownloadClientVuzeValidationErrorVersion": "Versión de protocolo no soportada, use Vuze 5.0.0.0 o superior con la extensión Vuze Web remote.", + "DownloadClientVuzeValidationErrorVersion": "Versión de protocolo no soportada, usa Vuze 5.0.0.0 o superior con el plugin de web remota de Vuze.", "Downloaded": "Descargado", "Downloading": "Descargando", "Duration": "Duración", @@ -696,7 +696,7 @@ "EditSeriesModalHeader": "Editar - {title}", "DownloadClientQbittorrentTorrentStateUnknown": "Estado de descarga desconocido: {state}", "DownloadClientSettingsOlderPriority": "Priorizar más antiguos", - "DownloadClientSettingsRecentPriority": "Priorizar más recientes", + "DownloadClientSettingsRecentPriority": "Priorizar recientes", "EditRemotePathMapping": "Editar mapeo de ruta remota", "EditRestriction": "Editar restricción", "EnableAutomaticSearch": "Habilitar Búsqueda Automática", @@ -709,34 +709,34 @@ "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", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Encolado no habilitado", "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", + "DownloadClientQbittorrentTorrentStatePathError": "No se pudo 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 'Distribución de contenido de torrent' NO esté configurado a 'Original' o 'Crear subcarpeta'?", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar ordenamiento de película", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenamiento de TV", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Deshabilita la opción 'Verificar antes de descargar' en Sabnbzd", "DownloadClientValidationTestTorrents": "Fallo al obtener la lista de torrents: {exceptionMessage}", - "DownloadClientValidationVerifySsl": "Verificar las opciones SSL", - "DownloadClientValidationVerifySslDetail": "Por favor, verifique su configuración SSL en {clientName} y {appName}", + "DownloadClientValidationVerifySsl": "Verificar opciones de SSL", + "DownloadClientValidationVerifySslDetail": "Por favor verifica tu configuración SSL tanto en {clientName} como en {appName}", "DownloadClients": "Clientes de descarga", "DownloadFailed": "La descarga falló", "DownloadPropersAndRepacks": "Propers y repacks", - "DownloadClientValidationErrorVersion": "La versión de {clientName} debería ser al menos {requiredVersion}. La versión devuelta es {reportedVersion}", + "DownloadClientValidationErrorVersion": "La versión de {clientName} debería ser al menos {requiredVersion}. La versión reportada es {reportedVersion}", "EnableAutomaticSearchHelpText": "Será usado cuando las búsquedas automáticas sean realizadas por la interfaz de usuario o por {appName}", "EnableColorImpairedModeHelpText": "Estilo modificado para permitir que usuarios con problemas de color distingan mejor la información codificada por colores", "NotificationsDiscordSettingsUsernameHelpText": "El nombre de usuario para publicar, por defecto es el webhook predeterminado de Discord", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Debe deshabilitar la ordenación de TV para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.", - "DownloadClientSettingsCategoryHelpText": "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.", - "DownloadClientRTorrentProviderMessage": "rTorrent no pausará los torrents cuando satisfagan los criterios de siembra. {appName} manejará la eliminación automática de torrents basada en el actual criterio de siembra en Opciones ->Indexadores solo cuando Eliminar completados esté habilitado. Después de importarla también establecerá {importedView} como una vista de rTorrent, la cuál puede ser usada en los scripts de rTorrent para personalizar el comportamiento.", - "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefiere que cada descarga tenga una carpeta separada. Con * añadida a la carpeta/ruta, Sabnzbd no creará esas carpetas de trabajo. Vaya a Sabnzbd para arreglarlo.", - "DownloadClientValidationAuthenticationFailureDetail": "Por favor, verifique su nombre de usuario y contraseña. También verifique si no está bloqueado el acceso del host en ejecución {appName} a {clientName} por limitaciones en la lista blanca en la configuración de {clientName}.", - "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.", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Debes deshabilitar el ordenamiento de TV para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.", + "DownloadClientSettingsCategoryHelpText": "Añade una categoría específica para que {appName} evite conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero altamente recomendado.", + "DownloadClientRTorrentProviderMessage": "rTorrent no pausará torrents cuando cumplan el criterio de sembrado. {appName} manejará la eliminación automática de torrents basados en el criterio de sembrado actual en Opciones -> Indexadores solo cuando Eliminar completados esté habilitado. Después de importarlo también se establecerá {importedView} como una vista de rTorrent, lo que puede ser usado en los scripts de rTorrent para personalizar su comportamiento.", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefiere que cada descarga tenga una carpeta separada. Con * añadido a la carpeta/ruta, Sabnzbd no creará esas carpetas de trabajo. Ve a Sabnzbd para corregirlo.", + "DownloadClientValidationAuthenticationFailureDetail": "Por favor verifica tu usuario y contraseña. Verifica también si al host que ejecuta {appName} no se le ha bloqueado el acceso a {clientName} por limitaciones en la lista blanca en la configuración de {clientName}.", + "DownloadClientValidationSslConnectFailureDetail": "{appName} no se pudo conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor intenta 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": "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?", + "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", "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador", "EnableRss": "Habilitar RSS", @@ -883,21 +883,21 @@ "Host": "Host", "HideEpisodes": "Ocultar episodios", "Hostname": "Nombre de host", - "ICalSeasonPremieresOnlyHelpText": "Solo el primer episodio de una temporada estará en el feed", + "ICalSeasonPremieresOnlyHelpText": "Solo el primer episodio de una temporada estará en el canal", "ImportExistingSeries": "Importar series existentes", "ImportErrors": "Importar errores", "ImportList": "Importar lista", "ImportListSettings": "Importar ajustes de lista", - "ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o desde listas de Trakt y gestionar listas de exclusiones", + "ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o listas de Trakt y gestionar exclusiones de lista", "IncludeCustomFormatWhenRenamingHelpText": "Incluir en formato de renombrado {Custom Formats}", "QualityCutoffNotMet": "Calidad del umbral que no ha sido alcanzado", "SearchForCutoffUnmetEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios en Umbrales no alcanzados?", - "IndexerOptionsLoadError": "No se pudo cargar las opciones del indexador", - "IndexerIPTorrentsSettingsFeedUrl": "URL de feed", - "ICalFeed": "Feed de iCal", + "IndexerOptionsLoadError": "No se pudieron cargar las opciones del indexador", + "IndexerIPTorrentsSettingsFeedUrl": "URL del canal", + "ICalFeed": "Canal de iCal", "Import": "Importar", "ImportFailed": "La importación falló: {sourceTitle}", - "HiddenClickToShow": "Oculto, click para mostrar", + "HiddenClickToShow": "Oculto, pulsa para mostrar", "HttpHttps": "HTTP(S)", "ICalLink": "Enlace de iCal", "IconForFinalesHelpText": "Muestra un icono para finales de series/temporadas basado en la información de episodio disponible", @@ -992,16 +992,16 @@ "ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)", "Importing": "Importando", "IncludeUnmonitored": "Incluir sin monitorizar", - "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a fallos durante más de 6 horas", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas", "IRC": "IRC", "ICalShowAsAllDayEvents": "Mostrar como eventos para todo el día", "IndexerHDBitsSettingsCategories": "Categorías", "IndexerHDBitsSettingsCategoriesHelpText": "Si no se especifica, se usan todas las opciones.", "HomePage": "Página principal", "ImportSeries": "Importar series", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a fallos durante más de 6 horas: {indexerNames}", - "IndexerIPTorrentsSettingsFeedUrlHelpText": "La URL completa de feed RSS generada por IPTorrents, usa solo las categorías que seleccionaste (HD, SD, x264, etc...)", - "ICalIncludeUnmonitoredEpisodesHelpText": "Incluye episodios sin monitorizar en el feed de iCal", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores durante más de 6 horas: {indexerNames}", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "La URL completa del canal RSS generada por IPTorrents, usa solo las categorías que seleccionaste (HD, SD, x264, etc...)", + "ICalIncludeUnmonitoredEpisodesHelpText": "Incluye episodios sin monitorizar en el canal de iCal", "Forecast": "Previsión", "IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador", "IndexerHDBitsSettingsCodecs": "Códecs", @@ -1051,18 +1051,18 @@ "ImportListsAniListSettingsImportWatchingHelpText": "Lista: Viendo actualmente", "ImportListsAniListSettingsImportNotYetReleased": "Importar Aún sin lanzar", "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} de la que importar", - "IndexerPriorityHelpText": "Prioridad del Indexador de 1 (la más alta) a 50 (la más baja). Por defecto: 25. Usada para desempatar lanzamientos iguales cuando se capturan, {appName} seguirá usando todos los indexadores habilitados para Sincronización de RSS y Búsqueda", + "IndexerPriorityHelpText": "Prioridad del indexador desde 1 (la más alta) a 50 (la más baja). Predeterminado: 25. Usado para desempatar lanzamientos capturados que, de otra forma, serían iguales. {appName} aún empleará todos los indexadores habilitados para la sincronización RSS y la búsqueda", "IncludeHealthWarnings": "Incluir avisos de salud", - "IndexerJackettAllHealthCheckMessage": "Indexadores usan el endpoint de Jackett no soportado 'todo': {indexerNames}", + "IndexerJackettAllHealthCheckMessage": "Indexadores que usan el endpoint no soportado de Jackett 'all': {indexerNames}", "HourShorthand": "h", - "ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o haz click para suscribirte si tu navegador soportar WebCal", - "ICalTagsSeriesHelpText": "El feed solo contendrá series con al menos una etiqueta coincidente", + "ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o pulsa para suscribirse si tu navegador soporta Webcal", + "ICalTagsSeriesHelpText": "El canal 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": "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", + "IndexerSettings": "Opciones del indexador", + "AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, por favor inténtalo de nuevo.", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexadores con capacidad de RSS no están disponibles temporalmente debido a errores recientes con el indexador", "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hay indexadores disponibles con la sincronización RSS activada, {appName} no capturará nuevos estrenos automáticamente", "IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores", @@ -1241,7 +1241,7 @@ "Name": "Nombre", "No": "No", "NotificationTriggers": "Disparadores de notificación", - "ClickToChangeIndexerFlags": "Clic para cambiar las banderas del indexador", + "ClickToChangeIndexerFlags": "Pulsa para cambiar los indicadores del indexador", "MinutesSixty": "60 minutos: {sixty}", "MonitorMissingEpisodesDescription": "Monitoriza episodios que no tienen archivos o que no se han emitido aún", "MoveFiles": "Mover archivos", @@ -1252,16 +1252,16 @@ "NoMatchFound": "¡Ninguna coincidencia encontrada!", "NoMinimumForAnyRuntime": "No hay mínimo para ningún tiempo de ejecución", "NoMonitoredEpisodes": "No hay episodios monitorizados en esta serie", - "NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponible debido a fallos: {notificationNames}", - "CustomFormatsSpecificationFlag": "Bandera", + "NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponibles debido a errores: {notificationNames}", + "CustomFormatsSpecificationFlag": "Indicador", "Never": "Nunca", "MinimumAge": "Edad mínima", "Mixed": "Mezclado", "MultiLanguages": "Multi-idiomas", "NoEpisodesFoundForSelectedSeason": "No se encontró ningún episodio para la temporada seleccionada", "NoEventsFound": "Ningún evento encontrado", - "IndexerFlags": "Banderas del indexador", - "CustomFilter": "Filtros personalizados", + "IndexerFlags": "Indicadores del indexador", + "CustomFilter": "Filtro personalizado", "Filters": "Filtros", "Label": "Etiqueta", "MonitorExistingEpisodes": "Episodios existentes", @@ -1285,7 +1285,7 @@ "NoChanges": "Sin cambios", "NoEpisodeOverview": "No hay sinopsis de episodio", "MultiEpisodeStyle": "Estilo de multi-episodio", - "NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos", + "NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a errores", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Clave de configuración para la Solución de almacenamiento persistente. Dejar vacío si se usan URLs sin estado.", "NotificationsAppriseSettingsPasswordHelpText": "Autenticación básica HTTP de contraseña", "Monitoring": "Monitorizando", @@ -1648,11 +1648,11 @@ "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", + "SelectIndexerFlags": "Seleccionar indicadores 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", + "SetIndexerFlags": "Establecer indicadores del indexador", "SkipRedownload": "Saltar redescarga", "ShowMonitored": "Mostrar monitorizado", "Space": "Espacio", @@ -1795,7 +1795,7 @@ "Reason": "Razón", "RegularExpression": "Expresión regular", "ReleaseHash": "Hash de lanzamiento", - "Rejections": "Rechazos", + "Rejections": "Rechazados", "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", @@ -1879,7 +1879,7 @@ "SelectFolderModalTitle": "{modalTitle} - Seleccionar carpeta", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio", "SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)", - "SetIndexerFlagsModalTitle": "{modalTitle} - Establecer banderas del indexador", + "SetIndexerFlagsModalTitle": "{modalTitle} - Establecer indicadores 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", @@ -1931,7 +1931,7 @@ "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", + "Repack": "Repack", "NotificationsGotifySettingsPriorityHelpText": "Prioridad de la notificación", "NotificationsGotifySettingsServer": "Servidor Gotify", "NotificationsPlexSettingsAuthToken": "Token de autenticación", @@ -2044,7 +2044,7 @@ "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.", + "UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de translocación de aplicaciones.", "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.", @@ -2057,7 +2057,7 @@ "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.", + "ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento causará 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", @@ -2069,5 +2069,7 @@ "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" + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones", + "IndexerSettingsMultiLanguageRelease": "Múltiples idiomas", + "IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index adbb36ef8..b92f93d57 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -359,7 +359,7 @@ "CountSeasons": "{count} kautta", "CountIndexersSelected": "{count} tietolähde(ttä) on valittu", "SetTags": "Tunnisteiden määritys", - "Monitored": "Valvotut", + "Monitored": "Valvonta", "ApplyTagsHelpTextHowToApplyDownloadClients": "Tunnisteiden käyttö valituissa lataustyökaluissa", "ApplyTagsHelpTextHowToApplyImportLists": "Tunnisteiden käyttö valituissa tuontilistoissa", "ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituissa sarjoissa", @@ -590,7 +590,7 @@ "DownloadClientFloodSettingsAdditionalTags": "Lisätunnisteet", "DownloadClientFloodSettingsPostImportTagsHelpText": "Sisällyttää tunnisteet kun lataus on tuotu.", "DownloadClientFloodSettingsStartOnAdd": "Käynnistä lisättäessä", - "ClickToChangeQuality": "Vaihda laatua klikkaamalla", + "ClickToChangeQuality": "Vaihda laatua painamalla tästä", "EpisodeDownloaded": "Jakso on ladattu", "InteractiveImportNoQuality": "Jokaisen valitun tiedoston laatu on määritettävä.", "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.", @@ -1335,7 +1335,7 @@ "NotificationsTelegramSettingsSendSilently": "Lähetä äänettömästi", "NotificationsTelegramSettingsTopicId": "Ketjun ID", "ProcessingFolders": "Käsittelykansiot", - "Preferred": "Haluttu", + "Preferred": "Tavoite", "SslCertPasswordHelpText": "Pfx-tiedoston salasana", "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".", "TorrentBlackholeSaveMagnetFilesReadOnly": "Vain luku", @@ -1768,7 +1768,7 @@ "ResetDefinitions": "Palauta määritykset", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent on määritetty poistamaan torrentit niiden saavuttaessa niitä koskevan jakosuhderajoituksen.", "SecretToken": "Salainen tunniste", - "ShownClickToHide": "Näkyvissä, piilota painamalla", + "ShownClickToHide": "Näytetään, piilota painamalla tästä", "DownloadClientValidationAuthenticationFailure": "Tunnistautuminen epäonnistui", "DownloadClientValidationApiKeyRequired": "Rajapinnan avain on pakollinen", "DownloadClientValidationVerifySsl": "Vahvista SSL-asetukset", @@ -1794,7 +1794,7 @@ "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää etuliite lataustyökalun {clientName} RPC-URL-osoitteeseen. Esimerkiksi {url}. Oletus on \"{defaultUrl}\".", "DownloadClientValidationApiKeyIncorrect": "Rajapinnan avain ei kelpaa", - "HiddenClickToShow": "Piilotettu, näytä painalla", + "HiddenClickToShow": "Piilotettu, näytä painamalla tästä", "ImportListsCustomListValidationAuthenticationFailure": "Tunnistautuminen epäonnistui", "ImportListsSonarrSettingsApiKeyHelpText": "{appName}-instanssin, josta tuodaan, rajapinan (API) avain.", "IndexerSettingsApiUrlHelpText": "Älä muuta tätä, jos et tiedä mitä teet, koska rajapinta-avaimesi lähetetään kyseiselle palvelimelle.", @@ -1802,5 +1802,13 @@ "Required": "Pakollinen", "TaskUserAgentTooltip": "User-Agent-tiedon ilmoitti rajapinnan kanssa viestinyt sovellus.", "TorrentBlackhole": "Torrent Blackhole", - "WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats)." + "WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats).", + "LabelIsRequired": "Nimi on pakollinen", + "SetIndexerFlags": "Aseta tietolähteen liput", + "ClickToChangeIndexerFlags": "Vaihda tietolähteen lippuja painamalla tästä", + "CustomFormatsSpecificationFlag": "Lippu", + "SelectIndexerFlags": "Valitse tietolähteen liput", + "SetIndexerFlagsModalTitle": "{modalTitle} - Aseta tietolähteen liput", + "CustomFilter": "Oma suodatin", + "Label": "Nimi" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 975286610..10fd9c8eb 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -299,12 +299,12 @@ "SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre", "Sunday": "Dimanche", "TorrentDelay": "Retard du torrent", - "DownloadClients": "Clients de télécharg.", + "DownloadClients": "Clients de téléchargement", "CustomFormats": "Formats perso.", "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", @@ -335,7 +335,7 @@ "EditGroups": "Modifier les groupes", "False": "Faux", "Example": "Exemple", - "FileNameTokens": "Tokens des noms de fichier", + "FileNameTokens": "Jetons de nom de fichier", "FileNames": "Noms de fichier", "Extend": "Étendu", "FileManagement": "Gestion de fichiers", @@ -343,7 +343,7 @@ "FailedToLoadQualityProfilesFromApi": "Échec du chargement des profils de qualité depuis l'API", "Filename": "Nom de fichier", "FailedToLoadTagsFromApi": "Échec du chargement des étiquettes depuis l'API", - "FormatTimeSpanDays": "{days}j {time}", + "FormatTimeSpanDays": "{days} j {time}", "FormatShortTimeSpanSeconds": "{seconds} seconde(s)", "FilterEqual": "égale", "Implementation": "Mise en œuvre", @@ -362,7 +362,7 @@ "Lowercase": "Minuscule", "MaximumSizeHelpText": "Taille maximale d'une version à récupérer en Mo. Régler sur zéro pour définir sur illimité", "MissingNoItems": "Aucun élément manquant", - "MoveAutomatically": "Se déplacer automatiquement", + "MoveAutomatically": "Importation automatique", "MoreInfo": "Plus d'informations", "NoHistory": "Aucun historique", "MonitoredStatus": "Surveillé/Statut", @@ -528,7 +528,7 @@ "IndexerDownloadClientHelpText": "Spécifiez quel client de téléchargement est utilisé pour les récupérations à partir de cet indexeur", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison de pannes pendant plus de 6 heures", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs pendant plus de six heures : {indexerNames}", - "IndexerPriorityHelpText": "Priorité de l'indexeur de 1 (la plus élevée) à 50 (la plus basse). Valeur par défaut : 25. Utilisé lors de la récupération des versions comme départage pour des versions par ailleurs égales, {appName} utilisera toujours tous les indexeurs activés pour la synchronisation RSS et la recherche", + "IndexerPriorityHelpText": "Priorité de l'indexeur de 1 (la plus élevée) à 50 (la plus basse). Par défaut : 25. Utilisé lors de la récupération de versions pour départager des versions égales, {appName} utilisera toujours tous les indexeurs activés pour la synchronisation et la recherche RSS", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Tous les indexeurs compatibles RSS sont temporairement indisponibles en raison d'erreurs récentes de l'indexeur", "IndexerSearchNoAutomaticHealthCheckMessage": "Aucun indexeur disponible avec la recherche automatique activée, {appName} ne fournira aucun résultat de recherche automatique", "IndexerSearchNoInteractiveHealthCheckMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée. {appName} ne fournira aucun résultat de recherche interactif", @@ -930,7 +930,7 @@ "NoLeaveIt": "Non, laisse tomber", "NotificationsLoadError": "Impossible de charger les notifications", "OnEpisodeFileDeleteForUpgrade": "Lors de la suppression du fichier de l'épisode pour la mise à niveau", - "OnGrab": "À saisir", + "OnGrab": "Lors de la saisie", "OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse", "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici]({url}).", "ReleaseProfileIndexerHelpText": "Spécifier l'indexeur auquel le profil s'applique", @@ -968,7 +968,7 @@ "Ungroup": "Dissocier", "Folder": "Dossier", "FullColorEvents": "Événements en couleur", - "GeneralSettingsSummary": "Port, SSL/TLS, nom d'utilisateur/mot de passe, proxy, analyses et mises à jour", + "GeneralSettingsSummary": "Port, SSL, nom d'utilisateur/mot de passe, proxy, analyses et mises à jour", "HistoryModalHeaderSeason": "Historique {season}", "HistorySeason": "Afficher l'historique de cette saison", "Images": "Images", @@ -1249,7 +1249,7 @@ "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é à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API.", + "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", @@ -1743,7 +1743,7 @@ "NotificationsMailgunSettingsUseEuEndpointHelpText": "Activer pour utiliser le point de terminaison MailGun européen", "NotificationsMailgunSettingsSenderDomain": "Domaine de l'expéditeur", "NotificationsMailgunSettingsApiKeyHelpText": "La clé API générée depuis MailGun", - "NotificationsKodiSettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque dans Importer & Renommer ?", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors de l'importation et du renommage ?", "NotificationsKodiSettingsGuiNotification": "Notification GUI", "NotificationsKodiSettingsDisplayTime": "Temps d'affichage", "NotificationsKodiSettingsCleanLibraryHelpText": "Nettoyer la bibliothèque après une mise à jour", @@ -1918,7 +1918,7 @@ "IgnoreDownload": "Ignorer le téléchargement", "IgnoreDownloads": "Ignorer les téléchargements", "IgnoreDownloadsHint": "Empêche {appName} de poursuivre le traitement de ces téléchargements", - "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrents bloqués lors de la saisie", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrent sur liste noir lors de la saisie", "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.", @@ -2069,5 +2069,7 @@ "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.`).", "AutoTaggingSpecificationTag": "Étiquette", "NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre", - "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications" + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications", + "IndexerSettingsMultiLanguageRelease": "Multilingue", + "IndexerSettingsMultiLanguageReleaseHelpText": "Quelles langues sont normalement présentes dans une version multiple de l'indexeur ?" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 4797f8499..c30a75c60 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2068,5 +2068,8 @@ "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." + "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.", + "IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?", + "AutoTaggingSpecificationTag": "Etiqueta", + "IndexerSettingsMultiLanguageRelease": "Multi Idiomas" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index adaa32070..dd97a1d37 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -11,7 +11,7 @@ "EditIndexerImplementation": "Koşul Ekle - {implementationName}", "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", "AddedToDownloadQueue": "İndirme sırasına eklendi", - "AllTitles": "Tüm Filmler", + "AllTitles": "Tüm Başlıklar", "AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları", "Actions": "Eylemler", "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası", @@ -24,8 +24,8 @@ "AirDate": "Yayınlanma Tarihi", "Add": "Ekle", "AddingTag": "Etiket ekleniyor", - "Age": "Yaş", - "AgeWhenGrabbed": "Yaş (yakalandığında)", + "Age": "Yıl", + "AgeWhenGrabbed": "Yıl (yakalandığında)", "AddDelayProfileError": "Yeni bir gecikme profili eklenemiyor, lütfen tekrar deneyin.", "AddImportList": "İçe Aktarım Listesi Ekle", "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", @@ -219,7 +219,292 @@ "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", "ApiKey": "API Anahtarı", "Analytics": "Analiz", - "All": "Herşey", + "All": "Hepsi", "AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak", - "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} Web arayüz sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız." + "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.", + "Backup": "Yedek", + "BindAddress": "Bind Adresi", + "DownloadClientFreeboxSettingsApiUrl": "API URL'si", + "DownloadClientFreeboxSettingsAppId": "Uygulama kimliği", + "DownloadClientFreeboxNotLoggedIn": "Giriş yapmadınız", + "DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')", + "Apply": "Uygula", + "DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}", + "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')", + "DownloadClientFreeboxSettingsHostHelpText": "Freebox'un ana bilgisayar adı veya ana bilgisayar IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağdaysa çalışır)", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox API temel URL'sini API sürümüyle tanımlayın, örneğin '{url}', varsayılan olarak '{defaultApiUrl}' olur", + "BlocklistReleases": "Kara Liste Sürümü", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet ayarı KeepHistory 0 olarak ayarlandı. Bu, {appName}'in tamamlanan indirmeleri görmesini engelliyor.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Güvenli bir bağlantı kullanın. qBittorrent'te Seçenekler -> Web Kullanıcı Arayüzü -> 'HTTP yerine HTTPS kullan' bölümüne bakın.", + "DownloadClientValidationCategoryMissingDetail": "Girdiğiniz kategori {clientName} içinde mevcut değil. Önce bunu {clientName} içinde oluşturun.", + "EditDownloadClientImplementation": "İndirme İstemcisini Düzenle - {implementationName}", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Kategori desteklenmiyor", + "DownloadClientValidationCategoryMissing": "Kategori mevcut değil", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Bu seçenek en az NzbGet sürüm 16.0'ı gerektirir", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Freebox API'sine ulaşılamıyor. Temel URL ve sürüm için 'API URL'si' ayarını doğrulayın.", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGet KeepHistory ayarı çok yüksek ayarlanmış.", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb Klasörü", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Bu klasördeki .strm dosyaları drone ile içe aktarılacak", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Önce ilk ve son parçaları indirin (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent, DHT devre dışıyken magnet bağlantısını çözemiyor", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent meta verileri indiriyor", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent bir hata bildiriyor", + "DownloadClientQbittorrentTorrentStateStalled": "Bağlantı kesildi indirme işlemi durduruldu", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Kategori yapılandırması başarısız oldu", + "DownloadClientQbittorrentTorrentStateUnknown": "Bilinmeyen indirme durumu: {state}", + "DownloadClientQbittorrentValidationCategoryRecommended": "Kategori önerilir", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName}, tamamlanan indirmeleri kategorisi olmadan içe aktarmaya çalışmaz.", + "DownloadClientRTorrentSettingsAddStopped": "Ekleme Durduruldu", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Tarih Sıralamayı Devre Dışı Bırak", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Film sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.", + "DownloadClientSabnzbdValidationUnknownVersion": "Bilinmeyen Sürüm: {rawVersion}", + "DownloadClientSabnzbdValidationEnableJobFolders": "İş klasörlerini etkinleştir", + "DownloadClientSettingsAddPaused": "Ekleme Durduruldu", + "DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın", + "DownloadClientSettingsInitialState": "Başlangıç Durumu", + "DownloadClientSettingsPostImportCategoryHelpText": "{appName}'in indirmeyi içe aktardıktan sonra ayarlayacağı kategori. {appName}, tohumlama tamamlansa bile bu kategorideki torrentleri kaldırmaz. Aynı kategoriyi korumak için boş bırakın.", + "DownloadClientSettingsUseSslHelpText": "{clientName} ile bağlantı kurulurken güvenli bağlantıyı kullan", + "DownloadClientSettingsUrlBaseHelpText": "{clientName} URL'sine {url} gibi bir önek ekler", + "DownloadClientValidationApiKeyRequired": "API Anahtarı Gerekli", + "DownloadClientValidationAuthenticationFailure": "Kimlik doğrulama hatası", + "AlreadyInYourLibrary": "Kütüphanenizde mevcut", + "EditMetadata": "{metadataType} Meta Verisini Düzenle", + "DownloadClientPneumaticSettingsStrmFolder": "Strm Klasörü", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Bu klasöre XBMC'den erişilmesi gerekecek", + "DownloadClientPriorityHelpText": "İstemci Önceliğini 1'den (En Yüksek) 50'ye (En Düşük) indirin. Varsayılan: 1. Aynı önceliğe sahip istemciler için Round-Robin kullanılır.", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName}, etiketi qBittorrent'e ekleyemedi.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Sıraya Alma Etkin Değil", + "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.", + "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd, sürüm 3.0.0 veya üzerini varsayarak sürüm geliştirir.", + "DownloadClientValidationUnableToConnect": "{clientName} ile bağlantı kurulamıyor", + "DownloadClientValidationUnknownException": "Bilinmeyen istisna: {exception}", + "DownloadClientValidationVerifySsl": "SSL Doğrulama ayarı", + "DownloadClientVuzeValidationErrorVersion": "Protokol sürümü desteklenmiyor; Vuze Web Remote eklentisi ile Vuze 5.0.0.0 veya üstünü kullanın.", + "EditAutoTag": "Otomatik Etiket Düzenle", + "EditConditionImplementation": "Koşulu Düzenle - {implementationName}", + "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGet ayarı KeepHistory 0'dan büyük olmalıdır", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGet ayarı KeepHistory 25000'den az olmalıdır", + "DownloadClientQbittorrentSettingsSequentialOrder": "Sıralı Sıra", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Sıralı olarak indirin (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Kategoriler qBittorrent 3.3.0 sürümüne kadar desteklenmemektedir. Lütfen yükseltme yapın veya boş bir Kategoriyle tekrar deneyin.", + "DownloadClientRTorrentSettingsUrlPath": "URL Yolu", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrent Kuyruğa Alma, qBittorrent ayarlarınızda etkin değil. qBittorrent'te etkinleştirin veya öncelik olarak 'Son'u seçin.", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Sabnbzd'de 'İndirmeden önce kontrol et' seçeneğini devre dışı bırakın", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Film Sıralamayı Devre Dışı Bırak", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "TV Sıralamasını Devre Dışı Bırak", + "DownloadClientValidationVerifySslDetail": "Lütfen hem {clientName} hem de {appName} üzerinde SSL yapılandırmanızı doğrulayın", + "DownloadClientValidationUnableToConnectDetail": "Lütfen ana bilgisayar adını ve bağlantı noktasını doğrulayın.", + "EditImportListImplementation": "İçe Aktarma Listesini Düzenle - {implementationName}", + "DownloadClientQbittorrentSettingsContentLayout": "İçerik Düzeni", + "DownloadClientSettingsRecentPriority": "Yeni Öncelik", + "DownloadClientUTorrentTorrentStateError": "uTorrent bir hata bildirdi", + "DownloadClientValidationApiKeyIncorrect": "API Anahtarı Yanlış", + "DownloadClientValidationGroupMissingDetail": "Girdiğiniz grup {clientName} içinde mevcut değil. Önce bunu {clientName} içinde oluşturun.", + "Duration": "Süre", + "DownloadIgnored": "Yoksayılanları İndir", + "DownloadClientsLoadError": "İndirme istemcileri yüklenemiyor", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Torrentlerin başlangıç durumu qBittorrent'e eklendi. Zorunlu Torrentlerin seed kısıtlamalarına uymadığını unutmayın", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Etkinleştirme, durdurulmuş durumdaki rTorrent'e torrentler ve magnet ekleyecektir. Bu magnet dosyalarını bozabilir.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "'İndirmeden önce kontrol et' seçeneğinin kullanılması, {appName} uygulamasının yeni indirilenleri takip etme yeteneğini etkiler. Ayrıca Sabnzbd, daha etkili olduğu için bunun yerine 'Tamamlanamayan işleri iptal et' seçeneğini öneriyor.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName}, geliştirme sürümlerini çalıştırırken SABnzbd'ye eklenen yeni özellikleri desteklemeyebilir.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Tarih sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.", + "DownloadClientValidationGroupMissing": "Grup mevcut değil", + "DownloadClientValidationTestTorrents": "Torrentlerin listesi alınamadı: {exceptionMessage}", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz.", + "DownloadClientSettingsCategoryHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir.", + "DownloadClientSettingsOlderPriority": "Eski Önceliği", + "DownloadClientValidationTestNzbs": "NZB'lerin listesi alınamadı: {exceptionMessage}", + "DownloadClientValidationSslConnectFailure": "SSL aracılığıyla bağlanılamıyor", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "{downloadClientName} indirme istemcisi, tamamlanan indirmeleri kaldıracak şekilde ayarlandı. Bu, indirilenlerin {appName} içe aktarılmadan önce istemcinizden kaldırılmasına neden olabilir.", + "DownloadClientQbittorrentTorrentStatePathError": "İçe Aktarılamıyor. Yol, istemci tabanlı indirme dizini ile eşleşiyor, bu torrent için 'Üst düzey klasörü tut' seçeneği devre dışı bırakılmış olabilir veya 'Torrent İçerik Düzeni' 'Orijinal' veya 'Alt Klasör Oluştur' olarak ayarlanmamış olabilir mi?", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent, Torrentleri Paylaşım Oranı Sınırına ulaştıklarında kaldıracak şekilde yapılandırılmıştır", + "DownloadClientRTorrentProviderMessage": "rTorrent, başlangıç kriterlerini karşılayan torrentleri duraklatmaz. {appName}, torrentlerin otomatik olarak kaldırılmasını Ayarlar->Dizinleyiciler'deki geçerli tohum kriterlerine göre yalnızca Tamamlandı Kaldırma etkinleştirildiğinde gerçekleştirecektir. İçe aktardıktan sonra, davranışı özelleştirmek için rTorrent komut dosyalarında kullanılabilen {importedView}'ı bir rTorrent görünümü olarak ayarlayacaktır.", + "DownloadClientValidationAuthenticationFailureDetail": "Kullanıcı adınızı ve şifrenizi kontrol edin. Ayrıca, {appName} çalıştıran ana bilgisayarın, {clientName} yapılandırmasındaki WhiteList sınırlamaları nedeniyle {clientName} erişiminin engellenip engellenmediğini de doğrulayın.", + "DownloadStationStatusExtracting": "Çıkarılıyor: %{progress}", + "DownloadClientNzbVortexMultipleFilesMessage": "İndirme birden fazla dosya içeriyor ve bir iş klasöründe değil: {outputPath}", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} her indirme işleminin ayrı bir klasöre sahip olmasını tercih ediyor. Klasör/Yol'a * eklendiğinde Sabnzbd bu iş klasörlerini oluşturmayacaktır. Düzeltmek için Sabnzbd'a gidin.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan rTorrent konumunu kullanmak için boş bırakın", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için TV sıralamasını devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.", + "DownloadClientSettingsCategorySubFolderHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir. Çıkış dizininde bir [kategori] alt dizini oluşturur.", + "DownloadClientSettingsInitialStateHelpText": "{clientName} dosyasına eklenen torrentler için başlangıç durumu", + "DownloadClientTransmissionSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan İletim konumunu kullanmak için boş bırakın", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "{clientName} rpc URL'sine bir önek ekler, örneğin {url}, varsayılan olarak '{defaultUrl}' olur", + "DownloadClientValidationErrorVersion": "{clientName} sürümü en az {requiredVersion} olmalıdır. Bildirilen sürüm: {reportedVersion}", + "DownloadClientValidationSslConnectFailureDetail": "{appName}, SSL kullanarak {clientName} uygulamasına bağlanamıyor. Bu sorun bilgisayarla ilgili olabilir. Lütfen hem {appName} hem de {clientName} uygulamasını SSL kullanmayacak şekilde yapılandırmayı deneyin.", + "Imported": "İçe aktarıldı", + "NotificationsAppriseSettingsTagsHelpText": "İsteğe bağlı olarak yalnızca uygun şekilde etiketlenenleri bilgilendirin.", + "NotificationsDiscordSettingsAvatar": "Avatar", + "NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Tokenı", + "ImportScriptPath": "Komut Dosyası Yolunu İçe Aktar", + "History": "Geçmiş", + "EditSelectedImportLists": "Seçilen İçe Aktarma Listelerini Düzenle", + "FormatShortTimeSpanSeconds": "{seconds} saniye", + "LabelIsRequired": "Etiket gerekli", + "NoHistoryFound": "Geçmiş bulunamadı", + "NotificationsCustomScriptSettingsArguments": "Argümanlar", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "'Manuel Etkileşimlerde' bildirimi için iletilen alanları değiştirin", + "FormatShortTimeSpanHours": "{hours} saat", + "FormatRuntimeMinutes": "{minutes}dk", + "FullColorEventsHelpText": "Etkinliğin tamamını yalnızca sol kenar yerine durum rengiyle renklendirecek şekilde stil değiştirildi. Gündem için geçerli değildir", + "GrabId": "ID Yakala", + "ImportUsingScriptHelpText": "Bir komut dosyası kullanarak içe aktarmak için dosyaları kopyalayın (ör. kod dönüştürme için)", + "InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için", + "ManageDownloadClients": "İndirme İstemcilerini Yönet", + "ManageImportLists": "İçe Aktarma Listelerini Yönet", + "NotificationTriggersHelpText": "Bu bildirimi hangi olayların tetikleyeceğini seçin", + "NotificationsAppriseSettingsTags": "Apprise Etiketler", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Komut dosyasına aktarılacak argümanlar", + "NotificationsCustomScriptValidationFileDoesNotExist": "Dosya bulunmuyor", + "NotificationsDiscordSettingsOnGrabFields": "Yakalamalarda", + "NotificationsDiscordSettingsAvatarHelpText": "Bu entegrasyondaki mesajlar için kullanılan avatarı değiştirin", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "'İçe aktarmalarda' bildirimi için iletilen alanları değiştirin", + "NotificationsDiscordSettingsOnImportFields": "İçe Aktarmalarda", + "NotificationsEmailSettingsBccAddress": "BCC Adres(ler)i", + "NotificationsEmailSettingsName": "E-posta", + "NotificationsEmailSettingsFromAddress": "Adresten", + "NotificationsEmailSettingsServerHelpText": "E-posta sunucusunun ana bilgisayar adı veya IP'si", + "NotificationsGotifySettingsServer": "Gotify Sunucusu", + "NotificationsGotifySettingsPriorityHelpText": "Bildirimin önceliği", + "NotificationsJoinSettingsDeviceNames": "Cihaz Adları", + "NotificationsKodiSettingsDisplayTime": "Gösterim Süresi", + "NotificationsKodiSettingsDisplayTimeHelpText": "Bildirimin ne kadar süreyle görüntüleneceği (Saniye cinsinden)", + "NotificationsMailgunSettingsUseEuEndpoint": "AB Uç Noktasını Kullan", + "NotificationsMailgunSettingsSenderDomain": "Gönderen Alanı", + "NotificationsNtfySettingsAccessToken": "Erişim Token'ı", + "NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı belirteç tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir", + "NotificationsNtfySettingsPasswordHelpText": "İsteğe bağlı şifre", + "NotificationsNtfySettingsTagsEmojisHelpText": "Kullanılacak etiketlerin veya emojilerin isteğe bağlı listesi", + "NotificationsNtfySettingsTopics": "Konular", + "NotificationsNtfySettingsUsernameHelpText": "İsteğe bağlı kullanıcı adı", + "NotificationsCustomScriptSettingsName": "Özel Komut Dosyası", + "NotificationsKodiSettingAlwaysUpdate": "Daima Güncelle", + "NotificationsKodiSettingsCleanLibrary": "Kütüphaneyi Temizle", + "NotificationsKodiSettingsCleanLibraryHelpText": "Güncellemeden sonra kitaplığı temizle", + "Unmonitored": "İzlenmeyen", + "FormatAgeHour": "saat", + "FormatAgeHours": "saat", + "NoHistory": "Geçmiş yok", + "FailedToFetchUpdates": "Güncellemeler getirilemedi", + "InstanceName": "Örnek isim", + "MoveAutomatically": "Otomatik Olarak Taşı", + "MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)", + "NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor", + "EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle", + "EnableProfileHelpText": "Sürüm profilini etkinleştirmek için işaretleyin", + "EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli aralıklarla sürüm değişikliği aradığında kullanacak", + "FormatTimeSpanDays": "{days}g {time}", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "NotificationsNtfySettingsTagsEmojis": "Ntfy Etiketler ve Emojiler", + "NotificationsJoinSettingsDeviceNamesHelpText": "Bildirim göndermek istediğiniz tam veya kısmi cihaz adlarının virgülle ayrılmış listesi. Ayarlanmadığı takdirde tüm cihazlar bildirim alacaktır.", + "NotificationsKodiSettingAlwaysUpdateHelpText": "Bir video oynatılırken bile kitaplık güncellensin mi?", + "EnableProfile": "Profili Etkinleştir", + "Example": "Örnek", + "FormatAgeDay": "gün", + "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatRuntimeHours": "{hours}s", + "LanguagesLoadError": "Diller yüklenemiyor", + "ListWillRefreshEveryInterval": "Liste her {refreshInterval} yenilenecektir", + "ManageIndexers": "Dizin Oluşturucuları Yönet", + "ManualGrab": "Manuel Yakalama", + "DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri", + "DownloadClients": "İndirme İstemcileri", + "InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı", + "ListQualityProfileHelpText": "Kalite Profili listesi öğeleri şu şekilde eklenecektir:", + "MustNotContainHelpText": "Bir veya daha fazla terimi içeriyorsa yayın reddedilecektir (büyük/küçük harfe duyarlı değildir)", + "NoDownloadClientsFound": "İndirme istemcisi bulunamadı", + "NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}", + "NotificationsAppriseSettingsStatelessUrls": "Apprise Durum bilgisi olmayan URL'ler", + "NotificationsCustomScriptSettingsProviderMessage": "Test, betiği EventType {eventTypeTest} olarak ayarlıyken yürütür; betiğinizin bunu doğru şekilde işlediğinden emin olun", + "NotificationsDiscordSettingsAuthorHelpText": "Bu bildirim için gösterilen yerleştirme yazarını geçersiz kılın. Boş örnek adıdır", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Bu 'yakalandı' bildirimi için iletilen alanları değiştirin", + "NotificationsDiscordSettingsUsernameHelpText": "Gönderimin yapılacağı kullanıcı adı varsayılan olarak Discord webhook varsayılanıdır", + "NotificationsKodiSettingsGuiNotification": "GUI Bildirimi", + "NotificationsJoinValidationInvalidDeviceId": "Cihaz kimlikleri geçersiz görünüyor.", + "NotificationsNtfySettingsClickUrl": "URL'ye tıklayın", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Profilinizdeki API anahtarınız", + "EditReleaseProfile": "Sürüm Profilini Düzenle", + "EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle", + "FormatShortTimeSpanMinutes": "{minutes} dakika", + "FullColorEvents": "Tam Renkli Etkinlikler", + "ListRootFolderHelpText": "Kök Klasör listesi öğeleri eklenecek", + "HourShorthand": "s", + "LogFilesLocation": "Günlük dosyaları şu konumda bulunur: {location}", + "ImportUsingScript": "Komut Dosyası Kullanarak İçe Aktar", + "IncludeHealthWarnings": "Sağlık Uyarılarını Dahil Et", + "IndexerSettingsMultiLanguageRelease": "Çok dil", + "IndexerSettingsMultiLanguageReleaseHelpText": "Bu indeksleyicideki çoklu sürümde normalde hangi diller bulunur?", + "InteractiveImportNoImportMode": "Bir içe aktarma modu seçilmelidir", + "InteractiveImportNoQuality": "Seçilen her dosya için kalite seçilmelidir", + "NotificationsEmailSettingsServer": "Sunucu", + "InvalidUILanguage": "Kullanıcı arayüzünüz geçersiz bir dile ayarlanmış, düzeltin ve ayarlarınızı kaydedin", + "NotificationsAppriseSettingsServerUrl": "Apprise Sunucu URL'si", + "ManageClients": "İstemcileri Yönet", + "ManageLists": "Listeleri Yönet", + "MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.", + "Never": "Asla", + "NoHistoryBlocklist": "Geçmiş engellenenler listesi yok", + "NoIndexersFound": "Dizin oluşturucu bulunamadı", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.", + "NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası", + "NotificationsAppriseSettingsUsernameHelpText": "HTTP Temel Kimlik Doğrulama Kullanıcı Adı", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Bildirimin nereye gönderilmesi gerektiğini belirten, virgülle ayrılmış bir veya daha fazla URL. Kalıcı Depolama kullanılıyorsa boş bırakın.", + "NotificationsDiscordSettingsOnManualInteractionFields": "Manuel Etkileşimlerde", + "NotificationsEmbySettingsSendNotifications": "Bildirim Gönder", + "NotificationsEmbySettingsSendNotificationsHelpText": "MediaBrowser'ın yapılandırılmış sağlayıcılara bildirim göndermesini sağlayın", + "NotificationsJoinSettingsDeviceIdsHelpText": "Kullanımdan kaldırıldı, bunun yerine Cihaz Adlarını kullanın. Bildirim göndermek istediğiniz Cihaz Kimliklerinin virgülle ayrılmış listesi. Ayarlanmadığı takdirde tüm cihazlar bildirim alacaktır.", + "NotificationsMailgunSettingsApiKeyHelpText": "MailGun'dan oluşturulan API anahtarı", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "AB MailGun uç noktasını kullanmayı etkinleştirin", + "NotificationsNtfySettingsClickUrlHelpText": "Kullanıcı bildirime tıkladığında isteğe bağlı bağlantı", + "NotificationsNtfySettingsServerUrl": "Sunucu URL'si", + "NotificationsNtfySettingsTopicsHelpText": "Bildirim gönderilecek konuların listesi", + "Save": "Kaydet", + "Connect": "Bildirimler", + "InfoUrl": "Bilgi URL'si", + "InteractiveSearchModalHeader": "İnteraktif Arama", + "No": "Hayır", + "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord kanalı webhook URL'si", + "NotificationsEmailSettingsBccAddressHelpText": "E-posta bcc alıcılarının virgülle ayrılmış listesi", + "FailedToUpdateSettings": "Ayarlar güncellenemedi", + "False": "Pasif", + "HistoryLoadError": "Geçmiş yüklenemiyor", + "NotificationsEmailSettingsRecipientAddressHelpText": "E-posta alıcılarının virgülle ayrılmış listesi", + "FormatAgeDays": "gün", + "IgnoreDownload": "İndirmeyi Yoksay", + "IgnoreDownloadHint": "{appName}'in bu indirmeyi daha fazla işlemesini durdurur", + "IgnoreDownloads": "İndirilenleri Yoksay", + "IgnoreDownloadsHint": "{appName}'ın bu indirmeleri daha fazla işlemesi durdurulur", + "Implementation": "Uygula", + "Import": "İçe aktar", + "NotificationsEmailSettingsCcAddressHelpText": "E-posta CC alıcılarının virgülle ayrılmış listesi", + "NotificationsEmailSettingsCcAddress": "CC Adres(ler)i", + "NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i", + "NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplık Güncellensin mi?", + "NotificationsGotifySettingsAppToken": "Uygulama Token'ı", + "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", + "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", + "Test": "Sına", + "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent karma tarafından engellendiyse, RSS/Bazı dizin oluşturucuları arama sırasında düzgün şekilde reddedilmeyebilir; bunun etkinleştirilmesi, torrent yakalandıktan sonra ancak istemciye gönderilmeden önce reddedilmesine olanak tanır.", + "NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı", + "NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü", + "NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si", + "NotificationsJoinSettingsApiKeyHelpText": "Katıl hesap ayarlarınızdaki API Anahtarı (API'ye Katıl düğmesine tıklayın).", + "ImportScriptPathHelpText": "İçe aktarma için kullanılacak komut dosyasının yolu", + "Label": "Etiket", + "NoDelay": "Gecikme yok", + "NoImportListsFound": "İçe aktarma listesi bulunamadı", + "FormatAgeMinute": "dakika", + "FormatAgeMinutes": "dakika", + "NotificationsDiscordSettingsAuthor": "Yazar", + "NotificationsEmailSettingsUseEncryption": "Şifreleme Kullan", + "NotificationsAppriseSettingsServerUrlHelpText": "Gerekiyorsa http(s):// ve bağlantı noktasını içeren Apprise sunucu URL'si", + "NotificationsEmailSettingsUseEncryptionHelpText": "Sunucuda yapılandırılmışsa şifrelemeyi kullanmayı mı tercih edeceğiniz, şifrelemeyi her zaman SSL (yalnızca Bağlantı Noktası 465) veya StartTLS (başka herhangi bir bağlantı noktası) aracılığıyla mı kullanacağınız veya hiçbir zaman şifrelemeyi kullanmama tercihlerini belirler", + "NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?", + "NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın", + "InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", + "IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index d04a2d4b3..385dc081b 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1808,5 +1808,24 @@ "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", - "AutoTaggingSpecificationOriginalLanguage": "语言" + "AutoTaggingSpecificationOriginalLanguage": "语言", + "CustomFormatsSpecificationFlag": "标记", + "CustomFormatsSpecificationLanguage": "语言", + "AddDelayProfileError": "无法添加新的延迟配置,请重试。", + "CustomFilter": "自定义过滤器", + "CustomFormatsSpecificationMinimumSizeHelpText": "必须大于该尺寸才会发布", + "AddAutoTagError": "无法添加新的自动标签,请重试。", + "AutoTaggingSpecificationTag": "标签", + "BlocklistReleaseHelpText": "禁止 {appName}通过 RSS 或自动搜索重新下载此版本", + "CleanLibraryLevel": "清除库等级", + "AutoTaggingSpecificationMinimumYear": "最小年份", + "AutoTaggingSpecificationRootFolder": "根文件夹", + "AutoTaggingSpecificationMaximumYear": "最大年份", + "AutoTaggingSpecificationQualityProfile": "质量概况", + "CustomFormatsSpecificationMaximumSize": "最大尺寸", + "CustomFormatsSpecificationMinimumSize": "最小尺寸", + "CustomFormatsSpecificationMaximumSizeHelpText": "必须小于或等于该尺寸时才会发布", + "AutoTaggingSpecificationGenre": "类型", + "AutoTaggingSpecificationSeriesType": "系列类型", + "AutoTaggingSpecificationStatus": "状态" } From dc3e9321022908fe6a8a2d39cd1df4a89a057a8a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 27 Apr 2024 14:54:07 -0700 Subject: [PATCH 256/762] macOS tests now run on arm64 --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cddbc438..bd0f3ffe4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,11 +76,11 @@ jobs: framework: ${{ env.FRAMEWORK }} runtime: linux-x64 - - name: Publish osx-x64 Test Artifact + - name: Publish osx-arm64 Test Artifact uses: ./.github/actions/publish-test-artifact with: framework: ${{ env.FRAMEWORK }} - runtime: osx-x64 + runtime: osx-arm64 # Build Artifacts (grouped by OS) @@ -143,7 +143,7 @@ jobs: artifact: tests-linux-x64 filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest - os: macos-latest - artifact: tests-osx-x64 + artifact: tests-osx-arm64 filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest - os: windows-latest artifact: tests-win-x64 @@ -190,10 +190,10 @@ jobs: binary_artifact: build_linux binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr - os: macos-latest - artifact: tests-osx-x64 + artifact: tests-osx-arm64 filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest binary_artifact: build_macos - binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr + binary_path: osx-arm64/${{ needs.backend.outputs.framework }}/Sonarr - os: windows-latest artifact: tests-win-x64 filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest From d738035fed859eb475051f3df494b9c975a42e82 Mon Sep 17 00:00:00 2001 From: Christopher <cm4181@gmail.com> Date: Sat, 27 Apr 2024 21:04:16 -0400 Subject: [PATCH 257/762] New: Remove qBitorrent torrents that reach inactive seeding time --- .../QBittorrentTests/QBittorrentFixture.cs | 69 +++++++++++++++++-- .../Clients/QBittorrent/QBittorrent.cs | 22 +++++- .../QBittorrent/QBittorrentPreferences.cs | 6 ++ .../Clients/QBittorrent/QBittorrentTorrent.cs | 6 ++ 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 3ae0edd84..d8029eeac 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First; } - protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause) + protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, int maxInactiveSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause) { Mocker.GetMock<IQBittorrentProxy>() .Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>())) @@ -118,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests MaxRatio = maxRatio, MaxRatioEnabled = maxRatio >= 0, MaxSeedingTime = maxSeedingTime, - MaxSeedingTimeEnabled = maxSeedingTime >= 0 + MaxSeedingTimeEnabled = maxSeedingTime >= 0, + MaxInactiveSeedingTime = maxInactiveSeedingTime, + MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTime >= 0 }); } @@ -610,7 +612,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests float ratio = 0.1f, float ratioLimit = -2, int seedingTime = 1, - int seedingTimeLimit = -2) + int seedingTimeLimit = -2, + int inactiveSeedingTimeLimit = -2, + long lastActivity = -1) { var torrent = new QBittorrentTorrent { @@ -624,7 +628,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests SavePath = "", Ratio = ratio, RatioLimit = ratioLimit, - SeedingTimeLimit = seedingTimeLimit + SeedingTimeLimit = seedingTimeLimit, + InactiveSeedingTimeLimit = inactiveSeedingTimeLimit, + LastActivity = lastActivity == -1 ? DateTimeOffset.UtcNow.ToUnixTimeSeconds() : lastActivity }; GivenTorrents(new List<QBittorrentTorrent>() { torrent }); @@ -739,6 +745,50 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeFalse(); } + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_inactive_seedingtime_reached_and_not_paused() + { + GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); + GivenCompletedTorrent("uploading", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused() + { + GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused() + { + GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds()); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused() + { + GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds()); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + [Test] public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused() { @@ -750,6 +800,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeTrue(); } + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused() + { + GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20); + GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + [Test] public void should_not_fetch_details_twice() { diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index b072b193d..ec5bbc437 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -630,7 +630,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - if (HasReachedSeedingTimeLimit(torrent, config)) + if (HasReachedSeedingTimeLimit(torrent, config) || HasReachedInactiveSeedingTimeLimit(torrent, config)) { return true; } @@ -702,6 +702,26 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return false; } + protected bool HasReachedInactiveSeedingTimeLimit(QBittorrentTorrent torrent, QBittorrentPreferences config) + { + long inactiveSeedingTimeLimit; + + if (torrent.InactiveSeedingTimeLimit >= 0) + { + inactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit * 60; + } + else if (torrent.InactiveSeedingTimeLimit == -2 && config.MaxInactiveSeedingTimeEnabled) + { + inactiveSeedingTimeLimit = config.MaxInactiveSeedingTime * 60; + } + else + { + return false; + } + + return DateTimeOffset.UtcNow.ToUnixTimeSeconds() - torrent.LastActivity > inactiveSeedingTimeLimit; + } + protected void FetchTorrentDetails(QBittorrentTorrent torrent) { var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs index e2979bd3a..d33b7bfe8 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs @@ -28,6 +28,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [JsonProperty(PropertyName = "max_seeding_time")] public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes + [JsonProperty(PropertyName = "max_inactive_seeding_time_enabled")] + public bool MaxInactiveSeedingTimeEnabled { get; set; } // True if share inactive time limit is enabled + + [JsonProperty(PropertyName = "max_inactive_seeding_time")] + public long MaxInactiveSeedingTime { get; set; } // Get the global share inactive time limit in minutes + [JsonProperty(PropertyName = "max_ratio_act")] public QBittorrentMaxRatioAction MaxRatioAction { get; set; } // Action performed when a torrent reaches the maximum share ratio. diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs index 92e6c7e02..d282e993a 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs @@ -37,6 +37,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited) public long SeedingTimeLimit { get; set; } = -2; + + [JsonProperty(PropertyName = "inactive_seeding_time_limit")] // Per torrent inactive seeding time limit (-2 = use global, -1 = unlimited) + public long InactiveSeedingTimeLimit { get; set; } = -2; + + [JsonProperty(PropertyName = "last_activity")] // Timestamp in unix seconds when a chunk was last downloaded/uploaded + public long LastActivity { get; set; } } public class QBittorrentTorrentProperties From a97fbcc40a6247bf59678425cf460588fd4dbecd Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 18 Apr 2024 21:40:22 -0700 Subject: [PATCH 258/762] Fixed: Improve paths longer than 256 on Windows failing to hardlink --- src/NzbDrone.Windows/Disk/DiskProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NzbDrone.Windows/Disk/DiskProvider.cs b/src/NzbDrone.Windows/Disk/DiskProvider.cs index 97b73c607..a7a204a5e 100644 --- a/src/NzbDrone.Windows/Disk/DiskProvider.cs +++ b/src/NzbDrone.Windows/Disk/DiskProvider.cs @@ -170,6 +170,11 @@ namespace NzbDrone.Windows.Disk { try { + if (source.Length > 256 && !source.StartsWith(@"\\?\")) + { + source = @"\\?\" + source; + } + return CreateHardLink(destination, source, IntPtr.Zero); } catch (Exception ex) From 2440672179f130db2e93071882fc4d91375ad8d6 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 19 Apr 2024 07:56:43 +0300 Subject: [PATCH 259/762] Bump SixLabors.ImageSharp to 3.1.4 ignore-downstream --- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 269005cfb..bcc1f7331 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -18,7 +18,7 @@ <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.1.3" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> From 316b5cbf75b45ef9a25f96ce1f2fbed25ad94296 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 27 Apr 2024 18:04:50 -0700 Subject: [PATCH 260/762] New: Validate that folders in paths don't start or end with a space Closes #6709 --- .../PathExtensionFixture.cs | 28 +++++++++++++++- .../Extensions/PathExtensions.cs | 32 +++++++++++++++++-- src/Sonarr.Api.V3/Series/SeriesController.cs | 2 -- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 00012a135..967126a1c 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -3,6 +3,7 @@ using System.IO; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Test.Common; @@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test [TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")] [TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")] [TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")] - [TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")] + [TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")] [TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")] public void Clean_Path_Windows(string dirty, string clean) { @@ -335,5 +336,30 @@ namespace NzbDrone.Common.Test result[2].Should().Be(@"TV"); result[3].Should().Be(@"Series Title"); } + + [TestCase(@"C:\Test\")] + [TestCase(@"C:\Test")] + [TestCase(@"C:\Test\TV\")] + [TestCase(@"C:\Test\TV")] + public void IsPathValid_should_be_true(string path) + { + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue(); + } + + [TestCase(@"C:\Test \")] + [TestCase(@"C:\Test ")] + [TestCase(@"C:\ Test\")] + [TestCase(@"C:\ Test")] + [TestCase(@"C:\Test \TV")] + [TestCase(@"C:\ Test\TV")] + [TestCase(@"C:\Test \TV\")] + [TestCase(@"C:\ Test\TV\")] + [TestCase(@" C:\Test\TV\")] + [TestCase(@" C:\Test\TV")] + + public void IsPathValid_should_be_false(string path) + { + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 280083e4c..4585326f1 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -29,6 +29,12 @@ namespace NzbDrone.Common.Extensions public static string CleanFilePath(this string path) { + if (path.IsNotNullOrWhiteSpace()) + { + // Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix. + path = path.TrimEnd(' '); + } + Ensure.That(path, () => path).IsNotNullOrWhiteSpace(); Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs); @@ -37,10 +43,10 @@ namespace NzbDrone.Common.Extensions // UNC if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\")) { - return info.FullName.TrimEnd('/', '\\', ' '); + return info.FullName.TrimEnd('/', '\\'); } - return info.FullName.TrimEnd('/').Trim('\\', ' '); + return info.FullName.TrimEnd('/').Trim('\\'); } public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null) @@ -154,6 +160,23 @@ namespace NzbDrone.Common.Extensions return false; } + if (path.Trim() != path) + { + return false; + } + + var directoryInfo = new DirectoryInfo(path); + + while (directoryInfo != null) + { + if (directoryInfo.Name.Trim() != directoryInfo.Name) + { + return false; + } + + directoryInfo = directoryInfo.Parent; + } + if (validationType == PathValidationType.AnyOs) { return IsPathValidForWindows(path) || IsPathValidForNonWindows(path); @@ -291,6 +314,11 @@ namespace NzbDrone.Common.Extensions return processName; } + public static string CleanPath(this string path) + { + return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray()); + } + public static string GetAppDataPath(this IAppFolderInfo appFolderInfo) { return appFolderInfo.AppDataFolder; diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 14ee8f465..e1d50ec0f 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -92,8 +92,6 @@ namespace Sonarr.Api.V3.Series .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); - - PutValidator.RuleFor(s => s.Path).IsValidPath(); } [HttpGet] From 5d01ecd30e515b615e54efca98df8e7f54ae5e5f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 21 Apr 2024 10:45:00 +0300 Subject: [PATCH 261/762] Bump frontend dependencies ignore-downstream --- package.json | 39 +- yarn.lock | 3319 +++++++++++++++++++++++++++----------------------- 2 files changed, 1823 insertions(+), 1535 deletions(-) diff --git a/package.json b/package.json index 05bac8b34..c86ab70c5 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", "@sentry/browser": "7.100.0", - "@types/node": "18.16.8", - "@types/react": "18.2.6", - "@types/react-dom": "18.2.4", + "@types/node": "18.19.31", + "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "classnames": "2.3.2", "clipboard": "2.0.11", "connected-react-router": "6.9.3", @@ -81,17 +81,16 @@ "redux-thunk": "2.4.2", "reselect": "4.1.8", "stacktrace-js": "2.0.2", - "typescript": "4.9.5" + "typescript": "5.1.6" }, "devDependencies": { - "@babel/core": "7.22.11", - "@babel/eslint-parser": "7.22.11", - "@babel/plugin-proposal-export-default-from": "7.22.5", + "@babel/core": "7.24.4", + "@babel/eslint-parser": "7.24.1", + "@babel/plugin-proposal-export-default-from": "7.24.1", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.22.14", - "@babel/preset-react": "7.22.5", - "@babel/preset-typescript": "7.22.11", - "@types/classnames": "2.3.1", + "@babel/preset-env": "7.24.4", + "@babel/preset-react": "7.24.1", + "@babel/preset-typescript": "7.24.1", "@types/lodash": "4.14.194", "@types/react-lazyload": "3.2.0", "@types/react-router-dom": "5.3.3", @@ -99,30 +98,30 @@ "@types/react-window": "1.8.5", "@types/redux-actions": "2.6.2", "@types/webpack-livereload-plugin": "^2.3.3", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@typescript-eslint/eslint-plugin": "6.21.0", + "@typescript-eslint/parser": "6.21.0", "autoprefixer": "10.4.14", "babel-loader": "9.1.2", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.32.1", + "core-js": "3.37.0", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", - "eslint": "8.40.0", - "eslint-config-prettier": "8.8.0", + "eslint": "8.57.0", + "eslint-config-prettier": "8.10.0", "eslint-plugin-filenames": "1.3.2", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.29.1", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.32.2", + "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.0", - "eslint-plugin-simple-import-sort": "10.0.0", + "eslint-plugin-simple-import-sort": "12.1.0", "file-loader": "6.2.0", "filemanager-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0", "html-webpack-plugin": "5.5.1", "loader-utils": "^3.2.1", "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.23", + "postcss": "8.4.38", "postcss-color-function": "4.1.0", "postcss-loader": "7.3.0", "postcss-mixins": "9.0.4", diff --git a/yarn.lock b/yarn.lock index 9c7bf1ffa..5cf57bfc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,69 +8,69 @@ integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== "@adobe/css-tools@^4.0.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" - integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + version "4.3.3" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" + integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== "@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" - integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" + integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== -"@babel/core@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.11.tgz#8033acaa2aa24c3f814edaaa057f3ce0ba559c24" - integrity sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ== +"@babel/core@7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" + integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helpers" "^7.22.11" - "@babel/parser" "^7.22.11" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.11" - "@babel/types" "^7.22.11" - convert-source-map "^1.7.0" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.24.4" + "@babel/parser" "^7.24.4" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.22.11.tgz#cceb8c7989c241a16dd14e12a6cd725618f3f58b" - integrity sha512-YjOYZ3j7TjV8OhLW6NCtyg8G04uStATEUe5eiLuCZaXz2VSDQ3dsAtm2D+TuQyAqNMUK2WacGo0/uma9Pein1w== +"@babel/eslint-parser@7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz#e27eee93ed1d271637165ef3a86e2b9332395c32" + integrity sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== +"@babel/generator@^7.24.1", "@babel/generator@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498" + integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw== dependencies: - "@babel/types" "^7.22.10" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" + "@babel/types" "^7.24.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" "@babel/helper-annotate-as-pure@^7.22.5": @@ -80,52 +80,52 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz#573e735937e99ea75ea30788b57eb52fab7468c9" - integrity sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" -"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" - browserslist "^4.21.9" + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz#4078686740459eeb4af3494a273ac09148dfb213" - integrity sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ== +"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3" + integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-member-expression-to-functions" "^7.23.0" "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-replace-supers" "^7.24.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" - integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-define-polyfill-provider@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" + integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -133,18 +133,18 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" - integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== -"@babel/helper-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" @@ -153,30 +153,30 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-member-expression-to-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" - integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== +"@babel/helper-member-expression-to-functions@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.24.1": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" + integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.24.0" -"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" - integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" "@babel/helper-simple-access" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" "@babel/helper-optimise-call-expression@^7.22.5": version "7.22.5" @@ -185,27 +185,27 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a" + integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w== -"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" - integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== +"@babel/helper-remap-async-to-generator@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-wrap-function" "^7.22.9" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" -"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" - integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== +"@babel/helper-replace-supers@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" + integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.23.0" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-simple-access@^7.22.5": @@ -229,76 +229,93 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" - integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== -"@babel/helper-wrap-function@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz#d845e043880ed0b8c18bd194a12005cb16d2f614" - integrity sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ== +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== dependencies: "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.10" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" -"@babel/helpers@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a" - integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg== +"@babel/helpers@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" + integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.11" - "@babel/types" "^7.22.11" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" -"@babel/highlight@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" - integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== dependencies: - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/parser@^7.22.11", "@babel/parser@^7.22.5": - version "7.22.14" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.14.tgz#c7de58e8de106e88efca42ce17f0033209dfd245" - integrity sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ== +"@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" - integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" + integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" - integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" + integrity sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz#da8261f2697f0f41b0855b91d3a20a1fbfd271d3" + integrity sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.24.1" -"@babel/plugin-proposal-export-default-from@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.22.5.tgz#825924eda1fad382c3de4db6fe1711b6fa03362f" - integrity sha512-UCe1X/hplyv6A5g2WnQ90tnHRvYL29dabCWww92lO7VdfMVTVReBTRrhiMrKQejHD9oVkdnRdwYuzUZkBVQisg== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz#1181d9685984c91d657b8ddf14f0487a6bab2988" + integrity sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-export-default-from" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-proposal-export-default-from@7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.1.tgz#d242019488277c9a5a8035e5b70de54402644b89" + integrity sha512-+0hrgGGV3xyYIjOrD/bUZk/iUwOIGuoANfRfVg1cPhYBxF+TIXSEcc42DqzBICmWsnAQ+SfKedY0bj8QD+LuMg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-export-default-from" "^7.24.1" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -333,12 +350,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-default-from@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz#ac3a24b362a04415a017ab96b9b4483d0e2a6e44" - integrity sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ== +"@babel/plugin-syntax-export-default-from@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.1.tgz#a92852e694910ae4295e6e51e87b83507ed5e6e8" + integrity sha512-cNXSxv9eTkGUtd0PsNMK8Yx5xeScxfpWOUAxE+ZPAXXEcAMOC3fk7LRdXq5fvpra2pLx2p1YtkAhpUbB2SwaRA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-export-namespace-from@^7.8.3": version "7.8.3" @@ -347,19 +364,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-import-assertions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" - integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== +"@babel/plugin-syntax-import-assertions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz#db3aad724153a00eaac115a3fb898de544e34971" + integrity sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-syntax-import-attributes@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" - integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== +"@babel/plugin-syntax-import-attributes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz#c66b966c63b714c4eec508fcf5763b1f2d381093" + integrity sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" @@ -375,12 +392,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" - integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== +"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" + integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -438,12 +455,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" - integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== +"@babel/plugin-syntax-typescript@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" + integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -453,212 +470,212 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" - integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== +"@babel/plugin-transform-arrow-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz#2bf263617060c9cc45bcdbf492b8cc805082bf27" + integrity sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-async-generator-functions@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz#dbe3b1ff5a52e2e5edc4b19a60d325a675ed2649" - integrity sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw== +"@babel/plugin-transform-async-generator-functions@^7.24.3": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz#8fa7ae481b100768cc9842c8617808c5352b8b89" + integrity sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-transform-async-to-generator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" - integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== +"@babel/plugin-transform-async-to-generator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz#0e220703b89f2216800ce7b1c53cb0cf521c37f4" + integrity sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw== dependencies: - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/helper-module-imports" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" -"@babel/plugin-transform-block-scoped-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" - integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== +"@babel/plugin-transform-block-scoped-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz#1c94799e20fcd5c4d4589523bbc57b7692979380" + integrity sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-block-scoping@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" - integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== +"@babel/plugin-transform-block-scoping@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012" + integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-class-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" - integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== +"@babel/plugin-transform-class-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz#bcbf1aef6ba6085cfddec9fc8d58871cf011fc29" + integrity sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-class-static-block@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" - integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== +"@babel/plugin-transform-class-static-block@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4" + integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" - integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== +"@babel/plugin-transform-classes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz#5bc8fc160ed96378184bc10042af47f50884dcb1" + integrity sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" - integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== +"@babel/plugin-transform-computed-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz#bc7e787f8e021eccfb677af5f13c29a9934ed8a7" + integrity sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/template" "^7.24.0" -"@babel/plugin-transform-destructuring@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" - integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== +"@babel/plugin-transform-destructuring@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz#b1e8243af4a0206841973786292b8c8dd8447345" + integrity sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-dotall-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" - integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== +"@babel/plugin-transform-dotall-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz#d56913d2f12795cc9930801b84c6f8c47513ac13" + integrity sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-duplicate-keys@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" - integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== +"@babel/plugin-transform-duplicate-keys@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz#5347a797fe82b8d09749d10e9f5b83665adbca88" + integrity sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-dynamic-import@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" - integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== +"@babel/plugin-transform-dynamic-import@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz#2a5a49959201970dd09a5fca856cb651e44439dd" + integrity sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" - integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== +"@babel/plugin-transform-exponentiation-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz#6650ebeb5bd5c012d5f5f90a26613a08162e8ba4" + integrity sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-export-namespace-from@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" - integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== +"@babel/plugin-transform-export-namespace-from@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz#f033541fc036e3efb2dcb58eedafd4f6b8078acd" + integrity sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-for-of@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" - integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== +"@babel/plugin-transform-for-of@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz#67448446b67ab6c091360ce3717e7d3a59e202fd" + integrity sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" - integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== +"@babel/plugin-transform-function-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz#8cba6f7730626cc4dfe4ca2fa516215a0592b361" + integrity sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA== dependencies: - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-json-strings@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" - integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== +"@babel/plugin-transform-json-strings@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz#08e6369b62ab3e8a7b61089151b161180c8299f7" + integrity sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" - integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== +"@babel/plugin-transform-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz#0a1982297af83e6b3c94972686067df588c5c096" + integrity sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-logical-assignment-operators@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" - integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== +"@babel/plugin-transform-logical-assignment-operators@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz#719d8aded1aa94b8fb34e3a785ae8518e24cfa40" + integrity sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" - integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== +"@babel/plugin-transform-member-expression-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz#896d23601c92f437af8b01371ad34beb75df4489" + integrity sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-modules-amd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" - integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== +"@babel/plugin-transform-modules-amd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz#b6d829ed15258536977e9c7cc6437814871ffa39" + integrity sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-modules-commonjs@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz#d7991d3abad199c03b68ee66a64f216c47ffdfae" - integrity sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g== +"@babel/plugin-transform-modules-commonjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz#e71ba1d0d69e049a22bf90b3867e263823d3f1b9" + integrity sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw== dependencies: - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-simple-access" "^7.22.5" -"@babel/plugin-transform-modules-systemjs@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1" - integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA== +"@babel/plugin-transform-modules-systemjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" + integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== dependencies: "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-identifier" "^7.22.20" -"@babel/plugin-transform-modules-umd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" - integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== +"@babel/plugin-transform-modules-umd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz#69220c66653a19cf2c0872b9c762b9a48b8bebef" + integrity sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": version "7.22.5" @@ -668,103 +685,102 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-new-target@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" - integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== +"@babel/plugin-transform-new-target@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz#29c59988fa3d0157de1c871a28cd83096363cc34" + integrity sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" - integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz#0cd494bb97cb07d428bd651632cb9d4140513988" + integrity sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" - integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== +"@babel/plugin-transform-numeric-separator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz#5bc019ce5b3435c1cadf37215e55e433d674d4e8" + integrity sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz#dbbb06ce783cd994a8f430d8cefa553e9b42ca62" - integrity sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw== +"@babel/plugin-transform-object-rest-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" + integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-parameters" "^7.24.1" -"@babel/plugin-transform-object-super@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" - integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== +"@babel/plugin-transform-object-super@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz#e71d6ab13483cca89ed95a474f542bbfc20a0520" + integrity sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" -"@babel/plugin-transform-optional-catch-binding@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" - integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== +"@babel/plugin-transform-optional-catch-binding@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz#92a3d0efe847ba722f1a4508669b23134669e2da" + integrity sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.12", "@babel/plugin-transform-optional-chaining@^7.22.5": - version "7.22.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz#d7ebf6a88cd2f4d307b0e000ab630acd8124b333" - integrity sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw== +"@babel/plugin-transform-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz#26e588acbedce1ab3519ac40cc748e380c5291e6" + integrity sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" - integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== +"@babel/plugin-transform-parameters@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" + integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-private-methods@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" - integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== +"@babel/plugin-transform-private-methods@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz#a0faa1ae87eff077e1e47a5ec81c3aef383dc15a" + integrity sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-private-property-in-object@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" - integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== +"@babel/plugin-transform-private-property-in-object@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz#756443d400274f8fb7896742962cc1b9f25c1f6a" + integrity sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" - integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== +"@babel/plugin-transform-property-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz#d6a9aeab96f03749f4eebeb0b6ea8e90ec958825" + integrity sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-react-display-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" - integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== +"@babel/plugin-transform-react-display-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz#554e3e1a25d181f040cf698b93fd289a03bfdcdb" + integrity sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-transform-react-jsx-development@^7.22.5": version "7.22.5" @@ -773,136 +789,138 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.22.5" -"@babel/plugin-transform-react-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" - integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== +"@babel/plugin-transform-react-jsx@^7.22.5", "@babel/plugin-transform-react-jsx@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" + integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/types" "^7.23.4" -"@babel/plugin-transform-react-pure-annotations@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz#1f58363eef6626d6fa517b95ac66fe94685e32c0" - integrity sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA== +"@babel/plugin-transform-react-pure-annotations@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz#c86bce22a53956331210d268e49a0ff06e392470" + integrity sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-regenerator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" - integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== +"@babel/plugin-transform-regenerator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz#625b7545bae52363bdc1fbbdc7252b5046409c8c" + integrity sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" - integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== +"@babel/plugin-transform-reserved-words@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz#8de729f5ecbaaf5cf83b67de13bad38a21be57c1" + integrity sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-shorthand-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" - integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== +"@babel/plugin-transform-shorthand-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz#ba9a09144cf55d35ec6b93a32253becad8ee5b55" + integrity sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" - integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== +"@babel/plugin-transform-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz#a1acf9152cbf690e4da0ba10790b3ac7d2b2b391" + integrity sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-sticky-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" - integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== +"@babel/plugin-transform-sticky-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz#f03e672912c6e203ed8d6e0271d9c2113dc031b9" + integrity sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-template-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" - integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== +"@babel/plugin-transform-template-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz#15e2166873a30d8617e3e2ccadb86643d327aab7" + integrity sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-typeof-symbol@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" - integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== +"@babel/plugin-transform-typeof-symbol@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz#6831f78647080dec044f7e9f68003d99424f94c7" + integrity sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-typescript@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz#9f27fb5e51585729374bb767ab6a6d9005a23329" - integrity sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA== +"@babel/plugin-transform-typescript@^7.24.1": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz#03e0492537a4b953e491f53f2bc88245574ebd15" + integrity sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-typescript" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-typescript" "^7.24.1" -"@babel/plugin-transform-unicode-escapes@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" - integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== +"@babel/plugin-transform-unicode-escapes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz#fb3fa16676549ac7c7449db9b342614985c2a3a4" + integrity sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-property-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" - integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== +"@babel/plugin-transform-unicode-property-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz#56704fd4d99da81e5e9f0c0c93cabd91dbc4889e" + integrity sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" - integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== +"@babel/plugin-transform-unicode-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz#57c3c191d68f998ac46b708380c1ce4d13536385" + integrity sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-sets-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" - integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== +"@babel/plugin-transform-unicode-sets-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz#c1ea175b02afcffc9cf57a9c4658326625165b7f" + integrity sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/preset-env@7.22.14": - version "7.22.14" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.14.tgz#1cbb468d899f64fa71c53446f13b7ff8c0005cc1" - integrity sha512-daodMIoVo+ol/g+//c/AH+szBkFj4STQUikvBijRGL72Ph+w+AMTSh55DUETe8KJlPlDT1k/mp7NBfOuiWmoig== +"@babel/preset-env@7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b" + integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/compat-data" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.22.5" - "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-assertions" "^7.24.1" + "@babel/plugin-syntax-import-attributes" "^7.24.1" "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -914,59 +932,58 @@ "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.11" - "@babel/plugin-transform-async-to-generator" "^7.22.5" - "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.10" - "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.11" - "@babel/plugin-transform-classes" "^7.22.6" - "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.10" - "@babel/plugin-transform-dotall-regex" "^7.22.5" - "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.11" - "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.11" - "@babel/plugin-transform-for-of" "^7.22.5" - "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.11" - "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" - "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.11" - "@babel/plugin-transform-modules-systemjs" "^7.22.11" - "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-arrow-functions" "^7.24.1" + "@babel/plugin-transform-async-generator-functions" "^7.24.3" + "@babel/plugin-transform-async-to-generator" "^7.24.1" + "@babel/plugin-transform-block-scoped-functions" "^7.24.1" + "@babel/plugin-transform-block-scoping" "^7.24.4" + "@babel/plugin-transform-class-properties" "^7.24.1" + "@babel/plugin-transform-class-static-block" "^7.24.4" + "@babel/plugin-transform-classes" "^7.24.1" + "@babel/plugin-transform-computed-properties" "^7.24.1" + "@babel/plugin-transform-destructuring" "^7.24.1" + "@babel/plugin-transform-dotall-regex" "^7.24.1" + "@babel/plugin-transform-duplicate-keys" "^7.24.1" + "@babel/plugin-transform-dynamic-import" "^7.24.1" + "@babel/plugin-transform-exponentiation-operator" "^7.24.1" + "@babel/plugin-transform-export-namespace-from" "^7.24.1" + "@babel/plugin-transform-for-of" "^7.24.1" + "@babel/plugin-transform-function-name" "^7.24.1" + "@babel/plugin-transform-json-strings" "^7.24.1" + "@babel/plugin-transform-literals" "^7.24.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.1" + "@babel/plugin-transform-member-expression-literals" "^7.24.1" + "@babel/plugin-transform-modules-amd" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-modules-systemjs" "^7.24.1" + "@babel/plugin-transform-modules-umd" "^7.24.1" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" - "@babel/plugin-transform-numeric-separator" "^7.22.11" - "@babel/plugin-transform-object-rest-spread" "^7.22.11" - "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.11" - "@babel/plugin-transform-optional-chaining" "^7.22.12" - "@babel/plugin-transform-parameters" "^7.22.5" - "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.11" - "@babel/plugin-transform-property-literals" "^7.22.5" - "@babel/plugin-transform-regenerator" "^7.22.10" - "@babel/plugin-transform-reserved-words" "^7.22.5" - "@babel/plugin-transform-shorthand-properties" "^7.22.5" - "@babel/plugin-transform-spread" "^7.22.5" - "@babel/plugin-transform-sticky-regex" "^7.22.5" - "@babel/plugin-transform-template-literals" "^7.22.5" - "@babel/plugin-transform-typeof-symbol" "^7.22.5" - "@babel/plugin-transform-unicode-escapes" "^7.22.10" - "@babel/plugin-transform-unicode-property-regex" "^7.22.5" - "@babel/plugin-transform-unicode-regex" "^7.22.5" - "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.24.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.1" + "@babel/plugin-transform-numeric-separator" "^7.24.1" + "@babel/plugin-transform-object-rest-spread" "^7.24.1" + "@babel/plugin-transform-object-super" "^7.24.1" + "@babel/plugin-transform-optional-catch-binding" "^7.24.1" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-private-methods" "^7.24.1" + "@babel/plugin-transform-private-property-in-object" "^7.24.1" + "@babel/plugin-transform-property-literals" "^7.24.1" + "@babel/plugin-transform-regenerator" "^7.24.1" + "@babel/plugin-transform-reserved-words" "^7.24.1" + "@babel/plugin-transform-shorthand-properties" "^7.24.1" + "@babel/plugin-transform-spread" "^7.24.1" + "@babel/plugin-transform-sticky-regex" "^7.24.1" + "@babel/plugin-transform-template-literals" "^7.24.1" + "@babel/plugin-transform-typeof-symbol" "^7.24.1" + "@babel/plugin-transform-unicode-escapes" "^7.24.1" + "@babel/plugin-transform-unicode-property-regex" "^7.24.1" + "@babel/plugin-transform-unicode-regex" "^7.24.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.1" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.11" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" core-js-compat "^3.31.0" semver "^6.3.1" @@ -979,28 +996,28 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6" - integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ== +"@babel/preset-react@7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.1.tgz#2450c2ac5cc498ef6101a6ca5474de251e33aa95" + integrity sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-transform-react-display-name" "^7.22.5" - "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-transform-react-display-name" "^7.24.1" + "@babel/plugin-transform-react-jsx" "^7.23.4" "@babel/plugin-transform-react-jsx-development" "^7.22.5" - "@babel/plugin-transform-react-pure-annotations" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations" "^7.24.1" -"@babel/preset-typescript@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz#f218cd0345524ac888aa3dc32f029de5b064b575" - integrity sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg== +"@babel/preset-typescript@7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz#89bdf13a3149a17b3b2a2c9c62547f06db8845ec" + integrity sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.11" - "@babel/plugin-transform-typescript" "^7.22.11" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-syntax-jsx" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-typescript" "^7.24.1" "@babel/regjsgen@^0.8.0": version "0.8.0" @@ -1008,60 +1025,60 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4" - integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== +"@babel/template@^7.22.15", "@babel/template@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" + integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" -"@babel/traverse@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.11.tgz#71ebb3af7a05ff97280b83f05f8865ac94b2027c" - integrity sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ== +"@babel/traverse@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" + integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/code-frame" "^7.24.1" + "@babel/generator" "^7.24.1" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.11" - "@babel/types" "^7.22.11" - debug "^4.1.0" + "@babel/parser" "^7.24.1" + "@babel/types" "^7.24.0" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5", "@babel/types@^7.4.4": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.11.tgz#0e65a6a1d4d9cbaa892b2213f6159485fe632ea2" - integrity sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg== +"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.24.0", "@babel/types@^7.4.4": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" "@csstools/css-parser-algorithms@^2.1.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" - integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd" + integrity sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA== "@csstools/css-tokenizer@^2.1.1": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" - integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== + version "2.2.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz#a4b8718ed7fcd2dcd555de16b31ca59ad4b96a06" + integrity sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw== "@csstools/media-query-list-parser@^2.0.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz#0017f99945f6c16dd81a7aacf6821770933c3a5c" - integrity sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw== + version "2.1.9" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz#feb4b7268f998956eb3ded69507869e73d005dda" + integrity sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA== "@csstools/selector-specificity@^2.2.0": version "2.2.0" @@ -1073,22 +1090,22 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005" - integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg== +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^2.0.3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1100,10 +1117,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.40.0": - version "8.40.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" - integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@fortawesome/fontawesome-common-types@6.4.0": version "6.4.0" @@ -1143,13 +1160,13 @@ dependencies: prop-types "^15.8.1" -"@humanwhocodes/config-array@^0.11.8": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" - integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -1157,47 +1174,47 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== dependencies: - "@jridgewell/set-array" "^1.0.1" + "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1333,39 +1350,32 @@ "@sentry/types" "7.100.0" "@types/archiver@^5.3.1": - version "5.3.2" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.2.tgz#a9f0bcb0f0b991400e7766d35f6e19d163bdadcc" - integrity sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw== + version "5.3.4" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" + integrity sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw== dependencies: "@types/readdir-glob" "*" -"@types/classnames@2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.3.1.tgz#3c2467aa0f1a93f1f021e3b9bcf938bd5dfdc0dd" - integrity sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A== - dependencies: - classnames "*" - "@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" "@types/eslint@*": - version "8.44.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a" - integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg== + version "8.56.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" + integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/history@^4.7.11": version "4.7.11" @@ -1373,9 +1383,9 @@ integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== "@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" @@ -1385,10 +1395,10 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" - integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" @@ -1401,53 +1411,57 @@ integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== "@types/minimist@^1.2.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@*": - version "20.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" - integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== + dependencies: + undici-types "~5.26.4" -"@types/node@18.16.8": - version "18.16.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.8.tgz#fcd9bd0a793aba2701caff4aeae7c988d4da6ce5" - integrity sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA== +"@types/node@18.19.31": + version "18.19.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.31.tgz#b7d4a00f7cb826b60a543cebdbda5d189aaecdcd" + integrity sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA== + dependencies: + undici-types "~5.26.4" "@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/postcss-modules-local-by-default@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#5c141c9bd3a994ae1ebe23d2ae094b24d19538f5" - integrity sha512-0VLab/pcLTLcfbxi6THSIMVYcw9hEUBGvjwwaGpW77mMgRXfGF+a76t7BxTGyLh1y68tBvrffp8UWnqvm76+yg== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz#8fee7513dd1558d74713d817c183a33a6dc583f9" + integrity sha512-CtYCcD+L+trB3reJPny+bKWKMzPfxEyQpKIwit7kErnOexf5/faaGpkFy4I5AwbV4hp1sk7/aTg0tt0B67VkLQ== dependencies: postcss "^8.0.0" "@types/postcss-modules-scope@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/postcss-modules-scope/-/postcss-modules-scope-3.0.1.tgz#f0ad443c2f31f90feacb83bb357692d581388afd" - integrity sha512-LNkp3c4ML9EQj2dgslp4i80Jxj72YK3HjYzrTn6ftUVylW1zaKFGqrMlNIyqBmPWmIhZ/Y5r0Y4T49Hk1IuDUg== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/postcss-modules-scope/-/postcss-modules-scope-3.0.4.tgz#f82d15ec9023c924b531a49e8087b32646233f41" + integrity sha512-//ygSisVq9kVI0sqx3UPLzWIMCmtSVrzdljtuaAEJtGoGnpjBikZ2sXO5MpH9SnWX9HRfXxHifDAXcQjupWnIQ== dependencies: postcss "^8.0.0" "@types/prop-types@*": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== -"@types/react-dom@18.2.4": - version "18.2.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.4.tgz#13f25bfbf4e404d26f62ac6e406591451acba9e0" - integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw== +"@types/react-dom@18.2.25": + version "18.2.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" + integrity sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA== dependencies: "@types/react" "*" @@ -1459,9 +1473,9 @@ "@types/react" "*" "@types/react-redux@^7.1.16": - version "7.1.26" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.26.tgz#84149f5614e40274bb70fcbe8f7cae6267d548b1" - integrity sha512-UKPo7Cm7rswYU6PH6CmTNCRv5NYF3HrgKuHEYTK8g/3czYLrUux50gQ2pkxc9c7ZpQZi+PNhgmI8oNIRoiVIxg== + version "7.1.33" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" + integrity sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1499,28 +1513,18 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.2.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" - integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== +"@types/react@*", "@types/react@18.2.79": + version "18.2.79" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" + integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== dependencies: "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@18.2.6": - version "18.2.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.6.tgz#5cd53ee0d30ffc193b159d3516c8c8ad2f19d571" - integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" csstype "^3.0.2" "@types/readdir-glob@*": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" - integrity sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ== + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" + integrity sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg== dependencies: "@types/node" "*" @@ -1529,53 +1533,48 @@ resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.2.tgz#5956d9e7b9a644358e2c0610f47b1fa3060edc21" integrity sha512-TvcINy8rWFANcpc3EiEQX9Yv3owM3d3KIrqr2ryUIOhYIYzXA/bhDZeGSSSuai62iVR2qMZUgz9tQ5kr0Kl+Tg== -"@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== - -"@types/semver@^7.3.12": - version "7.5.1" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367" - integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.6.tgz#164e169dd061795b50b83c19e4d3be09f8d3a454" + integrity sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g== "@types/tapable@^1": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" - integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== + version "1.0.12" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.12.tgz#bc2cab12e87978eee89fb21576b670350d6d86ab" + integrity sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q== "@types/uglify-js@*": - version "3.17.2" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.2.tgz#a2ba86fd524f6281a7655463338c546f845b29c3" - integrity sha512-9SjrHO54LINgC/6Ehr81NjAxAYvwEZqjUHLjJYvC4Nmr9jbLQCIZbWSvl4vXQkkmR1UAuaKDycau3O1kWGFyXQ== + version "3.17.5" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.5.tgz#905ce03a3cbbf2e31cbefcbc68d15497ee2e17df" + integrity sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ== dependencies: source-map "^0.6.1" "@types/webpack-livereload-plugin@^2.3.3": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.3.tgz#96f34133f1e1515571233a3e5099d863dc8723e7" - integrity sha512-R8P2HG2mAHY3Qptspt0il8zYVNqiEeOmMe2cGFcEjH7qnJZ4uC7hujLwfAm6jrIO7I5uEs6CxfpKSXM9ULAggw== + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.6.tgz#2c3ccefc8858525f40aeb8be0f784d5027144e23" + integrity sha512-H8nZSOWSiY/6kCpOmbutZPu7Sai1xyEXo/SrXQPCymMPNBwpYWAdOsjKqr32d+IrVjnn9GGgKSYY34TEPRxJ/A== dependencies: "@types/webpack" "^4" "@types/webpack-sources@*": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" - integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg== + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.3.tgz#b667bd13e9fa15a9c26603dce502c7985418c3d8" + integrity sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw== dependencies: "@types/node" "*" "@types/source-list-map" "*" source-map "^0.7.3" "@types/webpack@^4": - version "4.41.33" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc" - integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g== + version "4.41.38" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.38.tgz#5a40ac81bdd052bf405e8bdcf3e1236f6db6dc26" + integrity sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw== dependencies: "@types/node" "*" "@types/tapable" "^1" @@ -1584,94 +1583,101 @@ anymatch "^3.0.0" source-map "^0.6.0" -"@typescript-eslint/eslint-plugin@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz#f156827610a3f8cefc56baeaa93cd4a5f32966b4" - integrity sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg== +"@typescript-eslint/eslint-plugin@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== dependencies: - "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/type-utils" "5.59.5" - "@typescript-eslint/utils" "5.59.5" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - semver "^7.3.7" - tsutils "^3.21.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.5.tgz#63064f5eafbdbfb5f9dfbf5c4503cdf949852981" - integrity sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw== +"@typescript-eslint/parser@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz#33ffc7e8663f42cfaac873de65ebf65d2bce674d" - integrity sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz#485b0e2c5b923460bc2ea6b338c595343f06fc9b" - integrity sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== dependencies: - "@typescript-eslint/typescript-estree" "5.59.5" - "@typescript-eslint/utils" "5.59.5" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" -"@typescript-eslint/types@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.5.tgz#e63c5952532306d97c6ea432cee0981f6d2258c7" - integrity sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz#9b252ce55dd765e972a7a2f99233c439c5101e42" - integrity sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.5.tgz#15b3eb619bb223302e60413adb0accd29c32bcae" - integrity sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA== +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" - eslint-scope "^5.1.1" - semver "^7.3.7" + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" -"@typescript-eslint/visitor-keys@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz#ba5b8d6791a13cf9fea6716af1e7626434b29b9b" - integrity sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "5.59.5" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -1686,10 +1692,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -1705,15 +1711,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -1735,58 +1741,58 @@ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== "@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.0": @@ -1832,9 +1838,9 @@ acorn-jsx@^5.3.2: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== add-px-to-style@1.0.0: version "1.0.0" @@ -1868,7 +1874,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1965,23 +1971,24 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-buffer-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" - integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - call-bind "^1.0.2" - is-array-buffer "^3.0.1" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" -array-includes@^3.1.6: - version "3.1.7" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" - integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" is-string "^1.0.7" array-union@^2.1.0: @@ -1989,47 +1996,83 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== +array.prototype.findlast@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.tosorted@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" - -arraybuffer.prototype.slice@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" - integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== - dependencies: - array-buffer-byte-length "^1.0.0" call-bind "^1.0.2" define-properties "^1.2.0" - get-intrinsic "^1.2.1" - is-array-buffer "^3.0.2" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" arrify@^1.0.1: @@ -2055,9 +2098,9 @@ async@^2.6.4: lodash "^4.17.14" async@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== autoprefixer@10.4.14: version "10.4.14" @@ -2071,10 +2114,12 @@ autoprefixer@10.4.14: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" babel-loader@9.1.2: version "9.1.2" @@ -2089,29 +2134,29 @@ babel-plugin-inline-classnames@2.0.1: resolved "https://registry.yarnpkg.com/babel-plugin-inline-classnames/-/babel-plugin-inline-classnames-2.0.1.tgz#d871490af06781a42f231a1e090bc4133594f168" integrity sha512-Pq/jJ6hTiGiqcMmy2d4CyJcfBDeUHOdQl1t1MDWNaSKR2RxDmShSAx4Zqz6NDmFaiinaRqF8eQoTVgSRGU+McQ== -babel-plugin-polyfill-corejs2@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" + integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.6.1" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" - integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.31.0" + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" -babel-plugin-polyfill-regenerator@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" + integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.6.1" babel-plugin-transform-react-remove-prop-types@0.4.24: version "0.4.24" @@ -2152,9 +2197,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bl@^4.0.3: version "4.1.0" @@ -2202,15 +2247,15 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, browserslist@^4.21.10, browserslist@^4.21.5, browserslist@^4.21.9: - version "4.21.10" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" - integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== +browserslist@^4.14.5, browserslist@^4.21.5, browserslist@^4.22.2, browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== dependencies: - caniuse-lite "^1.0.30001517" - electron-to-chromium "^1.4.477" - node-releases "^2.0.13" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" @@ -2235,13 +2280,16 @@ bytes@1: resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ== -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" @@ -2275,10 +2323,10 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517: - version "1.0.30001591" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz" - integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== +caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001587: + version "1.0.30001611" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz#4dbe78935b65851c2d2df1868af39f709a93a96e" + integrity sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q== chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -2298,9 +2346,9 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: supports-color "^7.1.0" "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -2317,15 +2365,20 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -classnames@*, classnames@2.3.2, classnames@^2.2.6: +classnames@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== +classnames@^2.2.6: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" - integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== dependencies: source-map "~0.6.0" @@ -2463,10 +2516,10 @@ continuable-cache@^0.3.1: resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" integrity sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA== -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== copy-anything@^2.0.1: version "2.0.6" @@ -2475,17 +2528,17 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -core-js-compat@^3.31.0: - version "3.32.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.1.tgz#55f9a7d297c0761a8eb1d31b593e0f5b6ffae964" - integrity sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA== +core-js-compat@^3.31.0, core-js-compat@^3.36.1: + version "3.37.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73" + integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA== dependencies: - browserslist "^4.21.10" + browserslist "^4.23.0" -core-js@3.32.1: - version "3.32.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.1.tgz#a7d8736a3ed9dd05940c3c4ff32c591bb735be77" - integrity sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ== +core-js@3.37.0: + version "3.37.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb" + integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug== core-js@^1.0.0: version "1.2.7" @@ -2514,9 +2567,9 @@ cosmiconfig@^7.0.1: yaml "^1.10.0" cosmiconfig@^8.1.3: - version "8.3.3" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.3.tgz#45985f9f39f3c9330288ef642b1dcb7342bd76d7" - integrity sha512-/VY+0IvFoE47hwgKHu8feeBFIb1Z1mcJFiLrNwaJpLoLa9qwLVquMGMr2OUwQmhpJDtsSQSasg/TMv1imec9xA== + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: import-fresh "^3.3.0" js-yaml "^4.1.0" @@ -2564,9 +2617,9 @@ css-color-function@~1.3.3: rgb "~0.1.0" css-functions-list@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.0.tgz#8290b7d064bf483f48d6559c10e98dc4d1ad19ee" - integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.1.tgz#2eb205d8ce9f9ce74c5c1d7490b66b77c45ce3ea" + integrity sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ== css-loader@6.7.3: version "6.7.3" @@ -2620,28 +2673,55 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2671,11 +2751,21 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -2800,14 +2890,14 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.0.3: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== -electron-to-chromium@^1.4.477: - version "1.4.508" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz#5641ff2f5ba11df4bd960fe6a2f9f70aa8b9af96" - integrity sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg== +electron-to-chromium@^1.4.668: + version "1.4.745" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290" + integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA== element-class@0.2.2: version "0.2.2" @@ -2839,9 +2929,9 @@ end-of-stream@^1.4.1: once "^1.4.0" enhanced-resolve@^5.0.0, enhanced-resolve@^5.14.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + version "5.16.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" + integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2852,9 +2942,9 @@ entities@^2.0.0: integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== envinfo@^7.7.3: - version "7.10.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" - integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + version "7.12.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.12.0.tgz#b56723b39c2053d67ea5714f026d05d4f5cc7acd" + integrity sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg== errno@^0.1.1: version "0.1.8" @@ -2884,71 +2974,117 @@ error@^7.0.0: dependencies: string-template "~0.2.1" -es-abstract@^1.20.4, es-abstract@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" - integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: - array-buffer-byte-length "^1.0.0" - arraybuffer.prototype.slice "^1.0.1" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.1" - get-symbol-description "^1.0.0" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" globalthis "^1.0.3" gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" is-callable "^1.2.7" - is-negative-zero "^2.0.2" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" + is-shared-array-buffer "^1.0.3" is-string "^1.0.7" - is-typed-array "^1.1.10" + is-typed-array "^1.1.13" is-weakref "^1.0.2" - object-inspect "^1.12.3" + object-inspect "^1.13.1" object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.0" - safe-array-concat "^1.0.0" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-buffer "^1.0.0" - typed-array-byte-length "^1.0.0" - typed-array-byte-offset "^1.0.0" - typed-array-length "^1.0.4" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" unbox-primitive "^1.0.2" - which-typed-array "^1.1.10" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.17: + version "1.0.18" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" + integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" es-module-lexer@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" - integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + version "1.5.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236" + integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw== -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== - dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" - -es-shim-unscopables@^1.0.0: +es-object-atoms@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== dependencies: - has "^1.0.3" + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -2965,9 +3101,9 @@ es6-promise@^4.2.8: integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-string-regexp@^1.0.5: version "1.0.5" @@ -2979,12 +3115,12 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" - integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== +eslint-config-prettier@8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== -eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -2993,10 +3129,10 @@ eslint-import-resolver-node@^0.3.7: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-module-utils@^2.7.4: - version "2.8.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" - integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== +eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== dependencies: debug "^3.2.7" @@ -3010,26 +3146,28 @@ eslint-plugin-filenames@1.3.2: lodash.snakecase "4.1.1" lodash.upperfirst "4.3.1" -eslint-plugin-import@2.27.5: - version "2.27.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== +eslint-plugin-import@2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" - eslint-module-utils "^2.7.4" - has "^1.0.3" - is-core-module "^2.11.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.6" - resolve "^1.22.1" - semver "^6.3.0" - tsconfig-paths "^3.14.1" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" eslint-plugin-prettier@4.2.1: version "4.2.1" @@ -3043,33 +3181,36 @@ eslint-plugin-react-hooks@4.6.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@7.32.2: - version "7.32.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" - integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== +eslint-plugin-react@7.34.1: + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" doctrine "^2.1.0" + es-iterator-helpers "^1.0.17" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" prop-types "^15.8.1" - resolve "^2.0.0-next.4" - semver "^6.3.0" - string.prototype.matchall "^4.0.8" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.10" -eslint-plugin-simple-import-sort@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz#cc4ceaa81ba73252427062705b64321946f61351" - integrity sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw== +eslint-plugin-simple-import-sort@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.0.tgz#8186ad55474d2f5c986a2f1bf70625a981e30d05" + integrity sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig== -eslint-scope@5.1.1, eslint-scope@^5.1.1: +eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -3077,7 +3218,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.0: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -3090,32 +3231,33 @@ eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.40.0: - version "8.40.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" - integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== +eslint@8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.3" - "@eslint/js" "8.40.0" - "@humanwhocodes/config-array" "^0.11.8" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.1" - espree "^9.5.2" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3123,25 +3265,22 @@ eslint@8.40.0: find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.2, espree@^9.6.0: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -3205,9 +3344,9 @@ fast-diff@^1.1.2: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3231,9 +3370,9 @@ fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -3331,18 +3470,23 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.1.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" - integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.2.7" + flatted "^3.2.9" keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== focus-lock@^0.11.6: version "0.11.6" @@ -3377,9 +3521,9 @@ fork-ts-checker-webpack-plugin@8.0.0: tapable "^2.2.1" fraction.js@^4.2.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d" - integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg== + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fs-constants@^1.0.0: version "1.0.0" @@ -3396,9 +3540,9 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: universalify "^2.0.0" fs-monkey@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.4.tgz#ee8c1b53d3fe8bb7e5d2c5c5dfc0168afdd2f747" - integrity sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== fs.realpath@^1.0.0: version "1.0.0" @@ -3410,12 +3554,12 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.5: +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== @@ -3440,28 +3584,30 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-node-dimensions@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -3526,9 +3672,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.21.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" - integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -3575,10 +3721,10 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== gud@^1.0.0: version "1.0.0" @@ -3605,36 +3751,36 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: - has-symbols "^1.0.2" + has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" he@^1.2.0: version "1.2.0" @@ -3746,9 +3892,9 @@ ieee754@^1.1.13: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0, ignore@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== image-size@~0.5.0: version "0.5.5" @@ -3756,11 +3902,11 @@ image-size@~0.5.0: integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== "immutable@^3.8.1 || ^4.0.0", immutable@^4.0.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" - integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + version "4.3.5" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" + integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== -import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3809,13 +3955,13 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + es-errors "^1.3.0" + hasown "^2.0.0" side-channel "^1.0.4" interpret@^3.1.1: @@ -3830,20 +3976,26 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" - integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: call-bind "^1.0.2" - get-intrinsic "^1.2.0" - is-typed-array "^1.1.10" + get-intrinsic "^1.2.1" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -3871,14 +4023,21 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== +is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" -is-date-object@^1.0.1: +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -3890,11 +4049,25 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -3902,10 +4075,15 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-number-object@^1.0.4: version "1.0.7" @@ -3954,12 +4132,17 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" is-stream@^1.0.1: version "1.1.0" @@ -3980,12 +4163,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.9: - version "1.1.12" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" - integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== dependencies: - which-typed-array "^1.1.11" + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2: version "1.0.2" @@ -3994,6 +4182,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-what@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" @@ -4037,6 +4233,17 @@ isomorphic-fetch@^2.1.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + jdu@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jdu/-/jdu-1.0.0.tgz#28f1e388501785ae0a1d93e93ed0b14dd41e51ce" @@ -4052,20 +4259,15 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jiti@^1.18.2: - version "1.19.3" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.3.tgz#ef554f76465b3c2b222dc077834a71f0d4a37569" - integrity sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w== + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== jquery@3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.0.tgz#fe2c01a05da500709006d8790fe21c8a39d75612" integrity sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ== -js-sdsl@^4.1.4: - version "4.4.2" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" - integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4150,9 +4352,9 @@ just-curry-it@^3.1.0: integrity sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg== keyv@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -4361,6 +4563,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4375,11 +4582,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.0.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" - integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== - make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -4505,6 +4707,13 @@ mini-css-extract-plugin@2.7.5: dependencies: schema-utils "^4.0.0" +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4553,9 +4762,9 @@ minipass@^4.2.4: integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" - integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== mkdirp@^0.5.6: version "0.5.6" @@ -4589,15 +4798,10 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" @@ -4605,11 +4809,10 @@ natural-compare@^1.4.0: integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== needle@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44" - integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" + integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== dependencies: - debug "^3.2.6" iconv-lite "^0.6.3" sax "^1.2.4" @@ -4646,10 +4849,10 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-releases@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== normalize-package-data@^2.5.0: version "2.5.0" @@ -4703,60 +4906,71 @@ object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" + call-bind "^1.0.5" + define-properties "^1.2.1" has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" - integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -object.fromentries@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" - integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" -object.hasown@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" -object.values@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" - integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== +object.hasown@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" once@^1.3.0, once@^1.4.0: version "1.4.0" @@ -4765,7 +4979,7 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" -optionator@^0.9.1: +optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -4876,11 +5090,11 @@ path-parse@^1.0.7: integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-scurry@^1.6.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@^1.7.0: @@ -4936,6 +5150,11 @@ portfinder@^1.0.17: debug "^3.2.7" mkdirp "^0.5.6" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-color-function@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" @@ -4992,23 +5211,23 @@ postcss-mixins@9.0.4: sugarss "^4.0.1" postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" - integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== dependencies: postcss-selector-parser "^6.0.4" @@ -5037,9 +5256,9 @@ postcss-safe-parser@^6.0.0: integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" + integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -5074,14 +5293,14 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.23: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== +postcss@8.4.38, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== dependencies: - nanoid "^3.3.6" + nanoid "^3.3.7" picocolors "^1.0.0" - source-map-js "^1.0.2" + source-map-js "^1.2.0" postcss@^6.0.23: version "6.0.23" @@ -5092,15 +5311,6 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: - version "8.4.29" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd" - integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -5163,9 +5373,9 @@ psl@^1.1.33: integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== qs@6.11.1: version "6.11.1" @@ -5175,11 +5385,11 @@ qs@6.11.1: side-channel "^1.0.4" qs@^6.4.0: - version "6.11.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" - integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" querystringify@^2.1.1: version "2.2.0" @@ -5604,10 +5814,23 @@ redux@4.2.1, redux@^4.0.0, redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== dependencies: regenerate "^1.4.2" @@ -5622,9 +5845,9 @@ regenerator-runtime@^0.11.0: integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regenerator-transform@^0.15.2: version "0.15.2" @@ -5633,14 +5856,15 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" - integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - functions-have-names "^1.2.3" + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" regexpu-core@^5.3.1: version "5.3.2" @@ -5734,21 +5958,21 @@ resolve-pathname@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" - integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.4: - version "2.0.0-next.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -5783,13 +6007,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-array-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" - integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.0" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" has-symbols "^1.0.3" isarray "^2.0.5" @@ -5808,13 +6032,13 @@ safe-json-parse@~1.0.1: resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" integrity sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A== -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" + call-bind "^1.0.6" + es-errors "^1.3.0" is-regex "^1.1.4" "safer-buffer@>= 2.1.2 < 3.0.0": @@ -5823,15 +6047,20 @@ safe-regex-test@^1.0.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.58.3: - version "1.66.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.66.1.tgz#04b51c4671e4650aa393740e66a4e58b44d055b1" - integrity sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA== + version "1.75.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.75.0.tgz#91bbe87fb02dfcc34e052ddd6ab80f60d392be6c" + integrity sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sax@^1.2.4, sax@~1.2.4: +sax@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + +sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -5883,25 +6112,47 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== +semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -5936,14 +6187,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^4.0.1: version "4.1.0" @@ -5964,10 +6216,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== source-map-support@~0.5.20: version "0.5.21" @@ -6001,9 +6253,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -6014,9 +6266,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + version "3.0.17" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" + integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== stack-generator@^2.0.5: version "2.0.10" @@ -6061,46 +6313,51 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.8: - version "4.0.9" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.9.tgz#148779de0f75d36b13b15885fec5cadde994520d" - integrity sha512-6i5hL3MqG/K2G43mWXWgP+qizFW/QH/7kCNN13JrJS5q48FN5IKksLDscexKP3dnmB6cdm9jlNgAsWNLpSykmA== +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@0.10: version "0.10.31" @@ -6140,7 +6397,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -6267,9 +6524,9 @@ svg-tags@^1.0.0: integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== table@^6.8.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== dependencies: ajv "^8.0.1" lodash.truncate "^4.4.2" @@ -6305,20 +6562,20 @@ terser-webpack-plugin@5.3.8: terser "^5.16.8" terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.10.0, terser@^5.16.8: - version "5.19.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.3.tgz#359baeba615aef13db4b8c4d77a2aa0d8814aa9e" - integrity sha512-pQzJ9UJzM0IgmT4FAtYI6+VqFf0lj/to58AV0Xfgg0Up37RyPG7Al+1cepC6/BVuAxR9oNb41/DL4DEoHJvTdg== +terser@^5.10.0, terser@^5.16.8, terser@^5.26.0: + version "5.30.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" + integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -6336,9 +6593,9 @@ tiny-emitter@^2.0.0: integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== tiny-invariant@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== tiny-lr@^1.1.1: version "1.1.1" @@ -6408,6 +6665,11 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-loader@9.4.2: version "9.4.2" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" @@ -6418,10 +6680,10 @@ ts-loader@9.4.2: micromatch "^4.0.0" semver "^7.3.4" -tsconfig-paths@^3.14.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" - integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.2" @@ -6437,23 +6699,11 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -6481,44 +6731,49 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typed-array-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" - integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - is-typed-array "^1.1.10" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" -typed-array-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" - integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -typed-array-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" - integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - is-typed-array "^1.1.9" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" typed-styles@^0.0.7: version "0.0.7" @@ -6547,15 +6802,15 @@ typescript-plugin-css-modules@5.0.1: stylus "^0.59.0" tsconfig-paths "^4.1.2" -typescript@4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== + version "0.7.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" + integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== unbox-primitive@^1.0.2: version "1.0.2" @@ -6567,6 +6822,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -6604,14 +6864,14 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -6641,9 +6901,9 @@ url-parse@^1.5.3: requires-port "^1.0.0" use-callback-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" - integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== dependencies: tslib "^2.0.0" @@ -6701,9 +6961,9 @@ warning@^4.0.2: loose-envify "^1.0.0" watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -6743,11 +7003,12 @@ webpack-livereload-plugin@3.0.2: tiny-lr "^1.1.1" webpack-merge@^5.7.3: - version "5.9.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" - integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== dependencies: clone-deep "^4.0.1" + flat "^5.0.2" wildcard "^2.0.0" webpack-sources@^3.2.3: @@ -6800,9 +7061,9 @@ websocket-extensions@>=0.1.1: integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-fetch@>=0.10.0: - version "3.6.18" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.18.tgz#2f640cdee315abced7daeaed2309abd1e44e62d4" - integrity sha512-ltN7j66EneWn5TFDO4L9inYC1D+Czsxlrw2SalgjMmEMkLfA5SIZxEFdE6QtHFiiM6Q7WL32c7AkI3w6yxM84Q== + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== whatwg-url@^5.0.0: version "5.0.0" @@ -6823,16 +7084,44 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.10, which-typed-array@^1.1.11: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-tostringtag "^1.0.0" + has-tostringtag "^1.0.2" which@^1.3.1: version "1.3.1" From d051dac12c8b797761a0d1f3b4aa84cff47ed13d Mon Sep 17 00:00:00 2001 From: Jared <github@sillock.io> Date: Sun, 28 Apr 2024 01:06:26 +0000 Subject: [PATCH 262/762] New: Optionally use Environment Variables for settings in config.xml Closes #6744 --- .../ConfigFileProviderTest.cs | 22 +++++ .../ServiceFactoryFixture.cs | 6 ++ src/NzbDrone.Common/Options/AppOptions.cs | 8 ++ src/NzbDrone.Common/Options/AuthOptions.cs | 9 +++ src/NzbDrone.Common/Options/LogOptions.cs | 14 ++++ src/NzbDrone.Common/Options/ServerOptions.cs | 12 +++ src/NzbDrone.Common/Options/UpdateOptions.cs | 9 +++ .../Configuration/ConfigFileProvider.cs | 80 ++++++++++++------- src/NzbDrone.Host.Test/ContainerFixture.cs | 6 ++ src/NzbDrone.Host/Bootstrap.cs | 23 ++++-- 10 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 src/NzbDrone.Common/Options/AppOptions.cs create mode 100644 src/NzbDrone.Common/Options/AuthOptions.cs create mode 100644 src/NzbDrone.Common/Options/LogOptions.cs create mode 100644 src/NzbDrone.Common/Options/ServerOptions.cs create mode 100644 src/NzbDrone.Common/Options/UpdateOptions.cs diff --git a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index f381a96a0..b6fe45584 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using FluentAssertions; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Test.Common; @@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test Mocker.GetMock<IDiskProvider>() .Setup(v => v.WriteAllText(configFile, It.IsAny<string>())) .Callback<string, string>((p, t) => _configFileContents = t); + + Mocker.GetMock<IOptions<AuthOptions>>() + .Setup(v => v.Value) + .Returns(new AuthOptions()); + + Mocker.GetMock<IOptions<AppOptions>>() + .Setup(v => v.Value) + .Returns(new AppOptions()); + + Mocker.GetMock<IOptions<ServerOptions>>() + .Setup(v => v.Value) + .Returns(new ServerOptions()); + + Mocker.GetMock<IOptions<LogOptions>>() + .Setup(v => v.Value) + .Returns(new LogOptions()); + + Mocker.GetMock<IOptions<UpdateOptions>>() + .Setup(v => v.Value) + .Returns(new UpdateOptions()); } [Test] diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 58642f2e4..6749006e5 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Lifecycle; @@ -33,6 +34,11 @@ namespace NzbDrone.Common.Test container.RegisterInstance(new Mock<IHostLifetime>().Object); container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object); + container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object); + container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object); + container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object); + container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object); + container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object); var serviceProvider = container.GetServiceProvider(); diff --git a/src/NzbDrone.Common/Options/AppOptions.cs b/src/NzbDrone.Common/Options/AppOptions.cs new file mode 100644 index 000000000..74cdf1d29 --- /dev/null +++ b/src/NzbDrone.Common/Options/AppOptions.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Common.Options; + +public class AppOptions +{ + public string InstanceName { get; set; } + public string Theme { get; set; } + public bool? LaunchBrowser { get; set; } +} diff --git a/src/NzbDrone.Common/Options/AuthOptions.cs b/src/NzbDrone.Common/Options/AuthOptions.cs new file mode 100644 index 000000000..2b63308d3 --- /dev/null +++ b/src/NzbDrone.Common/Options/AuthOptions.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.Options; + +public class AuthOptions +{ + public string ApiKey { get; set; } + public bool? Enabled { get; set; } + public string Method { get; set; } + public string Required { get; set; } +} diff --git a/src/NzbDrone.Common/Options/LogOptions.cs b/src/NzbDrone.Common/Options/LogOptions.cs new file mode 100644 index 000000000..c36533cb4 --- /dev/null +++ b/src/NzbDrone.Common/Options/LogOptions.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Common.Options; + +public class LogOptions +{ + public string Level { get; set; } + public bool? FilterSentryEvents { get; set; } + public int? Rotate { get; set; } + public bool? Sql { get; set; } + public string ConsoleLevel { get; set; } + public bool? AnalyticsEnabled { get; set; } + public string SyslogServer { get; set; } + public int? SyslogPort { get; set; } + public string SyslogLevel { get; set; } +} diff --git a/src/NzbDrone.Common/Options/ServerOptions.cs b/src/NzbDrone.Common/Options/ServerOptions.cs new file mode 100644 index 000000000..d21e12b2a --- /dev/null +++ b/src/NzbDrone.Common/Options/ServerOptions.cs @@ -0,0 +1,12 @@ +namespace NzbDrone.Common.Options; + +public class ServerOptions +{ + public string UrlBase { get; set; } + public string BindAddress { get; set; } + public int? Port { get; set; } + public bool? EnableSsl { get; set; } + public int? SslPort { get; set; } + public string SslCertPath { get; set; } + public string SslCertPassword { get; set; } +} diff --git a/src/NzbDrone.Common/Options/UpdateOptions.cs b/src/NzbDrone.Common/Options/UpdateOptions.cs new file mode 100644 index 000000000..a8eaad8fb --- /dev/null +++ b/src/NzbDrone.Common/Options/UpdateOptions.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.Options; + +public class UpdateOptions +{ + public string Mechanism { get; set; } + public bool? Automatically { get; set; } + public string ScriptPath { get; set; } + public string Branch { get; set; } +} diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index fc2a29eb4..e1168c2fb 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Datastore; @@ -70,6 +71,11 @@ namespace NzbDrone.Core.Configuration private readonly IDiskProvider _diskProvider; private readonly ICached<string> _cache; private readonly PostgresOptions _postgresOptions; + private readonly AuthOptions _authOptions; + private readonly AppOptions _appOptions; + private readonly ServerOptions _serverOptions; + private readonly UpdateOptions _updateOptions; + private readonly LogOptions _logOptions; private readonly string _configFile; private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -80,13 +86,23 @@ namespace NzbDrone.Core.Configuration ICacheManager cacheManager, IEventAggregator eventAggregator, IDiskProvider diskProvider, - IOptions<PostgresOptions> postgresOptions) + IOptions<PostgresOptions> postgresOptions, + IOptions<AuthOptions> authOptions, + IOptions<AppOptions> appOptions, + IOptions<ServerOptions> serverOptions, + IOptions<UpdateOptions> updateOptions, + IOptions<LogOptions> logOptions) { _cache = cacheManager.GetCache<string>(GetType()); _eventAggregator = eventAggregator; _diskProvider = diskProvider; _configFile = appFolderInfo.GetConfigPath(); _postgresOptions = postgresOptions.Value; + _authOptions = authOptions.Value; + _appOptions = appOptions.Value; + _serverOptions = serverOptions.Value; + _updateOptions = updateOptions.Value; + _logOptions = logOptions.Value; } public Dictionary<string, object> GetConfigDictionary() @@ -142,7 +158,7 @@ namespace NzbDrone.Core.Configuration { const string defaultValue = "*"; - var bindAddress = GetValue("BindAddress", defaultValue); + var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue); if (string.IsNullOrWhiteSpace(bindAddress)) { return defaultValue; @@ -152,19 +168,19 @@ namespace NzbDrone.Core.Configuration } } - public int Port => GetValueInt("Port", 8989); + public int Port => _serverOptions.Port ?? GetValueInt("Port", 8989); - public int SslPort => GetValueInt("SslPort", 9898); + public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 9898); - public bool EnableSsl => GetValueBoolean("EnableSsl", false); + public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false); - public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true); + public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true); public string ApiKey { get { - var apiKey = GetValue("ApiKey", GenerateApiKey()); + var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey()); if (apiKey.IsNullOrWhiteSpace()) { @@ -180,7 +196,7 @@ namespace NzbDrone.Core.Configuration { get { - var enabled = GetValueBoolean("AuthenticationEnabled", false, false); + var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false); if (enabled) { @@ -188,20 +204,25 @@ namespace NzbDrone.Core.Configuration return AuthenticationType.Basic; } - return GetValueEnum("AuthenticationMethod", AuthenticationType.None); + return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue) + ? enumValue + : GetValueEnum("AuthenticationMethod", AuthenticationType.None); } } - public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public AuthenticationRequiredType AuthenticationRequired => + Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue) + ? enumValue + : GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); - public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); + public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false); - public string Branch => GetValue("Branch", "main").ToLowerInvariant(); + public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant(); - public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant(); - public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); + public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant(); + public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false); - public string Theme => GetValue("Theme", "auto", persist: false); + public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false); public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false); public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false); @@ -210,17 +231,17 @@ namespace NzbDrone.Core.Configuration public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "sonarr-log", persist: false); public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false); - public bool LogSql => GetValueBoolean("LogSql", false, persist: false); - public int LogRotate => GetValueInt("LogRotate", 50, persist: false); - public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); - public string SslCertPath => GetValue("SslCertPath", ""); - public string SslCertPassword => GetValue("SslCertPassword", ""); + public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false); + public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false); + public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false); + public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", ""); + public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", ""); public string UrlBase { get { - var urlBase = GetValue("UrlBase", "").Trim('/'); + var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/'); if (urlBase.IsNullOrWhiteSpace()) { @@ -237,7 +258,7 @@ namespace NzbDrone.Core.Configuration { get { - var instanceName = GetValue("InstanceName", BuildInfo.AppName); + var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName); if (instanceName.StartsWith(BuildInfo.AppName) || instanceName.EndsWith(BuildInfo.AppName)) { @@ -248,17 +269,20 @@ namespace NzbDrone.Core.Configuration } } - public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false); + public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false); - public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); + public UpdateMechanism UpdateMechanism => + Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue) + ? enumValue + : GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); - public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false); + public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false); - public string SyslogServer => GetValue("SyslogServer", "", persist: false); + public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false); - public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false); + public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false); - public string SyslogLevel => GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant(); + public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant(); public int GetValueInt(string key, int defaultValue, bool persist = true) { diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 6a43eaff9..8279a18f0 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Common; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; @@ -46,6 +47,11 @@ namespace NzbDrone.App.Test container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object); container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object); container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object); + container.RegisterInstance<IOptions<AuthOptions>>(new Mock<IOptions<AuthOptions>>().Object); + container.RegisterInstance<IOptions<AppOptions>>(new Mock<IOptions<AppOptions>>().Object); + container.RegisterInstance<IOptions<ServerOptions>>(new Mock<IOptions<ServerOptions>>().Object); + container.RegisterInstance<IOptions<UpdateOptions>>(new Mock<IOptions<UpdateOptions>>().Object); + container.RegisterInstance<IOptions<LogOptions>>(new Mock<IOptions<LogOptions>>().Object); _container = container.GetServiceProvider(); } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 0e9ff30a4..2073eb622 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -20,6 +20,7 @@ using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; using Sonarr.Http.ClientSchema; @@ -98,6 +99,11 @@ namespace NzbDrone.Host .ConfigureServices(services => { services.Configure<PostgresOptions>(config.GetSection("Sonarr:Postgres")); + services.Configure<AppOptions>(config.GetSection("Sonarr:App")); + services.Configure<AuthOptions>(config.GetSection("Sonarr:Auth")); + services.Configure<ServerOptions>(config.GetSection("Sonarr:Server")); + services.Configure<LogOptions>(config.GetSection("Sonarr:Log")); + services.Configure<UpdateOptions>(config.GetSection("Sonarr:Update")); }).Build(); break; @@ -119,12 +125,12 @@ namespace NzbDrone.Host { var config = GetConfiguration(context); - var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*"); - var port = config.GetValue(nameof(ConfigFileProvider.Port), 8989); - var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), 9898); - var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); - var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath)); - var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword)); + var bindAddress = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.BindAddress)}") ?? config.GetValue(nameof(ConfigFileProvider.BindAddress), "*"); + var port = config.GetValue<int?>($"Sonarr:Server:{nameof(ServerOptions.Port)}") ?? config.GetValue(nameof(ConfigFileProvider.Port), 8989); + var sslPort = config.GetValue<int?>($"Sonarr:Server:{nameof(ServerOptions.SslPort)}") ?? config.GetValue(nameof(ConfigFileProvider.SslPort), 9898); + var enableSsl = config.GetValue<bool?>($"Sonarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); + var sslCertPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath)); + var sslCertPassword = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword)); var urls = new List<string> { BuildUrl("http", bindAddress, port) }; @@ -152,6 +158,11 @@ namespace NzbDrone.Host .ConfigureServices(services => { services.Configure<PostgresOptions>(config.GetSection("Sonarr:Postgres")); + services.Configure<AppOptions>(config.GetSection("Sonarr:App")); + services.Configure<AuthOptions>(config.GetSection("Sonarr:Auth")); + services.Configure<ServerOptions>(config.GetSection("Sonarr:Server")); + services.Configure<LogOptions>(config.GetSection("Sonarr:Log")); + services.Configure<UpdateOptions>(config.GetSection("Sonarr:Update")); }) .ConfigureWebHost(builder => { From 1df7cdc65efc343042659712c882ff9235d0ee17 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 24 Apr 2024 18:57:41 +0300 Subject: [PATCH 263/762] New: Add KRaLiMaRKo and BluDragon to release group parsing exceptions ignore-downstream --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index b22d7c43a..5d4984954 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -85,6 +85,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")] [TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")] [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")] + [TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")] + [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index c4f61fbce..e111cfae6 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -538,7 +538,7 @@ namespace NzbDrone.Core.Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon)\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); From 973810104287d515fb73b9103b706087baa77c36 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:06:10 +0300 Subject: [PATCH 264/762] Treat CorruptDatabaseException as a startup failure ignore-downstream --- src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs b/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs index 4df2bfea4..895a11adf 100644 --- a/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs +++ b/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs @@ -3,7 +3,7 @@ using NzbDrone.Common.Exceptions; namespace NzbDrone.Core.Datastore { - public class CorruptDatabaseException : NzbDroneException + public class CorruptDatabaseException : SonarrStartupException { public CorruptDatabaseException(string message, params object[] args) : base(message, args) @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Datastore } public CorruptDatabaseException(string message, Exception innerException, params object[] args) - : base(message, innerException, args) + : base(innerException, message, args) { } public CorruptDatabaseException(string message, Exception innerException) - : base(message, innerException) + : base(innerException, message) { } } From 04bd535cfca5e25c6a2d5417c6f18d5bf5180f67 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 28 Apr 2024 03:07:41 +0200 Subject: [PATCH 265/762] New: Don't initially select 0 byte files in Interactive Import Closes #6686 --- .../src/InteractiveImport/Interactive/InteractiveImportRow.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 3a2b81874..402be769a 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -128,7 +128,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { seasonNumber != null && episodes.length && quality && - languages + languages && + size > 0 ) { onSelectedChange({ id, From efb3fa93e4ac28afde681f6f34ec8edbf99cd385 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 28 Apr 2024 04:08:40 +0300 Subject: [PATCH 266/762] Fixed: Retrying download for pushed releases ignore-downstream #6752 --- src/NzbDrone.Core/Download/DownloadClientBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 53318fb68..ed5c36568 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Download { { Result.HasHttpServerError: true } => PredicateResult.True(), { Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(), + { Exception: HttpException { Response.HasHttpServerError: true } } => PredicateResult.True(), _ => PredicateResult.False() }, Delay = TimeSpan.FromSeconds(3), From c81ae6546118e954e481894d0b3fa6e9a20359c7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:59:00 +0300 Subject: [PATCH 267/762] Fixed: Limit titles in task name to 10 series --- .../Tasks/Queued/QueuedTaskRowNameCell.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx index a3e327e01..70058af02 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -6,6 +6,22 @@ import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector import translate from 'Utilities/String/translate'; import styles from './QueuedTaskRowNameCell.css'; +function formatTitles(titles: string[]) { + if (!titles) { + return null; + } + + if (titles.length > 11) { + return ( + <span title={titles.join(', ')}> + {titles.slice(0, 10).join(', ')}, {titles.length - 10} more + </span> + ); + } + + return <span>{titles.join(', ')}</span>; +} + export interface QueuedTaskRowNameCellProps { commandName: string; body: CommandBody; @@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell( <span className={styles.commandName}> {commandName} {sortedSeries.length ? ( - <span> - {sortedSeries.map((s) => s.title).join(', ')}</span> + <span> - {formatTitles(sortedSeries.map((s) => s.title))}</span> ) : null} {body.seasonNumber ? ( <span> From 8ddf46113b48a83d76e44bbf07dea17732aa3efe Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 1 May 2024 13:59:39 +0000 Subject: [PATCH 268/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 4 +- .../Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/tr.json | 338 ++++++++++++++++-- 3 files changed, 315 insertions(+), 29 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 8a7af7034..0d19af500 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1467,7 +1467,7 @@ "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", + "SslCertPasswordHelpText": "Contraseña para archivo pfx", "SslPort": "Puerto SSL", "StandardEpisodeFormat": "Formato de episodio estándar", "StartProcessing": "Iniciar procesamiento", @@ -1668,7 +1668,7 @@ "Uppercase": "Mayúsculas", "SeriesDetailsRuntime": "{runtime} minutos", "ShowBannersHelpText": "Muestra banners en lugar de títulos", - "SslCertPathHelpText": "Ruta al archivo pfx", + "SslCertPathHelpText": "Ruta del archivo pfx", "Umask750Description": "{octal} - Usuario escribe, Grupo lee", "UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío", "UpdateAll": "Actualizar todo", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index c30a75c60..74578fd45 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -846,7 +846,7 @@ "SpecialsFolderFormat": "Formato da Pasta para Especiais", "SslCertPassword": "Senha do Certificado SSL", "SslCertPasswordHelpText": "Senha para arquivo pfx", - "SslCertPath": "Caminho do certificado SSL", + "SslCertPath": "Caminho do Certificado SSL", "SslCertPathHelpText": "Caminho para o arquivo pfx", "SslPort": "Porta SSL", "StandardEpisodeFormat": "Formato do Episódio Padrão", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index dd97a1d37..6e3810449 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -10,7 +10,7 @@ "AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}", "EditIndexerImplementation": "Koşul Ekle - {implementationName}", "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", - "AddedToDownloadQueue": "İndirme sırasına eklendi", + "AddedToDownloadQueue": "İndirme kuyruğuna eklendi", "AllTitles": "Tüm Başlıklar", "AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları", "Actions": "Eylemler", @@ -34,7 +34,7 @@ "AddIndexer": "Dizin Oluşturucu Ekle", "AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın", "AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", - "AddReleaseProfile": "Sürüm Profili Ekle", + "AddReleaseProfile": "Yayın Profili Ekle", "AddRemotePathMapping": "Uzak Yol Eşleme Ekleme", "AddRootFolder": "Kök Klasör Ekle", "AddSeriesWithTitle": "{title} Ekleyin", @@ -69,7 +69,7 @@ "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", + "ClickToChangeReleaseGroup": "Yayı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", @@ -80,7 +80,7 @@ "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", + "BypassDelayIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip yayın 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", @@ -104,8 +104,8 @@ "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", + "BlocklistReleaseHelpText": "Bu yayın {appName} tarafından RSS veya Otomatik Arama yoluyla yeniden indirilmesi engelleniyor", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Yayını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", @@ -145,7 +145,7 @@ "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", "CloneAutoTag": "Otomatik Etiketi Klonla", "Dash": "Çizgi", - "DeleteReleaseProfileMessageText": "'{name}' bu sürüm profilini silmek istediğinizden emin misiniz?", + "DeleteReleaseProfileMessageText": "'{name}' bu yayımlama profilini silmek istediğinizden emin misiniz?", "DownloadClientFreeboxApiError": "Freebox API'si şu hatayı döndürdü: {errorDescription}", "DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil", "DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?", @@ -154,7 +154,7 @@ "DeletedReasonUpgrade": "Bir yükseltmeyi içe aktarmak için dosya silindi", "DelayMinutes": "{delay} Dakika", "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", - "DeleteReleaseProfile": "Sürüm Profilini Sil", + "DeleteReleaseProfile": "Yayımlama Profilini Sil", "DeleteSelectedIndexers": "Dizin Oluşturucuları Sil", "Directory": "Rehber", "Donate": "Bağış yap", @@ -165,7 +165,7 @@ "DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.", "DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın", "DefaultNameCopiedProfile": "{name} - Kopyala", - "DeleteAutoTag": "Etiketi Otomatik Sil", + "DeleteAutoTag": "Etiketi Otomatik Sil", "DeleteCondition": "Koşulu Sil", "DeleteDelayProfileMessageText": "Bu gecikme profilini silmek istediğinizden emin misiniz?", "DeleteRootFolder": "Kök Klasörü Sil", @@ -195,7 +195,7 @@ "DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu", "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", "DeleteImportList": "İçe Aktarma Listesini Sil", - "IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan sürümler için eşitlik bozucu olarak sürümler alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", + "IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", @@ -208,7 +208,7 @@ "DeleteAutoTagHelpText": "'{name}' etiketini otomatik silmek istediğinizden emin misiniz?", "DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}", "DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir", - "DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {istisnaMessage})", + "DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {exceptionMessage})", "CustomFormatsSettingsTriggerInfo": "Bir yayına veya dosyaya, seçilen farklı koşul türlerinden en az biriyle eşleştiğinde Özel Format uygulanacaktır.", "Default": "Varsayılan", "DeleteSelectedImportListsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?", @@ -218,16 +218,16 @@ "DownloadClientDelugeSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", "ApiKey": "API Anahtarı", - "Analytics": "Analiz", + "Analytics": "Analitik", "All": "Hepsi", - "AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak", + "AppDataLocationHealthCheckMessage": "Güncelleme sırasında AppData'nın silinmesini önlemek için güncelleme yapılmayacaktır", "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.", - "Backup": "Yedek", + "Backup": "Yedekler", "BindAddress": "Bind Adresi", "DownloadClientFreeboxSettingsApiUrl": "API URL'si", "DownloadClientFreeboxSettingsAppId": "Uygulama kimliği", "DownloadClientFreeboxNotLoggedIn": "Giriş yapmadınız", - "DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı", + "DownloadClientFreeboxSettingsAppToken": "Uygulama Jetonu", "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')", "Apply": "Uygula", "DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}", @@ -276,7 +276,7 @@ "DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName}, etiketi qBittorrent'e ekleyemedi.", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "Sıraya Alma Etkin Değil", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "kuyruğa Alma Etkin Değil", "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.", "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd, sürüm 3.0.0 veya üzerini varsayarak sürüm geliştirir.", "DownloadClientValidationUnableToConnect": "{clientName} ile bağlantı kurulamıyor", @@ -313,7 +313,7 @@ "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Tarih sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.", "DownloadClientValidationGroupMissing": "Grup mevcut değil", "DownloadClientValidationTestTorrents": "Torrentlerin listesi alınamadı: {exceptionMessage}", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz", "DownloadClientSettingsCategoryHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir.", "DownloadClientSettingsOlderPriority": "Eski Önceliği", "DownloadClientValidationTestNzbs": "NZB'lerin listesi alınamadı: {exceptionMessage}", @@ -337,7 +337,7 @@ "Imported": "İçe aktarıldı", "NotificationsAppriseSettingsTagsHelpText": "İsteğe bağlı olarak yalnızca uygun şekilde etiketlenenleri bilgilendirin.", "NotificationsDiscordSettingsAvatar": "Avatar", - "NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Tokenı", + "NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Jetonu", "ImportScriptPath": "Komut Dosyası Yolunu İçe Aktar", "History": "Geçmiş", "EditSelectedImportLists": "Seçilen İçe Aktarma Listelerini Düzenle", @@ -349,7 +349,7 @@ "FormatShortTimeSpanHours": "{hours} saat", "FormatRuntimeMinutes": "{minutes}dk", "FullColorEventsHelpText": "Etkinliğin tamamını yalnızca sol kenar yerine durum rengiyle renklendirecek şekilde stil değiştirildi. Gündem için geçerli değildir", - "GrabId": "ID Yakala", + "GrabId": "ID'den Yakala", "ImportUsingScriptHelpText": "Bir komut dosyası kullanarak içe aktarmak için dosyaları kopyalayın (ör. kod dönüştürme için)", "InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için", "ManageDownloadClients": "İndirme İstemcilerini Yönet", @@ -373,8 +373,8 @@ "NotificationsKodiSettingsDisplayTimeHelpText": "Bildirimin ne kadar süreyle görüntüleneceği (Saniye cinsinden)", "NotificationsMailgunSettingsUseEuEndpoint": "AB Uç Noktasını Kullan", "NotificationsMailgunSettingsSenderDomain": "Gönderen Alanı", - "NotificationsNtfySettingsAccessToken": "Erişim Token'ı", - "NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı belirteç tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir", + "NotificationsNtfySettingsAccessToken": "Erişim Jetonu", + "NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı jeton tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir", "NotificationsNtfySettingsPasswordHelpText": "İsteğe bağlı şifre", "NotificationsNtfySettingsTagsEmojisHelpText": "Kullanılacak etiketlerin veya emojilerin isteğe bağlı listesi", "NotificationsNtfySettingsTopics": "Konular", @@ -393,8 +393,8 @@ "MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)", "NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor", "EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle", - "EnableProfileHelpText": "Sürüm profilini etkinleştirmek için işaretleyin", - "EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli aralıklarla sürüm değişikliği aradığında kullanacak", + "EnableProfileHelpText": "Yayımlama profilini etkinleştirmek için işaretleyin", + "EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli periyotlarda yayın değişikliği aradığında kullanacak", "FormatTimeSpanDays": "{days}g {time}", "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "NotificationsNtfySettingsTagsEmojis": "Ntfy Etiketler ve Emojiler", @@ -425,7 +425,7 @@ "NotificationsJoinValidationInvalidDeviceId": "Cihaz kimlikleri geçersiz görünüyor.", "NotificationsNtfySettingsClickUrl": "URL'ye tıklayın", "NotificationsNotifiarrSettingsApiKeyHelpText": "Profilinizdeki API anahtarınız", - "EditReleaseProfile": "Sürüm Profilini Düzenle", + "EditReleaseProfile": "Yayımlama Profilini Düzenle", "EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle", "FormatShortTimeSpanMinutes": "{minutes} dakika", "FullColorEvents": "Tam Renkli Etkinlikler", @@ -482,7 +482,7 @@ "NotificationsEmailSettingsCcAddress": "CC Adres(ler)i", "NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i", "NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplık Güncellensin mi?", - "NotificationsGotifySettingsAppToken": "Uygulama Token'ı", + "NotificationsGotifySettingsAppToken": "Uygulama Jetonu", "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", "Test": "Sına", @@ -506,5 +506,291 @@ "NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?", "NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın", "InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", - "IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin" + "IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin", + "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", + "LastDuration": "Yürütme Süresi", + "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", + "NotificationsPlexSettingsAuthToken": "Kimlik Doğrulama Jetonu", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Alıcının Grup Kimliği / Telefon Numarası", + "NotificationsSignalSettingsUsernameHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan kullanıcı adı", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Trakt ile kimlik doğrulama", + "NotificationsTwitterSettingsDirectMessageHelpText": "Herkese açık mesaj yerine doğrudan mesaj gönderin", + "NotificationsValidationUnableToSendTestMessage": "Test mesajı gönderilemiyor: {exceptionMessage}", + "OnManualInteractionRequired": "Manuel Etkileşim Gerektiğinde", + "Release": "Yayın", + "Remove": "Kaldır", + "RemoveFromDownloadClient": "İndirme İstemcisinden Kaldır", + "RemoveFromDownloadClientHint": "İndirilenleri ve dosyaları indirme istemcisinden kaldırır", + "ResetTitles": "Başlıkları Sıfırla", + "Space": "Boşluk", + "UsenetDelayTime": "Usenet Gecikmesi: {usenetDelay}", + "BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki zaman aralığı", + "BlocklistRelease": "Kara Liste Sürümü", + "CustomFormats": "Özel Formatlar", + "DeleteDownloadClientMessageText": "'{name}' indirme istemcisini silmek istediğinizden emin misiniz?", + "DeleteIndexerMessageText": "'{name}' dizinleyicisini silmek istediğinizden emin misiniz?", + "OneMinute": "1 dakika", + "TaskUserAgentTooltip": "API'yi çağıran uygulama tarafından sağlanan Kullanıcı Aracısı", + "SkipRedownload": "Yeniden İndirmeyi Atla", + "RestartLater": "Daha sonra yeniden başlayacağım", + "DeleteTagMessageText": "'{label}' etiketini silmek istediğinizden emin misiniz?", + "DeleteBackupMessageText": "'{name}' yedeğini silmek istediğinizden emin misiniz?", + "DeleteRemotePathMappingMessageText": "Bu uzak yol eşlemesini silmek istediğinizden emin misiniz?", + "QualitiesLoadError": "Nitelikler yüklenemiyor", + "SelectAll": "Hepsini Seç", + "SslCertPasswordHelpText": "Pfx dosyasının şifresi", + "SslCertPathHelpText": "Pfx dosyasının yolu", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Sadece oku", + "NotificationsSlackSettingsChannelHelpText": "Gelen webhook için varsayılan kanalı geçersiz kılar (#diğer kanal)", + "NotificationsTelegramSettingsSendSilently": "Sessizce Gönder", + "UseSsl": "SSL kullan", + "NotificationsTelegramSettingsTopicIdHelpText": "Bu konuya bildirim göndermek için bir Konu Kimliği belirtin. Genel konuyu kullanmak için boş bırakın (Yalnızca Süper Gruplar)", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Twitter uygulamasından kullanıcı gizliliği", + "VideoDynamicRange": "Video Dinamik Aralığı", + "WouldYouLikeToRestoreBackup": "'{name}' yedeğini geri yüklemek ister misiniz?", + "NotificationsTwitterSettingsDirectMessage": "Direk mesaj", + "PasswordConfirmation": "Şifre onayı", + "OrganizeRenamingDisabled": "Yeniden adlandırma devre dışı bırakıldı, yeniden adlandırılacak bir şey yok", + "ResetQualityDefinitionsMessageText": "Kalite tanımlarını sıfırlamak istediğinizden emin misiniz?", + "SelectFolderModalTitle": "{modalTitle} - Klasör seç", + "SelectReleaseGroup": "Yayımlama Grubunu Seçin", + "RemoveFailedDownloads": "Başarısız İndirmeleri Kaldır", + "Scheduled": "Planlı", + "Underscore": "Vurgula", + "SetIndexerFlags": "Dizin Oluşturucu Bayraklarını Ayarla", + "SetReleaseGroup": "Yayımlama Grubunu Ayarla", + "SetIndexerFlagsModalTitle": "{modalTitle} - Dizin Oluşturucu Bayraklarını Ayarla", + "SslCertPassword": "SSL Sertifika Şifresi", + "Rating": "Puan", + "GrabRelease": "Yayın Yakalama", + "NotificationsNtfyValidationAuthorizationRequired": "Yetkilendirme gerekli", + "NotificationsPushBulletSettingSenderId": "Gönderen ID", + "NotificationsPushBulletSettingsChannelTags": "Kanal Etiketleri", + "NotificationsPushBulletSettingsAccessToken": "Erişim Jetonu", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Bildirimlerin gönderileceği Kanal Etiketleri Listesi", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Cihaz kimliklerinin listesi (tüm cihazlara göndermek için boş bırakın)", + "NotificationsPushcutSettingsNotificationName": "Bildirim Adı", + "NotificationsPushcutSettingsTimeSensitive": "Zamana duyarlı", + "NotificationsPushoverSettingsDevicesHelpText": "Cihaz adlarının listesi (tüm cihazlara göndermek için boş bırakın)", + "NotificationsPushoverSettingsRetryHelpText": "Acil durum uyarılarını yeniden deneme aralığı, minimum 30 saniye", + "NotificationsPushoverSettingsRetry": "Yeniden dene", + "NotificationsPushoverSettingsSoundHelpText": "Bildirim sesi, varsayılanı kullanmak için boş bırakın", + "NotificationsSettingsUpdateMapPathsFrom": "Harita Yolları", + "NotificationsSignalSettingsGroupIdPhoneNumber": "Grup Kimliği / Telefon Numarası", + "NotificationsSettingsWebhookUrl": "Webhook URL'si", + "NotificationsSignalSettingsSenderNumber": "Gönderen Numarası", + "NotificationsSignalSettingsSenderNumberHelpText": "Signal-api'deki gönderen kaydının telefon numarası", + "NotificationsSignalValidationSslRequired": "SSL gerekli görünüyor", + "NotificationsSimplepushSettingsEvent": "Etkinlik", + "NotificationsSlackSettingsIcon": "Simge", + "NotificationsSlackSettingsIconHelpText": "Slack'e gönderilen mesajlar için kullanılan simgeyi değiştirin (Emoji veya URL)", + "NotificationsSynologyValidationInvalidOs": "Bir Synology olmalı", + "NotificationsTelegramSettingsBotToken": "Bot Jetonu", + "NotificationsTelegramSettingsChatIdHelpText": "Mesaj almak için botla bir konuşma başlatmanız veya onu grubunuza eklemeniz gerekir", + "NotificationsTelegramSettingsTopicId": "Konu Kimliği", + "NotificationsTraktSettingsAuthUser": "Yetkilendirilmiş Kullanıcı", + "NotificationsTwitterSettingsConsumerKey": "Kullanıcı anahtarı", + "NotificationsTwitterSettingsConnectToTwitter": "Twitter / X'e bağlanın", + "NotificationsValidationInvalidAccessToken": "Erişim Jetonu geçersiz", + "NotificationsValidationInvalidAuthenticationToken": "Kimlik Doğrulama Jetonu geçersiz", + "OverrideAndAddToDownloadQueue": "Geçersiz kıl ve indirme kuyruğuna ekle", + "Parse": "Ayrıştır", + "PackageVersion": "Paket Versiyonu", + "PostImportCategory": "İçe Aktarma Sonrası Kategorisi", + "Queued": "Kuyruğa alındı", + "ReleaseHash": "Yayın Karması", + "RemoveTagsAutomatically": "Otomatik Etiketlemeyi Kaldır", + "RestartRequiredWindowsService": "{appName} hizmetini hangi kullanıcının çalıştırdığına bağlı olarak, hizmetin otomatik olarak başlamasından önce {appName} hizmetini yönetici olarak bir kez yeniden başlatmanız gerekebilir.", + "SelectLanguageModalTitle": "{modalTitle} - Dil Seç", + "SkipRedownloadHelpText": "{appName} uygulamasının bu öğe için alternatif bir yayın indirmeye çalışmasını engeller", + "SslCertPath": "SSL Sertifika Yolu", + "StopSelecting": "Düzenlemeden Çık", + "TableOptionsButton": "Tablo Seçenekleri Butonu", + "TheLogLevelDefault": "Günlük düzeyi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/settings/general) bölümünden değiştirilebilir", + "NotificationsTwitterSettingsAccessToken": "Erişim Jetonu", + "AutoRedownloadFailedHelpText": "Otomatik olarak farklı bir Yayın arayın ve indirmeye çalışın", + "Queue": "Sırada", + "RemoveFromQueue": "Kuyruktan kaldır", + "TorrentDelayTime": "Torrent Gecikmesi: {torrentDelay}", + "Yes": "Evet", + "ChmodFolderHelpText": "Sekizli, medya klasörlerine ve dosyalara içe aktarma / yeniden adlandırma sırasında uygulanır (yürütme bitleri olmadan)", + "DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?", + "Or": "veya", + "OverrideGrabModalTitle": "Geçersiz Kıl ve Yakala - {title}", + "PreferProtocol": "{preferredProtocol}'u tercih edin", + "PreferredProtocol": "Tercih Edilen Protokol", + "PublishedDate": "Yayınlanma Tarihi", + "RemoveQueueItem": "Kaldır - {sourceTitle}", + "RemoveQueueItemConfirmation": "'{sourceTitle}' dosyasını kuyruktan kaldırmak istediğinizden emin misiniz?", + "RemoveSelectedBlocklistMessageText": "Seçilen öğeleri engellenenler listesinden kaldırmak istediğinizden emin misiniz?", + "RemoveSelectedItemQueueMessageText": "1 öğeyi kuyruktan kaldırmak istediğinizden emin misiniz?", + "RemoveSelectedItem": "Seçilen Öğeyi Kaldır", + "RemovedFromTaskQueue": "Görev kuyruğundan kaldırıldı", + "ResetAPIKeyMessageText": "API Anahtarınızı sıfırlamak istediğinizden emin misiniz?", + "ResetQualityDefinitions": "Kalite Tanımlarını Sıfırla", + "SelectLanguages": "Dil Seçin", + "StartupDirectory": "Başlangıç Dizini", + "TablePageSizeMaximum": "Sayfa boyutu {maximumValue} değerini aşmamalıdır", + "TablePageSizeHelpText": "Her sayfada gösterilecek öğe sayısı", + "TablePageSizeMinimum": "Sayfa boyutu en az {minimumValue} olmalıdır", + "TorrentBlackholeSaveMagnetFiles": "Magnet Dosyalarını Kaydet", + "TorrentBlackholeTorrentFolder": "Torrent Klasörü", + "TypeOfList": "{typeOfList} Liste", + "UnknownEventTooltip": "Bilinmeyen etkinlik", + "UpdateMechanismHelpText": "{appName}'ın yerleşik güncelleyicisini veya bir komut dosyasını kullanın", + "UsenetBlackhole": "Usenet Blackhole", + "CalendarOptions": "Takvim Seçenekleri", + "CustomFormatHelpText": "{appName}, özel formatlarla eşleşen puanların toplamını kullanarak her yayını puanlar. Yeni bir yayının, puanı aynı veya daha iyi kalitede iyileştirecekse, {appName} onu alacaktır.", + "CustomFormatsSettingsSummary": "Özel Formatlar ve Ayarlar", + "CustomFormatsSettings": "Özel Format Ayarları", + "NotificationsPushoverSettingsExpireHelpText": "Acil durum uyarılarını yeniden denemek için maksimum süre, maksimum 86400 saniye\"", + "NotificationsPushoverSettingsExpire": "Süresi dolmuş", + "NotificationsPushoverSettingsUserKey": "Kullanıcı Anahtarı", + "NotificationsSlackSettingsChannel": "Kanal", + "NotificationsValidationInvalidHttpCredentials": "HTTP Kimlik Doğrulama kimlik bilgileri geçersiz: {exceptionMessage}", + "OriginalLanguage": "Orjinal Dil", + "ReleaseProfiles": "Yayımlama Profilleri", + "SupportedAutoTaggingProperties": "{appName}, otomatik etiketleme kuralları için takip özelliklerini destekler", + "OptionalName": "İsteğe bağlı isim", + "OrganizeRelativePaths": "Tüm yollar şuna göredir: `{path}`", + "OverrideGrabNoQuality": "Kalite seçilmelidir", + "ParseModalErrorParsing": "Ayrıştırmada hata oluştu. Lütfen tekrar deneyin.", + "PackageVersionInfo": "{packageAuthor} tarafından {packageVersion}", + "RemoveCompleted": "Tamamlananları Kaldır", + "RemoveFailed": "Başarısızları Kaldır", + "ResetDefinitions": "Tanımları Sıfırla", + "RestartRequiredToApplyChanges": "{appName}, değişikliklerin uygulanabilmesi için yeniden başlatmayı gerektiriyor. Şimdi yeniden başlatmak istiyor musunuz?", + "RetryingDownloadOn": "{date} tarihinde, {time} itibarıyla indirme işlemi yeniden deneniyor", + "RssSyncIntervalHelpText": "Dakika cinsinden periyot. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın yakalamayı durduracaktır)", + "TorrentBlackhole": "Blackhole Torrent", + "PrioritySettings": "Öncelik: {priority}", + "SubtitleLanguages": "Altyazı Dilleri", + "OrganizeNothingToRename": "Başarılı! İşim bitti, yeniden adlandırılacak dosya yok.", + "MinimumCustomFormatScore": "Minimum Özel Format Puanı", + "MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin alınmadan önceki dakika cinsinden minimum yaşı. Yeni yayınların usenet sağlayıcınıza yayılması için zaman tanımak için bunu kullanın.", + "QueueLoadError": "Kuyruk yüklenemedi", + "SelectDropdown": "Seçimler...", + "NotificationsSettingsUpdateMapPathsTo": "Harita Yolları", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Plex.tv ile kimlik doğrulaması yapın", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Bir kütüphane dosyasını güncellemek için localhost'ta synoindex'i çağırın", + "NotificationsSlackSettingsWebhookUrlHelpText": "Slack kanal webhook URL'si", + "Organize": "Düzenle", + "OnApplicationUpdate": "Uygulama Güncellemesinde", + "PendingDownloadClientUnavailable": "Beklemede - İndirme istemcisi kullanılamıyor", + "PreviewRename": "Yeniden Adlandır ve Önizle", + "LocalPath": "Yerel Yol", + "RemoveMultipleFromDownloadClientHint": "İndirilenleri ve dosyaları indirme istemcisinden kaldırır", + "RemoveTagsAutomaticallyHelpText": "Koşullar karşılanmazsa otomatik etiketlemeyi kaldırın", + "RemoveSelectedItemsQueueMessageText": "{selectedCount} öğeyi kuyruktan kaldırmak istediğinizden emin misiniz?", + "Umask": "Umask", + "CustomFormatsLoadError": "Özel Formatlar yüklenemiyor", + "DownloadWarning": "Uyarıyı indir: {warningMessage}", + "Formats": "Formatlar", + "NotificationsValidationUnableToConnectToService": "{serviceName} hizmetine bağlanılamıyor", + "OrganizeLoadError": "Önizlemeler yüklenirken hata oluştu", + "ParseModalHelpTextDetails": "{appName}, başlığı ayrıştırmaya ve size konuyla ilgili ayrıntıları göstermeye çalışacak", + "NegateHelpText": "İşaretlenirse, bu {implementationName} koşulu eşleşirse özel format uygulanmayacaktır.", + "ParseModalHelpText": "Yukarıdaki girişe bir yayın başlığı girin", + "Period": "Periyot", + "NotificationsPushoverSettingsSound": "Ses", + "NotificationsSettingsUseSslHelpText": "{serviceName} hizmetine HTTP yerine HTTPS üzerinden bağlanın", + "NotificationsTelegramSettingsIncludeAppName": "{appName}'i Başlığa dahil et", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Farklı uygulamalardan gelen bildirimleri ayırt etmek için isteğe bağlı olarak mesaj başlığının önüne {appName} ekleyin", + "NotificationsValidationInvalidApiKey": "API Anahtarı geçersiz", + "NotificationsValidationInvalidUsernamePassword": "Geçersiz kullanıcı adı veya şifre", + "NotificationsValidationUnableToConnect": "Bağlantı kurulamıyor: {exceptionMessage}", + "NotificationsValidationUnableToConnectToApi": "{service} API'sine bağlanılamıyor. Sunucu bağlantısı başarısız oldu: ({responseCode}) {exceptionMessage}", + "OnHealthRestored": "Sağlığın İyileştirilmesi Hakkında", + "OverrideGrabNoLanguage": "En az bir dil seçilmelidir", + "ParseModalUnableToParse": "Sağlanan başlık ayrıştırılamadı, lütfen tekrar deneyin.", + "PreviouslyInstalled": "Önceden Yüklenmiş", + "QualityCutoffNotMet": "Kalite sınırı karşılanmadı", + "QueueIsEmpty": "Kuyruk boş", + "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizin oluşturucunun ayarlanması, bu profilin yalnızca söz konusu dizin oluşturucunun sürümlerine uygulanmasına neden olur.", + "ResetDefinitionTitlesHelpText": "Değerlerin yanı sıra tanım başlıklarını da sıfırlayın", + "SecretToken": "Gizlilik Jetonu", + "SetReleaseGroupModalTitle": "{modalTitle} - Yayımlama Grubunu Ayarla", + "SslPort": "SSL Bağlantı Noktası", + "True": "Aktif", + "WantMoreControlAddACustomFormat": "Hangi indirmelerin tercih edileceği konusunda daha fazla kontrole mi ihtiyacınız var? Bir [Özel Format](/settings/customformats) ekleyin", + "XmlRpcPath": "XML RPC Yolu", + "RootFolderPath": "Kök Klasör Yolu", + "CustomFormat": "Özel Format", + "NotificationsPushBulletSettingSenderIdHelpText": "Bildirimlerin gönderileceği cihaz kimliği, pushbullet.com'da cihazın URL'sinde Device_iden kullanın (kendinizden göndermek için boş bırakın)", + "OrganizeModalHeader": "Düzenle & Yeniden Adlandır", + "Rejections": "Reddedilenler", + "Uptime": "Çalışma süresi", + "RemotePath": "Uzak Yol", + "File": "Dosya", + "ReleaseProfileIndexerHelpText": "Profilin hangi dizin oluşturucuya uygulanacağını belirtin", + "TablePageSize": "Sayfa Boyutu", + "NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil", + "NotificationsTwitterSettingsAccessTokenSecret": "Erişim Jetonu Gizliliği", + "NotificationsSimplepushSettingsKey": "Anahtar", + "NotificationsPushBulletSettingsDeviceIds": "Cihaz Kimlikleri", + "NotificationsSendGridSettingsApiKeyHelpText": "SendGrid tarafından oluşturulan API Anahtar", + "NotificationsSettingsUpdateLibrary": "Kitaplığı Güncelle", + "NotificationsSettingsWebhookMethod": "Yöntem", + "SizeLimit": "Boyut Limiti", + "Sort": "Sınıflandır", + "ManualImport": "Manuel İçe Aktar", + "ReleaseProfilesLoadError": "Yayımlama Profilleri yüklenemiyor", + "RemoveQueueItemRemovalMethod": "Kaldırma Yöntemi", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirme işlemini ve dosyaları indirme istemcisinden kaldıracaktır.", + "RemoveSelectedItems": "Seçili öğeleri kaldır", + "SelectIndexerFlags": "Dizin Oluşturucu Bayraklarını Seçin", + "Started": "Başlatıldı", + "Size": "Boyut", + "SupportedCustomConditions": "{appName}, aşağıdaki yayın özelliklerine göre özel koşulları destekler.", + "TestParsing": "Ayrıştırma Testi", + "ThemeHelpText": "Uygulama Kullanıcı Arayüzü Temasını Değiştirin, 'Otomatik' Teması, Açık veya Koyu modu ayarlamak için İşletim Sistemi Temanızı kullanacaktır. Theme.Park'tan ilham alındı", + "TorrentBlackholeSaveMagnetFilesExtension": "Magnet Dosya Uzantısını Kaydet", + "Theme": "Tema", + "Unknown": "Bilinmeyen", + "ReleaseGroup": "Yayımlayan Grup", + "ReleaseGroupFootNote": "İsteğe bağlı olarak, üç nokta (`...`) dahil olmak üzere maksimum bayt sayısına kadar kesmeyi kontrol edin. Sondan (ör. `{Release Group:30}`) veya baştan (ör. `{Release Group:-30}`) kesmenin her ikisi de desteklenir.`).", + "RemoveCompletedDownloads": "Tamamlanan İndirmeleri Kaldır", + "TagDetails": "Etiket Ayrıntıları - {label}", + "RemoveDownloadsAlert": "Kaldırma ayarları, yukarıdaki tabloda bireysel İndirme İstemcisi ayarlarına taşınmıştır.", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet bağlantıları için kullanılacak uzantı, varsayılan olarak '.magnet'tir", + "TorrentBlackholeSaveMagnetFilesHelpText": ".torrent dosyası yoksa magnet bağlantısını kaydedin (yalnızca indirme istemcisi bir dosyaya kaydedilen magnetleri destekliyorsa kullanışlıdır)", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirilenleri ve dosyaları indirme istemcisinden kaldıracaktır.", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Bu, dosyaları taşımak yerine {appName}'e Kopyalama veya Sabit Bağlantı kurma talimatını verecektir (ayarlara/sistem yapılandırmasına bağlı olarak)", + "Interval": "Periyot", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Mesajı sessizce gönderir. Kullanıcılar sessiz bir bildirim alacak", + "NotificationsTraktSettingsAccessToken": "Erişim Jetonu", + "NotificationsTraktSettingsExpires": "Süresi doluyor", + "NotificationsTraktSettingsRefreshToken": "Jetonu Yenile", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Twitter uygulamasından kullanıcı anahtarı", + "NotificationsTelegramSettingsChatId": "Sohbet Kimliği", + "NotificationsTwitterSettingsConsumerSecret": "Kullanıcı Gizliliği", + "NotificationsValidationInvalidApiKeyExceptionMessage": "API Anahtarı geçersiz: {exceptionMessage}", + "UnknownDownloadState": "Bilinmeyen indirme durumu: {state}", + "UpdateFiltered": "Filtrelenenleri Güncelle", + "UsenetBlackholeNzbFolder": "Nzb Klasörü", + "UsenetDelayHelpText": "Usenet'ten bir yayın almadan önce beklemek için dakika cinsinden gecikme", + "UpdaterLogFiles": "Güncelleme Günlük Dosyaları", + "Usenet": "Usenet", + "Filters": "Filtreler", + "ImportListsSettingsSummary": "Başka bir {appName} örneğinden veya Trakt listelerinden içe aktarın ve liste hariç tutma işlemlerini yönetin", + "NotificationsPushcutSettingsApiKeyHelpText": "API Anahtarları, Pushcut uygulamasının Hesap görünümünden yönetilebilir", + "NotificationsPushcutSettingsNotificationNameHelpText": "Pushcut uygulamasının Bildirimler sekmesindeki bildirim adı", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Bildirimi \"Zamana Duyarlı\" olarak işaretlemek için etkinleştirin", + "NotificationsTwitterSettingsMention": "Bahset", + "NotificationsTwitterSettingsMentionHelpText": "Gönderilen tweetlerde bu kullanıcıdan bahsedin", + "NotificationsPushoverSettingsDevices": "Cihazlar", + "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} yolu, {serviceName} kitaplık yolu konumunu {appName}'dan farklı gördüğünde seri yollarını değiştirmek için kullanılır ('Kütüphaneyi Güncelle' gerektirir)", + "NotificationsSettingsWebhookMethodHelpText": "Web hizmetine göndermek için hangi HTTP yönteminin kullanılacağı", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Test mesajı gönderilemiyor. API'den yanıt: {error}", + "NzbgetHistoryItemMessage": "PAR Durumu: {parStatus} - Paketten Çıkarma Durumu: {unpackStatus} - Taşıma Durumu: {moveStatus} - Komut Dosyası Durumu: {scriptStatus} - Silme Durumu: {deleteStatus} - İşaretleme Durumu: {markStatus}", + "NotificationsSignalSettingsPasswordHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan şifre", + "NotificationsSimplepushSettingsEventHelpText": "Anlık bildirimlerin davranışını özelleştirme", + "NotificationsSlackSettingsUsernameHelpText": "Slack'e gönderilecek kullanıcı adı", + "QueueFilterHasNoItems": "Seçilen kuyruk filtresinde hiç öğe yok", + "ReleaseGroups": "Yayımlama Grupları", + "IncludeCustomFormatWhenRenamingHelpText": "{Custom Formats} yeniden adlandırma formatına dahil et", + "Logging": "Loglama", + "MinutesSixty": "60 Dakika: {sixty}", + "SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin", + "Repack": "Yeniden paketle" } From 23c741fd001582fa363c2723eff9facd3091618b Mon Sep 17 00:00:00 2001 From: Mika <1054229+mikabytes@users.noreply.github.com> Date: Sun, 5 May 2024 03:53:47 +0200 Subject: [PATCH 269/762] Add file-count for Transmission RPC --- .../Clients/Transmission/TransmissionProxy.cs | 3 ++- .../Clients/Transmission/TransmissionTorrent.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 2c53f103b..45190fb16 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -178,7 +178,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission "seedRatioMode", "seedIdleLimit", "seedIdleMode", - "fileCount" + "fileCount", + "file-count" }; var arguments = new Dictionary<string, object>(); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index e72fa55a7..3552c36e9 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Core.Download.Clients.Transmission +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Transmission { public class TransmissionTorrent { @@ -20,6 +22,12 @@ public int SeedRatioMode { get; set; } public long SeedIdleLimit { get; set; } public int SeedIdleMode { get; set; } - public int FileCount { get; set; } + public int FileCount => TransmissionFileCount ?? VuzeFileCount ?? 0; + + [JsonProperty(PropertyName = "file-count")] + public int? TransmissionFileCount { get; set; } + + [JsonProperty(PropertyName = "fileCount")] + public int? VuzeFileCount { get; set; } } } From 92eab4b2e26741b7f20b6b7177418402cb15a3aa Mon Sep 17 00:00:00 2001 From: Jared <github@sillock.io> Date: Sun, 5 May 2024 01:54:42 +0000 Subject: [PATCH 270/762] New: Config file setting to disable log database Closes #6743 --- .../ServiceFactoryFixture.cs | 1 + src/NzbDrone.Common/Options/LogOptions.cs | 1 + .../Configuration/ConfigFileProvider.cs | 3 ++- .../Extensions/CompositionExtensions.cs | 12 ++++++++++++ .../Update/History/UpdateHistoryService.cs | 7 +++++-- .../Update/RecentUpdateProvider.cs | 2 +- src/NzbDrone.Host/Bootstrap.cs | 19 +++++++++++++++++++ src/NzbDrone.Host/Startup.cs | 7 +++++-- src/Sonarr.Api.V3/Logs/LogController.cs | 10 +++++++++- src/Sonarr.Api.V3/Update/UpdateController.cs | 13 +++++++++++-- src/Sonarr.Http/PagingResource.cs | 2 +- 11 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 6749006e5..cc2524248 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Common.Test .AddNzbDroneLogger() .AutoAddServices(Bootstrap.ASSEMBLIES) .AddDummyDatabase() + .AddDummyLogDatabase() .AddStartupContext(new StartupContext("first", "second")); container.RegisterInstance(new Mock<IHostLifetime>().Object); diff --git a/src/NzbDrone.Common/Options/LogOptions.cs b/src/NzbDrone.Common/Options/LogOptions.cs index c36533cb4..1529bb1d0 100644 --- a/src/NzbDrone.Common/Options/LogOptions.cs +++ b/src/NzbDrone.Common/Options/LogOptions.cs @@ -11,4 +11,5 @@ public class LogOptions public string SyslogServer { get; set; } public int? SyslogPort { get; set; } public string SyslogLevel { get; set; } + public bool? DbEnabled { get; set; } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index e1168c2fb..4dc2e1fc2 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -54,6 +54,7 @@ namespace NzbDrone.Core.Configuration string SyslogServer { get; } int SyslogPort { get; } string SyslogLevel { get; } + bool LogDbEnabled { get; } string Theme { get; } string PostgresHost { get; } int PostgresPort { get; } @@ -230,7 +231,7 @@ namespace NzbDrone.Core.Configuration public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "sonarr-main", persist: false); public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "sonarr-log", persist: false); public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false); - + public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false); public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false); public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false); public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false); diff --git a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs index c5e31f92c..67e251805 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs @@ -8,6 +8,12 @@ namespace NzbDrone.Core.Datastore.Extensions public static IContainer AddDatabase(this IContainer container) { container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton); + + return container; + } + + public static IContainer AddLogDatabase(this IContainer container) + { container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton); return container; @@ -16,6 +22,12 @@ namespace NzbDrone.Core.Datastore.Extensions public static IContainer AddDummyDatabase(this IContainer container) { container.RegisterInstance<IMainDatabase>(new MainDatabase(null)); + + return container; + } + + public static IContainer AddDummyLogDatabase(this IContainer container) + { container.RegisterInstance<ILogDatabase>(new LogDatabase(null)); return container; diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs index 09cf70602..7be7349e1 100644 --- a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NLog; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Update.History.Events; @@ -18,13 +19,15 @@ namespace NzbDrone.Core.Update.History { private readonly IUpdateHistoryRepository _repository; private readonly IEventAggregator _eventAggregator; + private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; private Version _prevVersion; - public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, Logger logger) + public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, IConfigFileProvider configFileProvider, Logger logger) { _repository = repository; _eventAggregator = eventAggregator; + _configFileProvider = configFileProvider; _logger = logger; } @@ -58,7 +61,7 @@ namespace NzbDrone.Core.Update.History public void Handle(ApplicationStartedEvent message) { - if (BuildInfo.Version.Major == 10) + if (BuildInfo.Version.Major == 10 || !_configFileProvider.LogDbEnabled) { // Don't save dev versions, they change constantly return; diff --git a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs index a3264300d..08cc39865 100644 --- a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs +++ b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Update { var branch = _configFileProvider.Branch; var version = BuildInfo.Version; - var prevVersion = _updateHistoryService.PreviouslyInstalled(); + var prevVersion = _configFileProvider.LogDbEnabled ? _updateHistoryService.PreviouslyInstalled() : null; return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion); } } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 2073eb622..7462a47d0 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -95,6 +95,15 @@ namespace NzbDrone.Host .AddStartupContext(startupContext) .Resolve<UtilityModeRouter>() .Route(appMode); + + if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true)) + { + c.AddLogDatabase(); + } + else + { + c.AddDummyLogDatabase(); + } }) .ConfigureServices(services => { @@ -131,6 +140,7 @@ namespace NzbDrone.Host var enableSsl = config.GetValue<bool?>($"Sonarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); var sslCertPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath)); var sslCertPassword = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword)); + var logDbEnabled = config.GetValue<bool?>($"Sonarr:Log:{nameof(LogOptions.DbEnabled)}") ?? config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true); var urls = new List<string> { BuildUrl("http", bindAddress, port) }; @@ -153,6 +163,15 @@ namespace NzbDrone.Host .AddDatabase() .AddStartupContext(context); + if (logDbEnabled) + { + c.AddLogDatabase(); + } + else + { + c.AddDummyLogDatabase(); + } + SchemaBuilder.Initialize(c); }) .ConfigureServices(services => diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index eb18394fc..ca97ee60c 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -220,9 +220,12 @@ namespace NzbDrone.Host // instantiate the databases to initialize/migrate them _ = mainDatabaseFactory.Value; - _ = logDatabaseFactory.Value; - dbTarget.Register(); + if (configFileProvider.LogDbEnabled) + { + _ = logDatabaseFactory.Value; + dbTarget.Register(); + } if (OsInfo.IsNotWindows) { diff --git a/src/Sonarr.Api.V3/Logs/LogController.cs b/src/Sonarr.Api.V3/Logs/LogController.cs index e838bc7e6..ba8021a42 100644 --- a/src/Sonarr.Api.V3/Logs/LogController.cs +++ b/src/Sonarr.Api.V3/Logs/LogController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Instrumentation; using Sonarr.Http; using Sonarr.Http.Extensions; @@ -10,16 +11,23 @@ namespace Sonarr.Api.V3.Logs public class LogController : Controller { private readonly ILogService _logService; + private readonly IConfigFileProvider _configFileProvider; - public LogController(ILogService logService) + public LogController(ILogService logService, IConfigFileProvider configFileProvider) { _logService = logService; + _configFileProvider = configFileProvider; } [HttpGet] [Produces("application/json")] public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string level) { + if (!_configFileProvider.LogDbEnabled) + { + return new PagingResource<LogResource>(); + } + var pagingResource = new PagingResource<LogResource>(paging); var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(); diff --git a/src/Sonarr.Api.V3/Update/UpdateController.cs b/src/Sonarr.Api.V3/Update/UpdateController.cs index a6e22f33e..5ce4e1bb1 100644 --- a/src/Sonarr.Api.V3/Update/UpdateController.cs +++ b/src/Sonarr.Api.V3/Update/UpdateController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Update.History; using Sonarr.Http; @@ -13,11 +14,13 @@ namespace Sonarr.Api.V3.Update { private readonly IRecentUpdateProvider _recentUpdateProvider; private readonly IUpdateHistoryService _updateHistoryService; + private readonly IConfigFileProvider _configFileProvider; - public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) + public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService, IConfigFileProvider configFileProvider) { _recentUpdateProvider = recentUpdateProvider; _updateHistoryService = updateHistoryService; + _configFileProvider = configFileProvider; } [HttpGet] @@ -45,7 +48,13 @@ namespace Sonarr.Api.V3.Update installed.Installed = true; } - var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate) + if (!_configFileProvider.LogDbEnabled) + { + return resources; + } + + var updateHistory = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate); + var installDates = updateHistory .DistinctBy(v => v.Version) .ToDictionary(v => v.Version); diff --git a/src/Sonarr.Http/PagingResource.cs b/src/Sonarr.Http/PagingResource.cs index 6559d80ab..64123e66a 100644 --- a/src/Sonarr.Http/PagingResource.cs +++ b/src/Sonarr.Http/PagingResource.cs @@ -21,7 +21,7 @@ namespace Sonarr.Http public string SortKey { get; set; } public SortDirection SortDirection { get; set; } public int TotalRecords { get; set; } - public List<TResource> Records { get; set; } + public List<TResource> Records { get; set; } = new (); public PagingResource() { From 3fbe4361386e9fb8dafdf82ad9f00f02bec746cc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Apr 2024 09:25:42 -0700 Subject: [PATCH 271/762] Forward X-Forwarded-Host header Closes #6764 --- src/NzbDrone.Host/Startup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index ca97ee60c..f8e7f1c26 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Host services.Configure<ForwardedHeadersOptions>(options => { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); }); From 7166a6c019fb1ad531863c22763029f9ea54f114 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 28 Apr 2024 23:14:10 +0300 Subject: [PATCH 272/762] Parameter binding for API requests --- src/Sonarr.Api.V3/AutoTagging/AutoTaggingController.cs | 4 ++-- src/Sonarr.Api.V3/Commands/CommandController.cs | 2 +- src/Sonarr.Api.V3/Config/ConfigController.cs | 2 +- src/Sonarr.Api.V3/Config/HostConfigController.cs | 2 +- src/Sonarr.Api.V3/Config/NamingConfigController.cs | 2 +- src/Sonarr.Api.V3/Config/UiConfigController.cs | 2 +- src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs | 4 ++-- src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs | 4 ++-- src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs | 2 +- .../ImportLists/ImportListExclusionController.cs | 4 ++-- src/Sonarr.Api.V3/Indexers/ReleaseController.cs | 2 +- src/Sonarr.Api.V3/Indexers/ReleasePushController.cs | 2 +- src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs | 6 +++--- .../Profiles/Languages/LanguageProfileController.cs | 4 ++-- .../Profiles/Quality/QualityProfileController.cs | 4 ++-- .../Profiles/Release/ReleaseProfileController.cs | 4 ++-- src/Sonarr.Api.V3/ProviderControllerBase.cs | 2 +- src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs | 2 +- src/Sonarr.Api.V3/Queue/QueueActionController.cs | 2 +- .../RemotePathMappings/RemotePathMappingController.cs | 4 ++-- src/Sonarr.Api.V3/RootFolders/RootFolderController.cs | 2 +- src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs | 2 +- src/Sonarr.Api.V3/Series/SeriesController.cs | 4 ++-- src/Sonarr.Api.V3/System/Backup/BackupController.cs | 2 +- src/Sonarr.Api.V3/Tags/TagController.cs | 4 ++-- 25 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Sonarr.Api.V3/AutoTagging/AutoTaggingController.cs b/src/Sonarr.Api.V3/AutoTagging/AutoTaggingController.cs index a74c20c34..02cfbf9d5 100644 --- a/src/Sonarr.Api.V3/AutoTagging/AutoTaggingController.cs +++ b/src/Sonarr.Api.V3/AutoTagging/AutoTaggingController.cs @@ -51,7 +51,7 @@ namespace Sonarr.Api.V3.AutoTagging [RestPostById] [Consumes("application/json")] - public ActionResult<AutoTaggingResource> Create(AutoTaggingResource autoTagResource) + public ActionResult<AutoTaggingResource> Create([FromBody] AutoTaggingResource autoTagResource) { var model = autoTagResource.ToModel(_specifications); @@ -62,7 +62,7 @@ namespace Sonarr.Api.V3.AutoTagging [RestPutById] [Consumes("application/json")] - public ActionResult<AutoTaggingResource> Update(AutoTaggingResource resource) + public ActionResult<AutoTaggingResource> Update([FromBody] AutoTaggingResource resource) { var model = resource.ToModel(_specifications); diff --git a/src/Sonarr.Api.V3/Commands/CommandController.cs b/src/Sonarr.Api.V3/Commands/CommandController.cs index 1b17916de..980a6f875 100644 --- a/src/Sonarr.Api.V3/Commands/CommandController.cs +++ b/src/Sonarr.Api.V3/Commands/CommandController.cs @@ -51,7 +51,7 @@ namespace Sonarr.Api.V3.Commands [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<CommandResource> StartCommand(CommandResource commandResource) + public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource) { var commandType = _knownTypes.GetImplementations(typeof(Command)) diff --git a/src/Sonarr.Api.V3/Config/ConfigController.cs b/src/Sonarr.Api.V3/Config/ConfigController.cs index 9d170e720..0506496a9 100644 --- a/src/Sonarr.Api.V3/Config/ConfigController.cs +++ b/src/Sonarr.Api.V3/Config/ConfigController.cs @@ -34,7 +34,7 @@ namespace Sonarr.Api.V3.Config [RestPutById] [Consumes("application/json")] - public virtual ActionResult<TResource> SaveConfig(TResource resource) + public virtual ActionResult<TResource> SaveConfig([FromBody] TResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Sonarr.Api.V3/Config/HostConfigController.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs index 50ef5c900..38fab24e7 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigController.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -122,7 +122,7 @@ namespace Sonarr.Api.V3.Config } [RestPutById] - public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource) + public ActionResult<HostConfigResource> SaveHostConfig([FromBody] HostConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Sonarr.Api.V3/Config/NamingConfigController.cs b/src/Sonarr.Api.V3/Config/NamingConfigController.cs index f940519aa..a05894f45 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigController.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigController.cs @@ -53,7 +53,7 @@ namespace Sonarr.Api.V3.Config } [RestPutById] - public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource) + public ActionResult<NamingConfigResource> UpdateNamingConfig([FromBody] NamingConfigResource resource) { var nameSpec = resource.ToModel(); ValidateFormatResult(nameSpec); diff --git a/src/Sonarr.Api.V3/Config/UiConfigController.cs b/src/Sonarr.Api.V3/Config/UiConfigController.cs index 8aa83588a..c52479bda 100644 --- a/src/Sonarr.Api.V3/Config/UiConfigController.cs +++ b/src/Sonarr.Api.V3/Config/UiConfigController.cs @@ -32,7 +32,7 @@ namespace Sonarr.Api.V3.Config } [RestPutById] - public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource) + public override ActionResult<UiConfigResource> SaveConfig([FromBody] UiConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs index 51f46d100..8fe7a62c0 100644 --- a/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs +++ b/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs @@ -31,7 +31,7 @@ namespace Sonarr.Api.V3.CustomFilters [RestPostById] [Consumes("application/json")] - public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource) + public ActionResult<CustomFilterResource> AddCustomFilter([FromBody] CustomFilterResource resource) { var customFilter = _customFilterService.Add(resource.ToModel()); @@ -40,7 +40,7 @@ namespace Sonarr.Api.V3.CustomFilters [RestPutById] [Consumes("application/json")] - public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource) + public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFilterResource resource) { _customFilterService.Update(resource.ToModel()); return Accepted(resource.Id); diff --git a/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs b/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs index 0a5915452..4b560cd7e 100644 --- a/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs +++ b/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs @@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.CustomFormats [RestPostById] [Consumes("application/json")] - public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource) + public ActionResult<CustomFormatResource> Create([FromBody] CustomFormatResource customFormatResource) { var model = customFormatResource.ToModel(_specifications); @@ -60,7 +60,7 @@ namespace Sonarr.Api.V3.CustomFormats [RestPutById] [Consumes("application/json")] - public ActionResult<CustomFormatResource> Update(CustomFormatResource resource) + public ActionResult<CustomFormatResource> Update([FromBody] CustomFormatResource resource) { var model = resource.ToModel(_specifications); diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs index b1e9dd1fb..a9dc40f13 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs @@ -90,7 +90,7 @@ namespace Sonarr.Api.V3.EpisodeFiles [RestPutById] [Consumes("application/json")] - public ActionResult<EpisodeFileResource> SetQuality(EpisodeFileResource episodeFileResource) + public ActionResult<EpisodeFileResource> SetQuality([FromBody] EpisodeFileResource episodeFileResource) { var episodeFile = _mediaFileService.Get(episodeFileResource.Id); episodeFile.Quality = episodeFileResource.Quality; diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index d9cd55c03..58742ad79 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.ImportLists [RestPostById] [Consumes("application/json")] - public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource) + public ActionResult<ImportListExclusionResource> AddImportListExclusion([FromBody] ImportListExclusionResource resource) { var importListExclusion = _importListExclusionService.Add(resource.ToModel()); @@ -58,7 +58,7 @@ namespace Sonarr.Api.V3.ImportLists [RestPutById] [Consumes("application/json")] - public ActionResult<ImportListExclusionResource> UpdateImportListExclusion(ImportListExclusionResource resource) + public ActionResult<ImportListExclusionResource> UpdateImportListExclusion([FromBody] ImportListExclusionResource resource) { _importListExclusionService.Update(resource.ToModel()); return Accepted(resource.Id); diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs index e1d9c9f50..6de121914 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs @@ -68,7 +68,7 @@ namespace Sonarr.Api.V3.Indexers [HttpPost] [Consumes("application/json")] - public async Task<object> DownloadRelease(ReleaseResource release) + public async Task<object> DownloadRelease([FromBody] ReleaseResource release) { var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index 1b2f22417..96bcaf6c2 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.Indexers [HttpPost] [Consumes("application/json")] - public ActionResult<List<ReleaseResource>> Create(ReleaseResource release) + public ActionResult<List<ReleaseResource>> Create([FromBody] ReleaseResource release) { _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl); diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs index 03b357d62..b632bdf08 100644 --- a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs @@ -35,7 +35,7 @@ namespace Sonarr.Api.V3.Profiles.Delay [RestPostById] [Consumes("application/json")] - public ActionResult<DelayProfileResource> Create(DelayProfileResource resource) + public ActionResult<DelayProfileResource> Create([FromBody] DelayProfileResource resource) { var model = resource.ToModel(); model = _delayProfileService.Add(model); @@ -56,7 +56,7 @@ namespace Sonarr.Api.V3.Profiles.Delay [RestPutById] [Consumes("application/json")] - public ActionResult<DelayProfileResource> Update(DelayProfileResource resource) + public ActionResult<DelayProfileResource> Update([FromBody] DelayProfileResource resource) { var model = resource.ToModel(); _delayProfileService.Update(model); @@ -76,7 +76,7 @@ namespace Sonarr.Api.V3.Profiles.Delay } [HttpPut("reorder/{id}")] - public List<DelayProfileResource> Reorder([FromRoute] int id, int? after) + public List<DelayProfileResource> Reorder([FromRoute] int id, [FromQuery] int? after) { ValidateId(id); diff --git a/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileController.cs b/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileController.cs index 0b2751597..aba003004 100644 --- a/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileController.cs @@ -15,7 +15,7 @@ namespace Sonarr.Api.V3.Profiles.Languages [RestPostById] [Produces("application/json")] [Consumes("application/json")] - public ActionResult<LanguageProfileResource> Create(LanguageProfileResource resource) + public ActionResult<LanguageProfileResource> Create([FromBody] LanguageProfileResource resource) { return Accepted(resource); } @@ -28,7 +28,7 @@ namespace Sonarr.Api.V3.Profiles.Languages [RestPutById] [Produces("application/json")] [Consumes("application/json")] - public ActionResult<LanguageProfileResource> Update(LanguageProfileResource resource) + public ActionResult<LanguageProfileResource> Update([FromBody] LanguageProfileResource resource) { return Accepted(resource); } diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs index 87f39b5ad..46a1c6730 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -46,7 +46,7 @@ namespace Sonarr.Api.V3.Profiles.Quality [RestPostById] [Consumes("application/json")] - public ActionResult<QualityProfileResource> Create(QualityProfileResource resource) + public ActionResult<QualityProfileResource> Create([FromBody] QualityProfileResource resource) { var model = resource.ToModel(); model = _profileService.Add(model); @@ -61,7 +61,7 @@ namespace Sonarr.Api.V3.Profiles.Quality [RestPutById] [Consumes("application/json")] - public ActionResult<QualityProfileResource> Update(QualityProfileResource resource) + public ActionResult<QualityProfileResource> Update([FromBody] QualityProfileResource resource) { var model = resource.ToModel(); diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs index eb53716d3..3d33c3b25 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs @@ -36,7 +36,7 @@ namespace Sonarr.Api.V3.Profiles.Release } [RestPostById] - public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource) + public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource) { var model = resource.ToModel(); model = _profileService.Add(model); @@ -50,7 +50,7 @@ namespace Sonarr.Api.V3.Profiles.Release } [RestPutById] - public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource) + public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileResource resource) { var model = resource.ToModel(); diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index 2622b9b02..c80c41981 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -245,7 +245,7 @@ namespace Sonarr.Api.V3 [HttpPost("action/{name}")] [Consumes("application/json")] [Produces("application/json")] - public IActionResult RequestAction(string name, [FromBody] TProviderResource providerResource) + public IActionResult RequestAction([FromRoute] string name, [FromBody] TProviderResource providerResource) { var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; var providerDefinition = GetDefinition(providerResource, existingDefinition, false, false, false); diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs index 93fed5288..5910ee896 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs @@ -23,7 +23,7 @@ namespace Sonarr.Api.V3.Qualities } [RestPutById] - public ActionResult<QualityDefinitionResource> Update(QualityDefinitionResource resource) + public ActionResult<QualityDefinitionResource> Update([FromBody] QualityDefinitionResource resource) { var model = resource.ToModel(); _qualityDefinitionService.Update(model); diff --git a/src/Sonarr.Api.V3/Queue/QueueActionController.cs b/src/Sonarr.Api.V3/Queue/QueueActionController.cs index f4fe953e7..b54f968de 100644 --- a/src/Sonarr.Api.V3/Queue/QueueActionController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueActionController.cs @@ -21,7 +21,7 @@ namespace Sonarr.Api.V3.Queue } [HttpPost("grab/{id:int}")] - public async Task<object> Grab(int id) + public async Task<object> Grab([FromRoute] int id) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs index 251b51e3f..ffdb318d2 100644 --- a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs @@ -41,7 +41,7 @@ namespace Sonarr.Api.V3.RemotePathMappings [RestPostById] [Consumes("application/json")] - public ActionResult<RemotePathMappingResource> CreateMapping(RemotePathMappingResource resource) + public ActionResult<RemotePathMappingResource> CreateMapping([FromBody] RemotePathMappingResource resource) { var model = resource.ToModel(); @@ -62,7 +62,7 @@ namespace Sonarr.Api.V3.RemotePathMappings } [RestPutById] - public ActionResult<RemotePathMappingResource> UpdateMapping(RemotePathMappingResource resource) + public ActionResult<RemotePathMappingResource> UpdateMapping([FromBody] RemotePathMappingResource resource) { var mapping = resource.ToModel(); diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs index 87abcb067..de05cb4f1 100644 --- a/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs @@ -50,7 +50,7 @@ namespace Sonarr.Api.V3.RootFolders [RestPostById] [Consumes("application/json")] - public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource rootFolderResource) + public ActionResult<RootFolderResource> CreateRootFolder([FromBody] RootFolderResource rootFolderResource) { var model = rootFolderResource.ToModel(); diff --git a/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs b/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs index be45658b0..cf699ddff 100644 --- a/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs +++ b/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs @@ -19,7 +19,7 @@ namespace Sonarr.Api.V3.SeasonPass [HttpPost] [Consumes("application/json")] - public IActionResult UpdateAll(SeasonPassResource resource) + public IActionResult UpdateAll([FromBody] SeasonPassResource resource) { var seriesToUpdate = _seriesService.GetSeries(resource.Series.Select(s => s.Id)); diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index e1d50ec0f..57ff76bc4 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -156,7 +156,7 @@ namespace Sonarr.Api.V3.Series [RestPostById] [Consumes("application/json")] - public ActionResult<SeriesResource> AddSeries(SeriesResource seriesResource) + public ActionResult<SeriesResource> AddSeries([FromBody] SeriesResource seriesResource) { var series = _addSeriesService.AddSeries(seriesResource.ToModel()); @@ -165,7 +165,7 @@ namespace Sonarr.Api.V3.Series [RestPutById] [Consumes("application/json")] - public ActionResult<SeriesResource> UpdateSeries(SeriesResource seriesResource, bool moveFiles = false) + public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false) { var series = _seriesService.GetSeries(seriesResource.Id); diff --git a/src/Sonarr.Api.V3/System/Backup/BackupController.cs b/src/Sonarr.Api.V3/System/Backup/BackupController.cs index bb2554c03..23d499b4e 100644 --- a/src/Sonarr.Api.V3/System/Backup/BackupController.cs +++ b/src/Sonarr.Api.V3/System/Backup/BackupController.cs @@ -70,7 +70,7 @@ namespace Sonarr.Api.V3.System.Backup } [HttpPost("restore/{id:int}")] - public object Restore(int id) + public object Restore([FromRoute] int id) { var backup = GetBackup(id); diff --git a/src/Sonarr.Api.V3/Tags/TagController.cs b/src/Sonarr.Api.V3/Tags/TagController.cs index 04f76989d..46da11ce9 100644 --- a/src/Sonarr.Api.V3/Tags/TagController.cs +++ b/src/Sonarr.Api.V3/Tags/TagController.cs @@ -36,14 +36,14 @@ namespace Sonarr.Api.V3.Tags [RestPostById] [Consumes("application/json")] - public ActionResult<TagResource> Create(TagResource resource) + public ActionResult<TagResource> Create([FromBody] TagResource resource) { return Created(_tagService.Add(resource.ToModel()).Id); } [RestPutById] [Consumes("application/json")] - public ActionResult<TagResource> Update(TagResource resource) + public ActionResult<TagResource> Update([FromBody] TagResource resource) { _tagService.Update(resource.ToModel()); return Accepted(resource.Id); From 8be8c7f89cf4d40bee941c5ce768aa1a74ebe398 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 5 May 2024 03:55:44 +0200 Subject: [PATCH 273/762] Add missing translation key --- src/NzbDrone.Core/Localization/Core/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 78b8557e7..128651296 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -473,6 +473,7 @@ "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent cannot resolve magnet link with DHT disabled", "DownloadClientQbittorrentTorrentStateError": "qBittorrent is reporting an error", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent is downloading metadata", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent is reporting missing files", "DownloadClientQbittorrentTorrentStatePathError": "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?", "DownloadClientQbittorrentTorrentStateStalled": "The download is stalled with no connections", "DownloadClientQbittorrentTorrentStateUnknown": "Unknown download state: {state}", From e24ce40eb8b52ad428dddf96a4223520bac7a52e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 3 May 2024 15:45:52 -0700 Subject: [PATCH 274/762] Fixed: History with unknown episode Closes #6782 --- frontend/src/Series/History/SeriesHistoryRow.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/Series/History/SeriesHistoryRow.js b/frontend/src/Series/History/SeriesHistoryRow.js index 5b44f93aa..0213d9679 100644 --- a/frontend/src/Series/History/SeriesHistoryRow.js +++ b/frontend/src/Series/History/SeriesHistoryRow.js @@ -86,6 +86,10 @@ class SeriesHistoryRow extends Component { const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber; + if (!series || !episode) { + return null; + } + return ( <TableRow> <HistoryEventTypeCell From ba88185deae22e0cd5fad6e6791b52e4686d1080 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 3 May 2024 16:09:56 -0700 Subject: [PATCH 275/762] New: Treat batch releases with total episode count as full season release Closes #6757 --- src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 9910d612f..65b093c33 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)] [TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)] [TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)] + [TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)] + [TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)] public void should_parse_full_season_release(string postTitle, string title, int season) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index e111cfae6..74f2f4a34 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -183,7 +183,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Single or multi episode releases with multiple titles, then season and episode numbers after the last title. (Title1 / Title2 / ... / S1E1-2 of 6) - 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+)?\)?[ ._][\(\[]", + 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[ ._](?<episodecount>\d{1,2}))?\)?[ ._][\(\[]", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Multi-episode with title (S01E99-100, S01E05-06) @@ -1087,6 +1087,12 @@ namespace NzbDrone.Core.Parser result.FullSeason = true; } } + + if (episodeCaptures.Count == 2 && matchCollection[0].Groups["episodecount"].Success && episodeCaptures.Last().Value == matchCollection[0].Groups["episodecount"].Value) + { + result.EpisodeNumbers = Array.Empty<int>(); + result.FullSeason = true; + } } var seasons = new List<int>(); From 47ba002806fe2c2004a649aa193ae318343a84e4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 5 May 2024 04:56:52 +0300 Subject: [PATCH 276/762] Fixed: Indexer flags for torrent release pushes --- src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index bc4b5982d..ae8a6ed78 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -174,7 +174,8 @@ namespace Sonarr.Api.V3.Indexers MagnetUrl = resource.MagnetUrl, InfoHash = resource.InfoHash, Seeders = resource.Seeders, - Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null + Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null, + IndexerFlags = (IndexerFlags)resource.IndexerFlags }; } else From 73a4bdea5247ee87e6bbae95f5325e1f03c88a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Thomas?= <mickael9@gmail.com> Date: Thu, 9 May 2024 03:41:59 +0200 Subject: [PATCH 277/762] New: Support stoppedUP and stoppedDL states from qBittorrent --- .../QBittorrentTests/QBittorrentFixture.cs | 120 ++++++++++-------- .../Clients/QBittorrent/QBittorrent.cs | 8 +- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index d8029eeac..1d04855b5 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests VerifyWarning(item); } - [Test] - public void paused_item_should_have_required_properties() + [TestCase("pausedDL")] + [TestCase("stoppedDL")] + public void paused_item_should_have_required_properties(string state) { var torrent = new QBittorrentTorrent { @@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 0.7, Eta = 8640000, - State = "pausedDL", + State = state, Label = "", SavePath = "" }; @@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests } [TestCase("pausedUP")] + [TestCase("stoppedUP")] [TestCase("queuedUP")] [TestCase("uploading")] [TestCase("stalledUP")] @@ -418,8 +420,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12")); } - [Test] - public void api_261_should_use_content_path() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void api_261_should_use_content_path(string state) { var torrent = new QBittorrentTorrent { @@ -428,7 +431,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 0.7, Eta = 8640000, - State = "pausedUP", + State = state, Label = "", SavePath = @"C:\Torrents".AsOsAgnostic(), ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic() @@ -657,44 +660,48 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state) { GivenGlobalSeedLimits(-1); - GivenCompletedTorrent("pausedUP", ratio: 1.0f); + GivenCompletedTorrent(state, ratio: 1.0f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state) { GivenGlobalSeedLimits(1.0f); - GivenCompletedTorrent("pausedUP", ratio: 1.0f); + GivenCompletedTorrent(state, ratio: 1.0f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state) { GivenGlobalSeedLimits(2.0f); - GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f); + GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state) { GivenGlobalSeedLimits(0.2f); - GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f); + GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); @@ -712,33 +719,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, 20); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, 40); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, 20); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); @@ -756,66 +766,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); + GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds()); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds()); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state) { GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds()); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds()); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state) { GivenGlobalSeedLimits(2.0f, 20); - GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30); + GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state) { GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20); - GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); + GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds()); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); item.CanMoveFiles.Should().BeTrue(); } - [Test] - public void should_not_fetch_details_twice() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_not_fetch_details_twice(string state) { GivenGlobalSeedLimits(-1, 30); - GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20); + GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); @@ -827,8 +843,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests .Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once()); } - [Test] - public void should_get_category_from_the_category_if_set() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_get_category_from_the_category_if_set(string state) { const string category = "tv-sonarr"; GivenGlobalSeedLimits(1.0f); @@ -840,7 +857,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 1.0, Eta = 8640000, - State = "pausedUP", + State = state, Category = category, SavePath = "", Ratio = 1.0f @@ -852,8 +869,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.Category.Should().Be(category); } - [Test] - public void should_get_category_from_the_label_if_the_category_is_not_available() + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_get_category_from_the_label_if_the_category_is_not_available(string state) { const string category = "tv-sonarr"; GivenGlobalSeedLimits(1.0f); @@ -865,7 +883,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 1.0, Eta = 8640000, - State = "pausedUP", + State = state, Label = category, SavePath = "", Ratio = 1.0f diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index ec5bbc437..cea3a8c8b 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Avoid removing torrents that haven't reached the global max ratio. // Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api). - item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config); + item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config); switch (torrent.State) { @@ -248,7 +248,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateError"); break; - case "pausedDL": // torrent is paused and has NOT finished downloading + case "stoppedDL": // torrent is stopped and has NOT finished downloading + case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5) item.Status = DownloadItemStatus.Paused; break; @@ -259,7 +260,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent item.Status = DownloadItemStatus.Queued; break; - case "pausedUP": // torrent is paused and has finished downloading: + case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5) + case "stoppedUP": // torrent is stopped and has finished downloading case "uploading": // torrent is being seeded and data is being transferred case "stalledUP": // torrent is being seeded, but no connection were made case "queuedUP": // queuing is enabled and torrent is queued for upload From 128309068d167f0250ec286b0cdd1713c6a97054 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 1 May 2024 21:19:05 +0300 Subject: [PATCH 278/762] Fixed: Initialize databases after app folder migrations Co-authored-by: Mark McDowall <mark@mcdowall.ca> --- src/NzbDrone.Host/Bootstrap.cs | 3 --- src/NzbDrone.Host/Startup.cs | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 7462a47d0..165617ad8 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -23,7 +23,6 @@ using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Options; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; -using Sonarr.Http.ClientSchema; using LogLevel = Microsoft.Extensions.Logging.LogLevel; using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions; @@ -171,8 +170,6 @@ namespace NzbDrone.Host { c.AddDummyLogDatabase(); } - - SchemaBuilder.Initialize(c); }) .ConfigureServices(services => { diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index f8e7f1c26..b010e8e2c 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -26,6 +27,7 @@ using NzbDrone.SignalR; using Sonarr.Api.V3.System; using Sonarr.Http; using Sonarr.Http.Authentication; +using Sonarr.Http.ClientSchema; using Sonarr.Http.ErrorManagement; using Sonarr.Http.Frontend; using Sonarr.Http.Middleware; @@ -193,6 +195,7 @@ namespace NzbDrone.Host } public void Configure(IApplicationBuilder app, + IContainer container, IStartupContext startupContext, Lazy<IMainDatabase> mainDatabaseFactory, Lazy<ILogDatabase> logDatabaseFactory, @@ -227,6 +230,8 @@ namespace NzbDrone.Host dbTarget.Register(); } + SchemaBuilder.Initialize(container); + if (OsInfo.IsNotWindows) { Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null; From f81bb3ec1945d343dd0695a2826dac8833cb6346 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 9 May 2024 03:42:41 +0200 Subject: [PATCH 279/762] New: Blocklist Custom Filters Closes #6763 --- frontend/src/Activity/Blocklist/Blocklist.js | 27 +++++++++- .../Activity/Blocklist/BlocklistConnector.js | 11 +++- .../Blocklist/BlocklistFilterModal.tsx | 54 +++++++++++++++++++ frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/BlocklistAppState.ts | 8 +++ .../src/Store/Actions/blocklistActions.js | 33 +++++++++++- frontend/src/typings/Blocklist.ts | 16 ++++++ .../Blocklisting/BlocklistRepository.cs | 30 ++++++++--- src/NzbDrone.Core/Localization/Core/en.json | 23 ++++---- .../Blocklist/BlocklistController.cs | 16 +++++- 10 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx create mode 100644 frontend/src/App/State/BlocklistAppState.ts create mode 100644 frontend/src/typings/Blocklist.ts diff --git a/frontend/src/Activity/Blocklist/Blocklist.js b/frontend/src/Activity/Blocklist/Blocklist.js index 797aa5175..19026beb5 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.js +++ b/frontend/src/Activity/Blocklist/Blocklist.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; @@ -20,6 +21,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; +import BlocklistFilterModal from './BlocklistFilterModal'; import BlocklistRowConnector from './BlocklistRowConnector'; class Blocklist extends Component { @@ -114,9 +116,13 @@ class Blocklist extends Component { error, items, columns, + selectedFilterKey, + filters, + customFilters, totalRecords, isRemoving, isClearingBlocklistExecuting, + onFilterSelect, ...otherProps } = this.props; @@ -161,6 +167,15 @@ class Blocklist extends Component { iconName={icons.TABLE} /> </TableOptionsModalWrapper> + + <FilterMenu + alignMenu={align.RIGHT} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + filterModalConnectorComponent={BlocklistFilterModal} + onFilterSelect={onFilterSelect} + /> </PageToolbarSection> </PageToolbar> @@ -180,7 +195,11 @@ class Blocklist extends Component { { isPopulated && !error && !items.length && <Alert kind={kinds.INFO}> - {translate('NoHistoryBlocklist')} + { + selectedFilterKey === 'all' ? + translate('NoHistoryBlocklist') : + translate('BlocklistFilterHasNoItems') + } </Alert> } @@ -251,11 +270,15 @@ Blocklist.propTypes = { error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, totalRecords: PropTypes.number, isRemoving: PropTypes.bool.isRequired, isClearingBlocklistExecuting: PropTypes.bool.isRequired, onRemoveSelected: PropTypes.func.isRequired, - onClearBlocklistPress: PropTypes.func.isRequired + onClearBlocklistPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired }; export default Blocklist; diff --git a/frontend/src/Activity/Blocklist/BlocklistConnector.js b/frontend/src/Activity/Blocklist/BlocklistConnector.js index 454fa13a9..5eb055a06 100644 --- a/frontend/src/Activity/Blocklist/BlocklistConnector.js +++ b/frontend/src/Activity/Blocklist/BlocklistConnector.js @@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames'; import withCurrentPage from 'Components/withCurrentPage'; import * as blocklistActions from 'Store/Actions/blocklistActions'; import { executeCommand } from 'Store/Actions/commandActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import Blocklist from './Blocklist'; @@ -13,10 +14,12 @@ import Blocklist from './Blocklist'; function createMapStateToProps() { return createSelector( (state) => state.blocklist, + createCustomFiltersSelector('blocklist'), createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST), - (blocklist, isClearingBlocklistExecuting) => { + (blocklist, customFilters, isClearingBlocklistExecuting) => { return { isClearingBlocklistExecuting, + customFilters, ...blocklist }; } @@ -97,6 +100,10 @@ class BlocklistConnector extends Component { this.props.setBlocklistSort({ sortKey }); }; + onFilterSelect = (selectedFilterKey) => { + this.props.setBlocklistFilter({ selectedFilterKey }); + }; + onClearBlocklistPress = () => { this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST }); }; @@ -122,6 +129,7 @@ class BlocklistConnector extends Component { onPageSelect={this.onPageSelect} onRemoveSelected={this.onRemoveSelected} onSortPress={this.onSortPress} + onFilterSelect={this.onFilterSelect} onTableOptionChange={this.onTableOptionChange} onClearBlocklistPress={this.onClearBlocklistPress} {...this.props} @@ -142,6 +150,7 @@ BlocklistConnector.propTypes = { gotoBlocklistPage: PropTypes.func.isRequired, removeBlocklistItems: PropTypes.func.isRequired, setBlocklistSort: PropTypes.func.isRequired, + setBlocklistFilter: PropTypes.func.isRequired, setBlocklistTableOption: PropTypes.func.isRequired, clearBlocklist: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired diff --git a/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx b/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx new file mode 100644 index 000000000..ea80458f1 --- /dev/null +++ b/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setBlocklistFilter } from 'Store/Actions/blocklistActions'; + +function createBlocklistSelector() { + return createSelector( + (state: AppState) => state.blocklist.items, + (blocklistItems) => { + return blocklistItems; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.blocklist.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface BlocklistFilterModalProps { + isOpen: boolean; +} + +export default function BlocklistFilterModal(props: BlocklistFilterModalProps) { + const sectionItems = useSelector(createBlocklistSelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'blocklist'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + dispatch(setBlocklistFilter(payload)); + }, + [dispatch] + ); + + return ( + <FilterModal + // TODO: Don't spread all the props + {...props} + sectionItems={sectionItems} + filterBuilderProps={filterBuilderProps} + customFilterType={customFilterType} + dispatchSetFilter={dispatchSetFilter} + /> + ); +} diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 72aa0d7f0..222a8e26f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,4 +1,5 @@ import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; +import BlocklistAppState from './BlocklistAppState'; import CalendarAppState from './CalendarAppState'; import CommandAppState from './CommandAppState'; import EpisodeFilesAppState from './EpisodeFilesAppState'; @@ -54,6 +55,7 @@ export interface AppSectionState { interface AppState { app: AppSectionState; + blocklist: BlocklistAppState; calendar: CalendarAppState; commands: CommandAppState; episodeFiles: EpisodeFilesAppState; diff --git a/frontend/src/App/State/BlocklistAppState.ts b/frontend/src/App/State/BlocklistAppState.ts new file mode 100644 index 000000000..e838ad625 --- /dev/null +++ b/frontend/src/App/State/BlocklistAppState.ts @@ -0,0 +1,8 @@ +import Blocklist from 'typings/Blocklist'; +import AppSectionState, { AppSectionFilterState } from './AppSectionState'; + +interface BlocklistAppState + extends AppSectionState<Blocklist>, + AppSectionFilterState<Blocklist> {} + +export default BlocklistAppState; diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js index f341b72aa..6303ad2d1 100644 --- a/frontend/src/Store/Actions/blocklistActions.js +++ b/frontend/src/Store/Actions/blocklistActions.js @@ -1,6 +1,6 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; -import { sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -77,6 +77,31 @@ export const defaultState = { isVisible: true, isModifiable: false } + ], + + selectedFilterKey: 'all', + + filters: [ + { + key: 'all', + label: () => translate('All'), + filters: [] + } + ], + + filterBuilderProps: [ + { + name: 'seriesIds', + label: () => translate('Series'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.SERIES + }, + { + name: 'protocols', + label: () => translate('Protocol'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.PROTOCOL + } ] }; @@ -84,6 +109,7 @@ export const persistState = [ 'blocklist.pageSize', 'blocklist.sortKey', 'blocklist.sortDirection', + 'blocklist.selectedFilterKey', 'blocklist.columns' ]; @@ -97,6 +123,7 @@ export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage'; export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage'; export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage'; export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort'; +export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter'; export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption'; export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem'; export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems'; @@ -112,6 +139,7 @@ export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE); export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE); export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE); export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT); +export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER); export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION); export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM); export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS); @@ -132,7 +160,8 @@ export const actionHandlers = handleThunks({ [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE, [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE, - [serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT + [serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT, + [serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER }), [REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'), diff --git a/frontend/src/typings/Blocklist.ts b/frontend/src/typings/Blocklist.ts new file mode 100644 index 000000000..4cc675cc5 --- /dev/null +++ b/frontend/src/typings/Blocklist.ts @@ -0,0 +1,16 @@ +import ModelBase from 'App/ModelBase'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; + +interface Blocklist extends ModelBase { + languages: Language[]; + quality: QualityModel; + customFormats: CustomFormat[]; + title: string; + date?: string; + protocol: string; + seriesId?: number; +} + +export default Blocklist; diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs b/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs index 43348430b..c2cde2871 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs @@ -40,11 +40,29 @@ namespace NzbDrone.Core.Blocklisting Delete(x => seriesIds.Contains(x.SeriesId)); } - protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType).Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id); - protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder sql) => _database.QueryJoined<Blocklist, Series>(sql, (bl, movie) => - { - bl.Series = movie; - return bl; - }); + public override PagingSpec<Blocklist> GetPaged(PagingSpec<Blocklist> pagingSpec) + { + pagingSpec.Records = GetPagedRecords(PagedBuilder(), pagingSpec, PagedQuery); + + var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Blocklist))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\""; + pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder().Select(typeof(Blocklist)), pagingSpec, countTemplate); + + return pagingSpec; + } + + protected override SqlBuilder PagedBuilder() + { + var builder = Builder() + .Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id); + + return builder; + } + + protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => + _database.QueryJoined<Blocklist, Series>(builder, (blocklist, series) => + { + blocklist.Series = series; + return blocklist; + }); } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 128651296..b5766fa67 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -158,6 +158,7 @@ "BlocklistAndSearch": "Blocklist and Search", "BlocklistAndSearchHint": "Start a search for a replacement after blocklisting", "BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting", + "BlocklistFilterHasNoItems": "Selected blocklist filter contains no items", "BlocklistLoadError": "Unable to load blocklist", "BlocklistMultipleOnlyHint": "Blocklist without searching for replacements", "BlocklistOnly": "Blocklist Only", @@ -248,8 +249,8 @@ "ConnectionLost": "Connection Lost", "ConnectionLostReconnect": "{appName} will try to connect automatically, or you can click reload below.", "ConnectionLostToBackend": "{appName} has lost its connection to the backend and will need to be reloaded to restore functionality.", - "Connections": "Connections", "ConnectionSettingsUrlBaseHelpText": "Adds a prefix to the {connectionName} url, such as {url}", + "Connections": "Connections", "Continuing": "Continuing", "ContinuingOnly": "Continuing Only", "ContinuingSeriesDescription": "More episodes/another season is expected", @@ -280,8 +281,8 @@ "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", + "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.", "CustomFormatsSpecificationFlag": "Flag", "CustomFormatsSpecificationLanguage": "Language", "CustomFormatsSpecificationMaximumSize": "Maximum Size", @@ -410,16 +411,16 @@ "DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}", + "DownloadClientDelugeSettingsDirectory": "Download Directory", + "DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location", + "DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location", "DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}", "DownloadClientDelugeTorrentStateError": "Deluge is reporting an error", "DownloadClientDelugeValidationLabelPluginFailure": "Configuration of label failed", "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}", @@ -867,12 +868,12 @@ "ImportListsSimklSettingsUserListTypePlanToWatch": "Plan To Watch", "ImportListsSimklSettingsUserListTypeWatching": "Watching", "ImportListsSonarrSettingsApiKeyHelpText": "API Key of the {appName} instance to import from", - "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring", - "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored", "ImportListsSonarrSettingsFullUrl": "Full URL", "ImportListsSonarrSettingsFullUrlHelpText": "URL, including port, of the {appName} instance to import from", "ImportListsSonarrSettingsQualityProfilesHelpText": "Quality Profiles from the source instance to import from", "ImportListsSonarrSettingsRootFoldersHelpText": "Root Folders from the source instance to import from", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored", "ImportListsSonarrSettingsTagsHelpText": "Tags from the source instance to import from", "ImportListsSonarrValidationInvalidUrl": "{appName} URL is invalid, are you missing a URL base?", "ImportListsTraktSettingsAdditionalParameters": "Additional Parameters", @@ -976,11 +977,11 @@ "IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.", "IndexerSettingsMinimumSeeders": "Minimum Seeders", "IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.", + "IndexerSettingsMultiLanguageRelease": "Multi Languages", + "IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?", "IndexerSettingsPasskey": "Passkey", "IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", - "IndexerSettingsMultiLanguageRelease": "Multi Languages", - "IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", @@ -1790,7 +1791,6 @@ "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", @@ -1804,6 +1804,7 @@ "SeriesFolderFormat": "Series Folder Format", "SeriesFolderFormatHelpText": "Used when adding a new series or moving series via the series editor", "SeriesFolderImportedTooltip": "Episode imported from series folder", + "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.", "SeriesID": "Series ID", "SeriesIndexFooterContinuing": "Continuing (All episodes downloaded)", "SeriesIndexFooterDownloading": "Downloading (One or more episodes)", diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs index 2091d3cfe..c1f69974b 100644 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs @@ -1,7 +1,9 @@ +using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Blocklisting; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Indexers; using Sonarr.Http; using Sonarr.Http.Extensions; using Sonarr.Http.REST.Attributes; @@ -23,12 +25,22 @@ namespace Sonarr.Api.V3.Blocklist [HttpGet] [Produces("application/json")] - public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging) + public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging, [FromQuery] int[] seriesIds = null, [FromQuery] DownloadProtocol[] protocols = null) { var pagingResource = new PagingResource<BlocklistResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending); - return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator)); + if (seriesIds?.Any() == true) + { + pagingSpec.FilterExpressions.Add(b => seriesIds.Contains(b.SeriesId)); + } + + if (protocols?.Any() == true) + { + pagingSpec.FilterExpressions.Add(b => protocols.Contains(b.Protocol)); + } + + return pagingSpec.ApplyToPage(b => _blocklistService.Paged(pagingSpec), b => BlocklistResourceMapper.MapToResource(b, _formatCalculator)); } [RestDeleteById] From cae134ec7b331d1c906343716472f3d043614b2c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 3 May 2024 20:53:03 -0700 Subject: [PATCH 280/762] New: Dark theme for login screen Closes #6751 --- frontend/src/App/App.js | 9 +- frontend/src/App/ApplyTheme.js | 50 ---------- frontend/src/App/ApplyTheme.tsx | 37 ++++++++ frontend/src/Styles/Themes/index.js | 2 +- frontend/src/login.html | 93 +++++++++++++++---- frontend/src/typings/UiSettings.ts | 2 +- .../Frontend/Mappers/HtmlMapperBase.cs | 2 +- .../Frontend/Mappers/LoginHtmlMapper.cs | 13 +++ 8 files changed, 131 insertions(+), 77 deletions(-) delete mode 100644 frontend/src/App/ApplyTheme.js create mode 100644 frontend/src/App/ApplyTheme.tsx diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index 781b2ca10..754c75035 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -12,11 +12,10 @@ function App({ store, history }) { <DocumentTitle title={window.Sonarr.instanceName}> <Provider store={store}> <ConnectedRouter history={history}> - <ApplyTheme> - <PageConnector> - <AppRoutes app={App} /> - </PageConnector> - </ApplyTheme> + <ApplyTheme /> + <PageConnector> + <AppRoutes app={App} /> + </PageConnector> </ConnectedRouter> </Provider> </DocumentTitle> diff --git a/frontend/src/App/ApplyTheme.js b/frontend/src/App/ApplyTheme.js deleted file mode 100644 index ef177749f..000000000 --- a/frontend/src/App/ApplyTheme.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment, useCallback, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import themes from 'Styles/Themes'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.ui.item.theme || window.Sonarr.theme, - ( - theme - ) => { - return { - theme - }; - } - ); -} - -function ApplyTheme({ theme, children }) { - // Update the CSS Variables - - const updateCSSVariables = useCallback(() => { - const arrayOfVariableKeys = Object.keys(themes[theme]); - const arrayOfVariableValues = Object.values(themes[theme]); - - // Loop through each array key and set the CSS Variables - arrayOfVariableKeys.forEach((cssVariableKey, index) => { - // Based on our snippet from MDN - document.documentElement.style.setProperty( - `--${cssVariableKey}`, - arrayOfVariableValues[index] - ); - }); - }, [theme]); - - // On Component Mount and Component Update - useEffect(() => { - updateCSSVariables(theme); - }, [updateCSSVariables, theme]); - - return <Fragment>{children}</Fragment>; -} - -ApplyTheme.propTypes = { - theme: PropTypes.string.isRequired, - children: PropTypes.object.isRequired -}; - -export default connect(createMapStateToProps)(ApplyTheme); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx new file mode 100644 index 000000000..ad3ad69e9 --- /dev/null +++ b/frontend/src/App/ApplyTheme.tsx @@ -0,0 +1,37 @@ +import React, { Fragment, ReactNode, useCallback, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import themes from 'Styles/Themes'; +import AppState from './State/AppState'; + +interface ApplyThemeProps { + children: ReactNode; +} + +function createThemeSelector() { + return createSelector( + (state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme, + (theme) => { + return theme; + } + ); +} + +function ApplyTheme({ children }: ApplyThemeProps) { + const theme = useSelector(createThemeSelector()); + + const updateCSSVariables = useCallback(() => { + Object.entries(themes[theme]).forEach(([key, value]) => { + document.documentElement.style.setProperty(`--${key}`, value); + }); + }, [theme]); + + // On Component Mount and Component Update + useEffect(() => { + updateCSSVariables(); + }, [updateCSSVariables, theme]); + + return <Fragment>{children}</Fragment>; +} + +export default ApplyTheme; diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index d93c5dd8c..4dec39164 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? { ...dark } : { ...light }; +const auto = defaultDark ? dark : light; export default { auto, diff --git a/frontend/src/login.html b/frontend/src/login.html index e89099276..db7262276 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -57,8 +57,8 @@ <style> body { - background-color: #f5f7fa; - color: #656565; + background-color: var(--pageBackground); + color: var(--textColor); font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } @@ -88,14 +88,14 @@ padding: 10px; border-top-left-radius: 4px; border-top-right-radius: 4px; - background-color: #3a3f51; + background-color: var(--themeDarkColor); } .panel-body { padding: 20px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; - background-color: #fff; + background-color: var(--panelBackground); } .sign-in { @@ -112,16 +112,17 @@ padding: 6px 16px; width: 100%; height: 35px; - border: 1px solid #dde6e9; + background-color: var(--inputBackgroundColor); + border: 1px solid var(--inputBorderColor); border-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); } .form-input:focus { outline: 0; - border-color: #66afe9; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), - 0 0 8px rgba(102, 175, 233, 0.6); + border-color: var(--inputFocusBorderColor); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), + 0 0 8px var(--inputFocusBoxShadowColor); } .button { @@ -130,10 +131,10 @@ padding: 10px 0; width: 100%; border: 1px solid; - border-color: #5899eb; + border-color: var(--primaryBorderColor); border-radius: 4px; - background-color: #5d9cec; - color: #fff; + background-color: var(--primaryBackgroundColor); + color: var(--white); vertical-align: middle; text-align: center; white-space: nowrap; @@ -141,9 +142,9 @@ } .button:hover { - border-color: #3483e7; - background-color: #4b91ea; - color: #fff; + border-color: var(--primaryHoverBorderColor); + background-color: var(--primaryHoverBackgroundColor); + color: var(--white); text-decoration: none; } @@ -165,24 +166,24 @@ .forgot-password { margin-left: auto; - color: #909fa7; + color: var(--forgotPasswordColor); text-decoration: none; font-size: 13px; } .forgot-password:focus, .forgot-password:hover { - color: #748690; + color: var(--forgotPasswordAltColor); text-decoration: underline; } .forgot-password:visited { - color: #748690; + color: var(--forgotPasswordAltColor); } .login-failed { margin-top: 20px; - color: #f05050; + color: var(--failedColor); font-size: 14px; } @@ -291,5 +292,59 @@ loginFailedDiv.classList.remove("hidden"); } + + var light = { + white: '#fff', + pageBackground: '#f5f7fa', + textColor: '#656565', + themeDarkColor: '#3a3f51', + panelBackground: '#fff', + inputBackgroundColor: '#fff', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + primaryBackgroundColor: '#5d9cec', + primaryBorderColor: '#5899eb', + primaryHoverBackgroundColor: '#4b91ea', + primaryHoverBorderColor: '#3483e7', + failedColor: '#f05050', + forgotPasswordColor: '#909fa7', + forgotPasswordAltColor: '#748690' + }; + + var dark = { + white: '#fff', + pageBackground: '#202020', + textColor: '#656565', + themeDarkColor: '#494949', + panelBackground: '#111', + inputBackgroundColor: '#333', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + primaryBackgroundColor: '#5d9cec', + primaryBorderColor: '#5899eb', + primaryHoverBackgroundColor: '#4b91ea', + primaryHoverBorderColor: '#3483e7', + failedColor: '#f05050', + forgotPasswordColor: '#737d83', + forgotPasswordAltColor: '#546067' + }; + + var theme = "_THEME_"; + var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ? + dark : + light; + + Object.entries(finalTheme).forEach(([key, value]) => { + document.documentElement.style.setProperty( + `--${key}`, + value + ); + }); + </script> </html> diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/UiSettings.ts index 785ca0c8e..3c23a0356 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/UiSettings.ts @@ -1,5 +1,5 @@ export interface UiSettings { - theme: string; + theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; longDateFormat: string; diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs index 6dcd25b16..efa8a7882 100644 --- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -39,7 +39,7 @@ namespace Sonarr.Http.Frontend.Mappers return stream; } - protected string GetHtmlText() + protected virtual string GetHtmlText() { if (RuntimeInfo.IsProduction && _generatedContent != null) { diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index 58f73db91..ca325add6 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -9,6 +9,8 @@ namespace Sonarr.Http.Frontend.Mappers { public class LoginHtmlMapper : HtmlMapperBase { + private readonly IConfigFileProvider _configFileProvider; + public LoginHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, @@ -16,6 +18,7 @@ namespace Sonarr.Http.Frontend.Mappers Logger logger) : base(diskProvider, cacheBreakProviderFactory, logger) { + _configFileProvider = configFileProvider; HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); UrlBase = configFileProvider.UrlBase; } @@ -29,5 +32,15 @@ namespace Sonarr.Http.Frontend.Mappers { return resourceUrl.StartsWith("/login"); } + + protected override string GetHtmlText() + { + var html = base.GetHtmlText(); + var theme = _configFileProvider.Theme; + + html = html.Replace("_THEME_", theme); + + return html; + } } } From 7e8d8500f28ef8f44560da4ce827745e12f9a346 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 9 May 2024 04:43:51 +0300 Subject: [PATCH 281/762] Fixed: Next/previous/last air dates with Postgres DB Closes #6790 --- .../SeriesStats/SeasonStatistics.cs | 7 +- .../SeriesStats/SeriesStatistics.cs | 75 +------------------ .../SeriesStats/SeriesStatisticsService.cs | 24 +++--- 3 files changed, 19 insertions(+), 87 deletions(-) diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs index 5fe1507c2..793a56f85 100644 --- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -28,7 +29,7 @@ namespace NzbDrone.Core.SeriesStats try { - if (!DateTime.TryParse(NextAiringString, out nextAiring)) + if (!DateTime.TryParse(NextAiringString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out nextAiring)) { return null; } @@ -51,7 +52,7 @@ namespace NzbDrone.Core.SeriesStats try { - if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) + if (!DateTime.TryParse(PreviousAiringString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out previousAiring)) { return null; } @@ -74,7 +75,7 @@ namespace NzbDrone.Core.SeriesStats try { - if (!DateTime.TryParse(LastAiredString, out lastAired)) + if (!DateTime.TryParse(LastAiredString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out lastAired)) { return null; } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs index cb054da7c..030b04ee3 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs @@ -7,83 +7,14 @@ namespace NzbDrone.Core.SeriesStats public class SeriesStatistics : ResultSet { public int SeriesId { get; set; } - public string NextAiringString { get; set; } - public string PreviousAiringString { get; set; } - public string LastAiredString { get; set; } + public DateTime? NextAiring { get; set; } + public DateTime? PreviousAiring { get; set; } + public DateTime? LastAired { get; set; } public int EpisodeFileCount { get; set; } public int EpisodeCount { get; set; } public int TotalEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public List<string> ReleaseGroups { get; set; } public List<SeasonStatistics> SeasonStatistics { get; set; } - - public DateTime? NextAiring - { - get - { - DateTime nextAiring; - - try - { - if (!DateTime.TryParse(NextAiringString, out nextAiring)) - { - return null; - } - } - catch (ArgumentOutOfRangeException) - { - // GHI 3518: Can throw on mono (6.x?) despite being a Try* - return null; - } - - return nextAiring; - } - } - - public DateTime? PreviousAiring - { - get - { - DateTime previousAiring; - - try - { - if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) - { - return null; - } - } - catch (ArgumentOutOfRangeException) - { - // GHI 3518: Can throw on mono (6.x?) despite being a Try* - return null; - } - - return previousAiring; - } - } - - public DateTime? LastAired - { - get - { - DateTime lastAired; - - try - { - if (!DateTime.TryParse(LastAiredString, out lastAired)) - { - return null; - } - } - catch (ArgumentOutOfRangeException) - { - // GHI 3518: Can throw on mono (6.x?) despite being a Try* - return null; - } - - return lastAired; - } - } } } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs index 5baef28f1..e9c05da97 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs @@ -40,23 +40,23 @@ namespace NzbDrone.Core.SeriesStats private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics) { var seriesStatistics = new SeriesStatistics - { - SeasonStatistics = seasonStatistics, - SeriesId = seasonStatistics.First().SeriesId, - EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount), - EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount), - TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), - SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk), - ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList() - }; + { + SeasonStatistics = seasonStatistics, + SeriesId = seasonStatistics.First().SeriesId, + EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount), + EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount), + TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), + SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk), + ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList() + }; var nextAiring = seasonStatistics.Where(s => s.NextAiring != null).MinBy(s => s.NextAiring); var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null).MaxBy(s => s.PreviousAiring); var lastAired = seasonStatistics.Where(s => s.SeasonNumber > 0 && s.LastAired != null).MaxBy(s => s.LastAired); - seriesStatistics.NextAiringString = nextAiring?.NextAiringString; - seriesStatistics.PreviousAiringString = previousAiring?.PreviousAiringString; - seriesStatistics.LastAiredString = lastAired?.LastAiredString; + seriesStatistics.NextAiring = nextAiring?.NextAiring; + seriesStatistics.PreviousAiring = previousAiring?.PreviousAiring; + seriesStatistics.LastAired = lastAired?.LastAired; return seriesStatistics; } From 8360dd7a7bab1dfb49a40aae382b47e9253d9fd1 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 6 May 2024 21:24:34 +0300 Subject: [PATCH 282/762] Fixed: Parsing long downloading/seeding values from Transmission --- .../Download/Clients/Transmission/TransmissionTorrent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index 3552c36e9..4e66b7a02 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission public bool IsFinished { get; set; } public long Eta { get; set; } public TransmissionTorrentStatus Status { get; set; } - public int SecondsDownloading { get; set; } - public int SecondsSeeding { get; set; } + public long SecondsDownloading { get; set; } + public long SecondsSeeding { get; set; } public string ErrorString { get; set; } public long DownloadedEver { get; set; } public long UploadedEver { get; set; } From 1eddf3a152fae04142263c02a3e3b317ff2feeb2 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 7 May 2024 00:21:50 +0300 Subject: [PATCH 283/762] Use number input for seed ratio --- src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs | 2 +- src/Sonarr.Http/ClientSchema/SchemaBuilder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs index af9fad86f..ff3d593fb 100644 --- a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Indexers public class SeedCriteriaSettings { - [FieldDefinition(0, Type = FieldType.Textbox, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")] + [FieldDefinition(0, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")] public double? SeedRatio { get; set; } [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)] diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index f6fc874f1..39e2965a4 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -162,7 +162,7 @@ namespace Sonarr.Http.ClientSchema field.Hidden = fieldAttribute.Hidden.ToString().FirstCharToLower(); } - if (fieldAttribute.Type is FieldType.Number && propertyInfo.PropertyType == typeof(double)) + if (fieldAttribute.Type is FieldType.Number && (propertyInfo.PropertyType == typeof(double) || propertyInfo.PropertyType == typeof(double?))) { field.IsFloat = true; } From 29176c8367df8086a04048a2a240917bab3040b2 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 7 May 2024 17:03:32 -0700 Subject: [PATCH 284/762] New: Has Unmonitored Season filter for Series --- frontend/src/Store/Actions/seriesActions.js | 22 +++++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 23 insertions(+) diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index d47eb9d86..54524ba38 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -192,6 +192,22 @@ export const filterPredicates = { }); return predicate(hasMissingSeason, filterValue); + }, + + hasUnmonitoredSeason: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + const { seasons = [] } = item; + + const hasUnmonitoredSeason = seasons.some((season) => { + const { + seasonNumber, + monitored + } = season; + + return seasonNumber > 0 && !monitored; + }); + + return predicate(hasUnmonitoredSeason, filterValue); } }; @@ -353,6 +369,12 @@ export const filterBuilderProps = [ type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.BOOL }, + { + name: 'hasUnmonitoredSeason', + label: () => translate('HasUnmonitoredSeason'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, { name: 'year', label: () => translate('Year'), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b5766fa67..46546a8ca 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -752,6 +752,7 @@ "Group": "Group", "HardlinkCopyFiles": "Hardlink/Copy Files", "HasMissingSeason": "Has Missing Season", + "HasUnmonitoredSeason": "Has Unmonitored Season", "Health": "Health", "HealthMessagesInfoBox": "You can find more information about the cause of these health check messages by clicking the wiki link (book icon) at the end of the row, or by checking your [logs]({link}). If you have difficulty interpreting these messages then you can reach out to our support, at the links below.", "Here": "here", From f50a263f4f89d7b791e1467c93149ce5ea90e3af Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 7 May 2024 17:14:28 -0700 Subject: [PATCH 285/762] New: Add Custom Format Score to file in Episode Details --- frontend/src/Episode/Summary/EpisodeFileRow.css | 6 ++++++ .../src/Episode/Summary/EpisodeFileRow.css.d.ts | 1 + frontend/src/Episode/Summary/EpisodeFileRow.js | 14 ++++++++++++++ frontend/src/Episode/Summary/EpisodeSummary.js | 15 ++++++++++++++- .../Episode/Summary/EpisodeSummaryConnector.js | 6 ++++-- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.css b/frontend/src/Episode/Summary/EpisodeFileRow.css index e694aa8c3..e7e17d240 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.css +++ b/frontend/src/Episode/Summary/EpisodeFileRow.css @@ -17,6 +17,12 @@ width: 175px; } +.customFormatScore { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 65px; +} + .actions { composes: cell from '~Components/Table/Cells/TableRowCell.css'; diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.css.d.ts b/frontend/src/Episode/Summary/EpisodeFileRow.css.d.ts index 7352d59f7..cb57d0e8a 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.css.d.ts +++ b/frontend/src/Episode/Summary/EpisodeFileRow.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'actions': string; + 'customFormatScore': string; 'customFormats': string; 'languages': string; 'quality': string; diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.js b/frontend/src/Episode/Summary/EpisodeFileRow.js index f23b7c9ce..31c1c93c1 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.js +++ b/frontend/src/Episode/Summary/EpisodeFileRow.js @@ -11,6 +11,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; import MediaInfo from './MediaInfo'; import styles from './EpisodeFileRow.css'; @@ -55,6 +56,7 @@ class EpisodeFileRow extends Component { languages, quality, customFormats, + customFormatScore, qualityCutoffNotMet, mediaInfo, columns @@ -127,6 +129,17 @@ class EpisodeFileRow extends Component { ); } + if (name === 'customFormatScore') { + return ( + <TableRowCell + key={name} + className={styles.customFormatScore} + > + {formatCustomFormatScore(customFormatScore, customFormats.length)} + </TableRowCell> + ); + } + if (name === 'actions') { return ( <TableRowCell @@ -183,6 +196,7 @@ EpisodeFileRow.propTypes = { quality: PropTypes.object.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), + customFormatScore: PropTypes.number.isRequired, mediaInfo: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, onDeleteEpisodeFile: PropTypes.func.isRequired diff --git a/frontend/src/Episode/Summary/EpisodeSummary.js b/frontend/src/Episode/Summary/EpisodeSummary.js index 0d454e90e..497008467 100644 --- a/frontend/src/Episode/Summary/EpisodeSummary.js +++ b/frontend/src/Episode/Summary/EpisodeSummary.js @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Icon from 'Components/Icon'; import Label from 'Components/Label'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import { kinds, sizes } from 'Helpers/Props'; +import { icons, kinds, sizes } from 'Helpers/Props'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import translate from 'Utilities/String/translate'; import EpisodeAiringConnector from './EpisodeAiringConnector'; @@ -42,6 +43,15 @@ const columns = [ isSortable: false, isVisible: true }, + { + name: 'customFormatScore', + label: React.createElement(Icon, { + name: icons.SCORE, + title: () => translate('CustomFormatScore') + }), + isSortable: true, + isVisible: true + }, { name: 'actions', label: '', @@ -94,6 +104,7 @@ class EpisodeSummary extends Component { languages, quality, customFormats, + customFormatScore, qualityCutoffNotMet, onDeleteEpisodeFile } = this.props; @@ -143,6 +154,7 @@ class EpisodeSummary extends Component { quality={quality} qualityCutoffNotMet={qualityCutoffNotMet} customFormats={customFormats} + customFormatScore={customFormatScore} mediaInfo={mediaInfo} columns={columns} onDeleteEpisodeFile={onDeleteEpisodeFile} @@ -179,6 +191,7 @@ EpisodeSummary.propTypes = { quality: PropTypes.object, qualityCutoffNotMet: PropTypes.bool, customFormats: PropTypes.arrayOf(PropTypes.object), + customFormatScore: PropTypes.number.isRequired, onDeleteEpisodeFile: PropTypes.func.isRequired }; diff --git a/frontend/src/Episode/Summary/EpisodeSummaryConnector.js b/frontend/src/Episode/Summary/EpisodeSummaryConnector.js index 530138944..15f940f2c 100644 --- a/frontend/src/Episode/Summary/EpisodeSummaryConnector.js +++ b/frontend/src/Episode/Summary/EpisodeSummaryConnector.js @@ -31,7 +31,8 @@ function createMapStateToProps() { languages, quality, qualityCutoffNotMet, - customFormats + customFormats, + customFormatScore } = episodeFile; return { @@ -45,7 +46,8 @@ function createMapStateToProps() { languages, quality, qualityCutoffNotMet, - customFormats + customFormats, + customFormatScore }; } ); From cc0a284660f139d5f47b27a2c389973e5e888587 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 7 May 2024 17:45:28 -0700 Subject: [PATCH 286/762] New: Add series tags to Webhook and Notifiarr events --- .../CustomScript/CustomScript.cs | 23 +++++++++---- .../Notifications/Notifiarr/Notifiarr.cs | 5 +-- .../Notifications/Webhook/Webhook.cs | 5 +-- .../Notifications/Webhook/WebhookBase.cs | 34 +++++++++++++------ .../Notifications/Webhook/WebhookSeries.cs | 5 ++- src/NzbDrone.Core/Tags/TagRepository.cs | 7 ++++ 6 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index ec2974b54..6b6f3a086 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_Release_EpisodeCount", remoteEpisode.Episodes.Count.ToString()); environmentVariables.Add("Sonarr_Release_SeasonNumber", remoteEpisode.Episodes.First().SeasonNumber.ToString()); environmentVariables.Add("Sonarr_Release_EpisodeNumbers", string.Join(",", remoteEpisode.Episodes.Select(e => e.EpisodeNumber))); @@ -121,7 +121,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_EpisodeFile_Id", episodeFile.Id.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_RelativePath", episodeFile.RelativePath); @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_EpisodeFile_Ids", string.Join(",", renamedFiles.Select(e => e.EpisodeFile.Id))); environmentVariables.Add("Sonarr_EpisodeFile_RelativePaths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.RelativePath))); environmentVariables.Add("Sonarr_EpisodeFile_Paths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.Path))); @@ -218,7 +218,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_EpisodeFile_Id", episodeFile.Id.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_RelativePath", episodeFile.RelativePath); @@ -257,7 +257,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); ExecuteScript(environmentVariables); } @@ -281,7 +281,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_Series_DeletedFiles", deleteMessage.DeletedFiles.ToString()); ExecuteScript(environmentVariables); @@ -350,7 +350,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label))); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientInfo?.Name ?? string.Empty); environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientInfo?.Type ?? string.Empty); environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); @@ -411,5 +411,14 @@ namespace NzbDrone.Core.Notifications.CustomScript { return possibleParent.IsParentPath(path); } + + private List<string> GetTagLabels(Series series) + { + return _tagRepository.GetTags(series.Tags) + .Select(s => s.Label) + .Where(l => l.IsNotNullOrWhiteSpace()) + .OrderBy(l => l) + .ToList(); + } } } diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs index cc3c9b7b7..96251eb32 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications.Webhook; +using NzbDrone.Core.Tags; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; @@ -14,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Notifiarr { private readonly INotifiarrProxy _proxy; - public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService) - : base(configFileProvider, configService, localizationService) + public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) + : base(configFileProvider, configService, localizationService, tagRepository) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs index b7cf6379d..7d0e84478 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tags; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; @@ -13,8 +14,8 @@ namespace NzbDrone.Core.Notifications.Webhook { private readonly IWebhookProxy _proxy; - public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService) - : base(configFileProvider, configService, localizationService) + public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) + : base(configFileProvider, configService, localizationService, tagRepository) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index d114a88db..df2d29355 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; @@ -15,12 +17,14 @@ namespace NzbDrone.Core.Notifications.Webhook private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; protected readonly ILocalizationService _localizationService; + private readonly ITagRepository _tagRepository; - protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService) + protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) { _configFileProvider = configFileProvider; _configService = configService; _localizationService = localizationService; + _tagRepository = tagRepository; } protected WebhookGrabPayload BuildOnGrabPayload(GrabMessage message) @@ -33,7 +37,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Grab, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series), + Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)), Release = new WebhookRelease(quality, remoteEpisode), DownloadClient = message.DownloadClientName, @@ -52,7 +56,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Download, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series), + Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)), EpisodeFile = new WebhookEpisodeFile(episodeFile), Release = new WebhookGrabbedRelease(message.Release), @@ -82,7 +86,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.EpisodeFileDelete, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(deleteMessage.Series), + Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)), Episodes = deleteMessage.EpisodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)), EpisodeFile = new WebhookEpisodeFile(deleteMessage.EpisodeFile), DeleteReason = deleteMessage.Reason @@ -96,7 +100,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.SeriesAdd, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(addMessage.Series), + Series = new WebhookSeries(addMessage.Series, GetTagLabels(addMessage.Series)), }; } @@ -107,7 +111,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.SeriesDelete, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(deleteMessage.Series), + Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)), DeletedFiles = deleteMessage.DeletedFiles }; } @@ -119,7 +123,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Rename, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(series), + Series = new WebhookSeries(series, GetTagLabels(series)), RenamedEpisodeFiles = renamedFiles.ConvertAll(x => new WebhookRenamedEpisodeFile(x)) }; } @@ -172,7 +176,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.ManualInteractionRequired, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series), + Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)), DownloadInfo = new WebhookDownloadClientItem(quality, message.TrackedDownload.DownloadItem), DownloadClient = message.DownloadClientInfo?.Name, @@ -192,12 +196,13 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Test, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries() + Series = new WebhookSeries { Id = 1, Title = "Test Title", Path = "C:\\testpath", - TvdbId = 1234 + TvdbId = 1234, + Tags = new List<string> { "test-tag" } }, Episodes = new List<WebhookEpisode>() { @@ -211,5 +216,14 @@ namespace NzbDrone.Core.Notifications.Webhook } }; } + + private List<string> GetTagLabels(Series series) + { + return _tagRepository.GetTags(series.Tags) + .Select(s => s.Label) + .Where(l => l.IsNotNullOrWhiteSpace()) + .OrderBy(l => l) + .ToList(); + } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs index c3662ee11..57200fb40 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Webhook @@ -13,12 +14,13 @@ namespace NzbDrone.Core.Notifications.Webhook public string ImdbId { get; set; } public SeriesTypes Type { get; set; } public int Year { get; set; } + public List<string> Tags { get; set; } public WebhookSeries() { } - public WebhookSeries(Series series) + public WebhookSeries(Series series, List<string> tags) { Id = series.Id; Title = series.Title; @@ -29,6 +31,7 @@ namespace NzbDrone.Core.Notifications.Webhook ImdbId = series.ImdbId; Type = series.SeriesType; Year = series.Year; + Tags = tags; } } } diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index e24315675..39927d963 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -9,6 +10,7 @@ namespace NzbDrone.Core.Tags { Tag GetByLabel(string label); Tag FindByLabel(string label); + List<Tag> GetTags(HashSet<int> tagIds); } public class TagRepository : BasicRepository<Tag>, ITagRepository @@ -34,5 +36,10 @@ namespace NzbDrone.Core.Tags { return Query(c => c.Label == label).SingleOrDefault(); } + + public List<Tag> GetTags(HashSet<int> tagIds) + { + return Query(t => tagIds.Contains(t.Id)); + } } } From b4d05214ae78b82f537494b191df8438aab8f2c5 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 7 May 2024 20:47:44 -0700 Subject: [PATCH 287/762] Fixed: Ignore invalid movie tags when writing XBMC metadata Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com> --- .../Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 38982b579..249a9cbd3 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -183,7 +183,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc if (series.Tags.Any()) { - var tags = _tagRepo.Get(series.Tags); + var tags = _tagRepo.GetTags(series.Tags); foreach (var tag in tags) { From 20d00fe88c49c63c276c448d80a06c1acfbf6211 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 10 May 2024 00:26:41 +0000 Subject: [PATCH 288/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dani Talens <databio@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Michael5564445 <michaelvelosk@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 50 +++- src/NzbDrone.Core/Localization/Core/es.json | 9 +- .../Localization/Core/pt_BR.json | 10 +- src/NzbDrone.Core/Localization/Core/tr.json | 52 ++-- src/NzbDrone.Core/Localization/Core/uk.json | 243 +++++++++++++++++- 5 files changed, 326 insertions(+), 38 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 4deede861..77decb65e 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -1,6 +1,6 @@ { "Added": "Afegit", - "ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau de l'API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració", + "ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració", "AgeWhenGrabbed": "Edat (quan es captura)", "AuthenticationRequired": "Autenticació necessària", "AutomaticAdd": "Afegeix automàticament", @@ -49,7 +49,7 @@ "Default": "Per defecte", "DeleteImportList": "Suprimeix la llista d'importació", "DeleteSelectedImportLists": "Suprimeix la(es) llista(es) d'importació", - "DeletedReasonManual": "El fitxer va ser suprimit mitjançant la interfície d'usuari", + "DeletedReasonManual": "El fitxer s'ha suprimit mitjançant {appName}, ja sigui manualment o amb una altra eina mitjançant l'API", "AbsoluteEpisodeNumbers": "Números d'episodis absoluts", "AirDate": "Data d'emissió", "DefaultNameCopiedProfile": "{name} - Còpia", @@ -225,8 +225,8 @@ "BlocklistReleases": "Llista de llançaments bloquejats", "BuiltIn": "Integrat", "BrowserReloadRequired": "Es requereix una recàrrega del navegador", - "ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base", - "AuthBasic": "Basic (finestra emergent del navegador)", + "ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base", + "AuthBasic": "Bàsic (finestra emergent del navegador)", "AutomaticSearch": "Cerca automàtica", "Automatic": "Automàtic", "BindAddress": "Adreça d'enllaç", @@ -239,13 +239,13 @@ "BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}", "CalendarLoadError": "No es pot carregar el calendari", "AudioInfo": "Informació d'àudio", - "ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes", + "ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes", "AutoAdd": "Afegeix automàticament", "Apply": "Aplica", "Backup": "Còpia de seguretat", "Blocklist": "Llista de bloquejats", "Calendar": "Calendari", - "ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent", + "ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent", "AptUpdater": "Utilitzeu apt per a instal·lar l'actualització", "BackupNow": "Fes ara la còpia de seguretat", "AppDataDirectory": "Directori AppData", @@ -411,7 +411,7 @@ "PrioritySettings": "Prioritat: {priority}", "QualitiesLoadError": "No es poden carregar perfils de qualitat", "RemoveSelectedItemQueueMessageText": "Esteu segur que voleu eliminar 1 element de la cua?", - "ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?", + "ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?", "TheLogLevelDefault": "El nivell de registre per defecte és \"Info\" i es pot canviar a [Configuració general](/configuració/general)", "ClickToChangeLanguage": "Feu clic per a canviar l'idioma", "ClickToChangeEpisode": "Feu clic per a canviar l'episodi", @@ -669,7 +669,7 @@ "AutoTaggingSpecificationOriginalLanguage": "Llenguatge", "AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat", "AutoTaggingSpecificationRootFolder": "Carpeta arrel", - "AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar", + "AddDelayProfileError": "No s'ha pogut afegir un perfil de retard, torna-ho a provar.", "AutoTaggingSpecificationSeriesType": "Tipus de Sèries", "AutoTaggingSpecificationStatus": "Estat", "BlocklistAndSearch": "Llista de bloqueig i cerca", @@ -711,5 +711,37 @@ "MinutesSixty": "60 minuts: {sixty}", "CustomFilter": "Filtres personalitzats", "CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules", - "CustomFormatsSpecificationFlag": "Bandera" + "CustomFormatsSpecificationFlag": "Bandera", + "DeleteSelectedSeries": "Suprimeix les sèries seleccionades", + "DeleteSeriesFolder": "Suprimeix la carpeta de sèries", + "DeleteSeriesFolderConfirmation": "La carpeta de sèries '{path}' i tot el seu contingut es suprimiran.", + "File": "Fitxer", + "FilterContains": "conté", + "FilterIs": "és", + "Ok": "D'acord", + "Qualities": "Qualitats", + "DeleteSeriesFolderCountConfirmation": "Esteu segur que voleu suprimir {count} sèries seleccionades?", + "Forecast": "Previsió", + "Paused": "En pausa", + "Range": "Rang", + "Today": "Avui", + "Month": "Mes", + "Reason": "Raó", + "Pending": "Pendents", + "FilterEqual": "igual", + "Global": "Global", + "Grab": "Captura", + "Logout": "Tanca la sessió", + "Filter": "Filtre", + "Local": "Local", + "Week": "Setmana", + "AutoTaggingSpecificationTag": "Etiqueta", + "WhatsNew": "Novetats", + "DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {clientName}, com ara {url}", + "IndexerHDBitsSettingsCodecs": "Còdecs", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de rTorrent", + "IndexerHDBitsSettingsMediums": "Mitjans", + "UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.", + "UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 0d19af500..858696d42 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -249,7 +249,7 @@ "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.", "CalendarOptions": "Opciones de Calendario", "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", - "Default": "Por defecto", + "Default": "Predeterminado", "DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{name}'?", "DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?", "AddImportListImplementation": "Añadir lista de importación - {implementationName}", @@ -1964,7 +1964,7 @@ "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}", + "OverrideGrabModalTitle": "Sobrescribir y capturar - {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.", @@ -2071,5 +2071,8 @@ "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título", "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones", "IndexerSettingsMultiLanguageRelease": "Múltiples idiomas", - "IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?" + "IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está reportando archivos faltantes", + "BlocklistFilterHasNoItems": "El filtro de lista de bloqueo seleccionado no contiene ningún elemento", + "HasUnmonitoredSeason": "Tiene temporada sin monitorizar" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 74578fd45..6ef21a7f7 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1190,7 +1190,7 @@ "FilterNotInNext": "não no próximo", "FilterSeriesPlaceholder": "Filtrar séries", "FilterStartsWith": "começa com", - "GrabRelease": "Obter lançamento", + "GrabRelease": "Baixar Lançamento", "HardlinkCopyFiles": "Hardlink/Copiar Arquivos", "InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado", "InteractiveImportNoImportMode": "Defina um modo de importação", @@ -1203,7 +1203,7 @@ "KeyboardShortcutsOpenModal": "Abrir este pop-up", "Local": "Local", "Logout": "Sair", - "ManualGrab": "Obter manualmente", + "ManualGrab": "Baixar Manualmente", "ManualImport": "Importação manual", "Mapping": "Mapeamento", "MarkAsFailedConfirmation": "Tem certeza de que deseja marcar \"{sourceTitle}\" como em falha?", @@ -1229,7 +1229,7 @@ "RemoveFilter": "Remover filtro", "RootFolderSelectFreeSpace": "{freeSpace} Livre", "Search": "Pesquisar", - "SelectDownloadClientModalTitle": "{modalTitle} - Selecione o Cliente de Download", + "SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download", "SelectReleaseGroup": "Selecionar um Grupo de Lançamento", "SelectSeason": "Selecionar Temporada", "SelectSeasonModalTitle": "{modalTitle} - Selecione a Temporada", @@ -2071,5 +2071,7 @@ "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.", "IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?", "AutoTaggingSpecificationTag": "Etiqueta", - "IndexerSettingsMultiLanguageRelease": "Multi Idiomas" + "IndexerSettingsMultiLanguageRelease": "Multi Idiomas", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está relatando arquivos perdidos", + "BlocklistFilterHasNoItems": "O filtro da lista de bloqueio selecionado não contém itens" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 6e3810449..74b90b989 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -31,7 +31,7 @@ "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", "AddImportListExclusionError": "Yeni bir içe aktarım listesi dışlaması eklenemiyor, lütfen tekrar deneyin.", "AddImportListImplementation": "İçe Aktarım Listesi Ekle -{implementationName}", - "AddIndexer": "Dizin Oluşturucu Ekle", + "AddIndexer": "Dizinleyici Ekle", "AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın", "AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", "AddReleaseProfile": "Yayın Profili Ekle", @@ -40,7 +40,7 @@ "AddSeriesWithTitle": "{title} Ekleyin", "Agenda": "Ajanda", "Airs": "Yayınlar", - "AddIndexerError": "Yeni dizin oluşturucu eklenemiyor, lütfen tekrar deneyin.", + "AddIndexerError": "Yeni dizinleyici eklenemiyor, lütfen tekrar deneyin.", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Karşılanmamış bölümleri aramaya başlayın", "AddQualityProfileError": "Yeni kalite profili eklenemiyor, lütfen tekrar deneyin.", "AddRemotePathMappingError": "Yeni bir uzak yol eşlemesi eklenemiyor, lütfen tekrar deneyin.", @@ -68,7 +68,7 @@ "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", + "ClickToChangeIndexerFlags": "Dizinleyici bayraklarını değiştirmek için tıklayın", "ClickToChangeReleaseGroup": "Yayım grubunu değiştirmek için tıklayın", "AppUpdated": "{appName} Güncellendi", "ApplicationURL": "Uygulama URL'si", @@ -127,7 +127,7 @@ "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", + "CountIndexersSelected": "{count} dizinleyici 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", @@ -155,7 +155,7 @@ "DelayMinutes": "{delay} Dakika", "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", "DeleteReleaseProfile": "Yayımlama Profilini Sil", - "DeleteSelectedIndexers": "Dizin Oluşturucuları Sil", + "DeleteSelectedIndexers": "Dizinleyicileri Sil", "Directory": "Rehber", "Donate": "Bağış yap", "DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil", @@ -176,7 +176,7 @@ "DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?", "DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", "DeleteQualityProfileMessageText": "'{name}' kalite profilini silmek istediğinizden emin misiniz?", - "DeleteSelectedIndexersMessageText": "Seçilen {count} dizin oluşturucuyu silmek istediğinizden emin misiniz?", + "DeleteSelectedIndexersMessageText": "Seçilen {count} dizinleyiciyi silmek istediğinizden emin misiniz?", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName}, etiketi {clientName} uygulamasına ekleyemedi.", "DownloadClientDownloadStationProviderMessage": "DSM hesabınızda 2 Faktörlü Kimlik Doğrulama etkinleştirilmişse {appName}, Download Station'a bağlanamaz", "DownloadClientDownloadStationValidationApiVersion": "Download Station API sürümü desteklenmiyor; en az {requiredVersion} olmalıdır. {minVersion}'dan {maxVersion}'a kadar destekler", @@ -195,13 +195,13 @@ "DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu", "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", "DeleteImportList": "İçe Aktarma Listesini Sil", - "IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", + "IndexerPriorityHelpText": "Dizinleyici Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation'da '{sharedFolder}' adında bir Paylaşımlı Klasör yok, bunu doğru belirttiğinizden emin misiniz?", - "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizin Oluşturucular'daki mevcut tohum kriterlerine göre torrentlerin otomatik olarak kaldırılmasını gerçekleştirecek", + "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizinleyiciler'deki geçerli başlangıç ölçütlerine göre torrentlerin otomatik olarak kaldırılmasını sağlar", "Database": "Veri tabanı", "DelayProfileProtocol": "Protokol: {preferredProtocol}", "DownloadClientDownloadStationValidationFolderMissingDetail": "'{downloadDir}' klasörü mevcut değil, '{sharedFolder}' Paylaşımlı Klasöründe manuel olarak oluşturulması gerekiyor.", @@ -390,9 +390,9 @@ "FailedToFetchUpdates": "Güncellemeler getirilemedi", "InstanceName": "Örnek isim", "MoveAutomatically": "Otomatik Olarak Taşı", - "MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)", + "MustContainHelpText": "Yayın, bu terimlerden en az birini içermelidir (büyük / küçük harfe duyarsız)", "NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor", - "EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle", + "EditSelectedIndexers": "Seçili Dizinleyicileri Düzenle", "EnableProfileHelpText": "Yayımlama profilini etkinleştirmek için işaretleyin", "EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli periyotlarda yayın değişikliği aradığında kullanacak", "FormatTimeSpanDays": "{days}g {time}", @@ -407,13 +407,13 @@ "FormatRuntimeHours": "{hours}s", "LanguagesLoadError": "Diller yüklenemiyor", "ListWillRefreshEveryInterval": "Liste her {refreshInterval} yenilenecektir", - "ManageIndexers": "Dizin Oluşturucuları Yönet", + "ManageIndexers": "Dizinleyicileri Yönet", "ManualGrab": "Manuel Yakalama", "DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri", "DownloadClients": "İndirme İstemcileri", "InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı", "ListQualityProfileHelpText": "Kalite Profili listesi öğeleri şu şekilde eklenecektir:", - "MustNotContainHelpText": "Bir veya daha fazla terimi içeriyorsa yayın reddedilecektir (büyük/küçük harfe duyarlı değildir)", + "MustNotContainHelpText": "Yayın, bir veya daha fazla terim içeriyorsa reddedilir (büyük/ küçük harfe duyarsız)", "NoDownloadClientsFound": "İndirme istemcisi bulunamadı", "NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}", "NotificationsAppriseSettingsStatelessUrls": "Apprise Durum bilgisi olmayan URL'ler", @@ -446,7 +446,7 @@ "MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.", "Never": "Asla", "NoHistoryBlocklist": "Geçmiş engellenenler listesi yok", - "NoIndexersFound": "Dizin oluşturucu bulunamadı", + "NoIndexersFound": "Dizinleyici bulunamadı", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.", "NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası", "NotificationsAppriseSettingsUsernameHelpText": "HTTP Temel Kimlik Doğrulama Kullanıcı Adı", @@ -488,7 +488,7 @@ "Test": "Sına", "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", "IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent karma tarafından engellendiyse, RSS/Bazı dizin oluşturucuları arama sırasında düzgün şekilde reddedilmeyebilir; bunun etkinleştirilmesi, torrent yakalandıktan sonra ancak istemciye gönderilmeden önce reddedilmesine olanak tanır.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent yakalandıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", "NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı", "NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü", "NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si", @@ -506,7 +506,7 @@ "NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?", "NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın", "InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", - "IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin", + "IndexerDownloadClientHelpText": "Bu dizinleyiciden yakalamak için hangi indirme istemcisinin kullanılacağını belirtin", "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", "LastDuration": "Yürütme Süresi", "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", @@ -557,9 +557,9 @@ "RemoveFailedDownloads": "Başarısız İndirmeleri Kaldır", "Scheduled": "Planlı", "Underscore": "Vurgula", - "SetIndexerFlags": "Dizin Oluşturucu Bayraklarını Ayarla", + "SetIndexerFlags": "Dizinleyici Bayraklarını Ayarla", "SetReleaseGroup": "Yayımlama Grubunu Ayarla", - "SetIndexerFlagsModalTitle": "{modalTitle} - Dizin Oluşturucu Bayraklarını Ayarla", + "SetIndexerFlagsModalTitle": "{modalTitle} - Dizinleyici Bayraklarını Ayarla", "SslCertPassword": "SSL Sertifika Şifresi", "Rating": "Puan", "GrabRelease": "Yayın Yakalama", @@ -706,7 +706,7 @@ "PreviouslyInstalled": "Önceden Yüklenmiş", "QualityCutoffNotMet": "Kalite sınırı karşılanmadı", "QueueIsEmpty": "Kuyruk boş", - "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizin oluşturucunun ayarlanması, bu profilin yalnızca söz konusu dizin oluşturucunun sürümlerine uygulanmasına neden olur.", + "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizinleyicinin ayarlanması, bu profilin yalnızca söz konusu dizinleyicinin yayınlarına uygulanmasına neden olur.", "ResetDefinitionTitlesHelpText": "Değerlerin yanı sıra tanım başlıklarını da sıfırlayın", "SecretToken": "Gizlilik Jetonu", "SetReleaseGroupModalTitle": "{modalTitle} - Yayımlama Grubunu Ayarla", @@ -722,7 +722,7 @@ "Uptime": "Çalışma süresi", "RemotePath": "Uzak Yol", "File": "Dosya", - "ReleaseProfileIndexerHelpText": "Profilin hangi dizin oluşturucuya uygulanacağını belirtin", + "ReleaseProfileIndexerHelpText": "Profilin hangi dizinleyiciye uygulanacağını belirtin", "TablePageSize": "Sayfa Boyutu", "NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil", "NotificationsTwitterSettingsAccessTokenSecret": "Erişim Jetonu Gizliliği", @@ -738,7 +738,7 @@ "RemoveQueueItemRemovalMethod": "Kaldırma Yöntemi", "RemoveQueueItemRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirme işlemini ve dosyaları indirme istemcisinden kaldıracaktır.", "RemoveSelectedItems": "Seçili öğeleri kaldır", - "SelectIndexerFlags": "Dizin Oluşturucu Bayraklarını Seçin", + "SelectIndexerFlags": "Dizinleyici Bayraklarını Seçin", "Started": "Başlatıldı", "Size": "Boyut", "SupportedCustomConditions": "{appName}, aşağıdaki yayın özelliklerine göre özel koşulları destekler.", @@ -792,5 +792,15 @@ "Logging": "Loglama", "MinutesSixty": "60 Dakika: {sixty}", "SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin", - "Repack": "Yeniden paketle" + "Repack": "Yeniden paketle", + "IndexerFlags": "Dizinleyici Bayrakları", + "Indexer": "Dizinleyici", + "Indexers": "Dizinleyiciler", + "IndexerPriority": "Dizinleyici Önceliği", + "IndexerOptionsLoadError": "Dizinleyici seçenekleri yüklenemiyor", + "IndexerSettings": "Dizinleyici Ayarları", + "MustContain": "İçermeli", + "MustNotContain": "İçermemeli", + "RssSyncIntervalHelpTextWarning": "Bu, tüm dizinleyiciler için geçerli olacaktır, lütfen onlar tarafından belirlenen kurallara uyun", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index a514c1313..00bfc0bbb 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -104,5 +104,246 @@ "AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.", "AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм", "AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль", - "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача" + "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача", + "DeleteSelectedEpisodeFiles": "Видалити вибрані файли серій", + "DownloadClientQbittorrentTorrentStateUnknown": "Невідомий стан завантаження: {state}", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не намагатиметься імпортувати завершені завантаження без категорії.", + "AddedDate": "Додано: {date}", + "AnEpisodeIsDownloading": "Виконується завантаження серії", + "Blocklist": "Чорний список", + "CalendarFeed": "Стрічка календаря {appName}", + "CancelProcessing": "Скасувати обробку", + "ChmodFolderHelpTextWarning": "Це працює лише в тому випадку, якщо власником файлу є користувач, на якому працює {appName}. Краще переконатися, що клієнт завантаження правильно встановлює дозволи.", + "ChownGroupHelpTextWarning": "Це працює, лише якщо користувач, який запускає {appName}, є власником файлу. Краще переконатися, що клієнт завантаження використовує ту саму групу, що й {appName}.", + "ClickToChangeSeries": "Натисніть, щоб змінити серіал", + "ConnectSettings": "Налаштування підключення", + "ConnectionLostReconnect": "{appName} спробує підключитися автоматично, або ви можете натиснути «Перезавантажити» нижче.", + "ContinuingOnly": "Тільки продовження", + "CustomFormatJson": "Спеціальний формат JSON", + "DeleteSelectedEpisodeFilesHelpText": "Ви впевнені, що бажаєте видалити вибрані файли серії?", + "DeleteSeriesFolderCountWithFilesConfirmation": "Ви впевнені, що хочете видалити {count} вибраних серіалів і весь вміст?", + "DeleteSeriesFolderCountConfirmation": "Ви впевнені, що хочете видалити {count} вибраних серіалів?", + "DeletedSeriesDescription": "Серіал видалено з TheTVDB", + "DeleteSpecificationHelpText": "Ви впевнені, що хочете видалити специфікацію '{name}'?", + "DoNotPrefer": "Не віддавати перевагу", + "Disabled": "Вимкнено", + "Added": "Додано", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Підтвердити новий пароль", + "CancelPendingTask": "Ви впевнені, що хочете скасувати це незавершене завдання?", + "Cancel": "Скасувати", + "ChownGroupHelpText": "Назва групи або gid. Використовуйте gid для віддалених файлових систем.", + "ClickToChangeQuality": "Натисніть, щоб змінити якість", + "ChooseImportMode": "Виберіть режим імпорту", + "Close": "Закрити", + "ClickToChangeSeason": "Натисніть, щоб змінити сезон", + "ConditionUsingRegularExpressions": "Ця умова відповідає використанню регулярних виразів. Зауважте, що символи `\\^$.|?*+()[{` мають спеціальні значення та потребують екранування за допомогою `\\`", + "ConnectionLostToBackend": "{appName} втратив з’єднання з серверною частиною, і його потрібно перезавантажити, щоб відновити роботу.", + "ConnectionLost": "Зв'язок втрачений", + "ContinuingSeriesDescription": "Більше серій/очікується ще один сезон", + "CreateGroup": "Створити групу", + "CustomFormatUnknownConditionOption": "Невідомий параметр '{key}' для умови '{implementation}'", + "Default": "За замовчуванням", + "DelayProfile": "Профіль затримки", + "DelayMinutes": "{delay} Хвилин", + "DeleteEpisodeFile": "Видалити файл серії", + "DeleteEpisodeFileMessage": "Ви впевнені, що хочете видалити '{path}'?", + "DeleteIndexer": "Видалити індексатор", + "DeleteIndexerMessageText": "Ви впевнені, що хочете видалити індексатор \"{name}\"?", + "DeleteSelectedSeries": "Видалити вибраний серіал", + "DeleteSeriesFolderHelpText": "Видалити папку серіалу та її вміст", + "DeleteTag": "Видалити тег", + "DeleteSeriesModalHeader": "Видалити - {title}", + "DeleteSpecification": "Видалити специфікацію", + "DeletedReasonManual": "Файл було видалено за допомогою {appName} вручну або іншим інструментом через API", + "DestinationPath": "Шлях призначення", + "DetailedProgressBar": "Детальний індикатор прогресу", + "DetailedProgressBarHelpText": "Показати текст на панелі виконання", + "DisabledForLocalAddresses": "Вимкнено для локальних адрес", + "Discord": "Discord", + "DoNotUpgradeAutomatically": "Не оновлювати автоматично", + "DiskSpace": "Дисковий простір", + "DoneEditingGroups": "Редагування груп завершено", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Категорія не підтримується", + "DeleteImportList": "Видалити список імпорту", + "DeleteReleaseProfile": "Видалити профіль випуску", + "DeleteCustomFormatMessageText": "Ви впевнені, що хочете видалити спеціальний формат \"{name}\"?", + "DeleteRootFolder": "Видалити кореневу папку", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категорії не підтримуються до qBittorrent версії 3.3.0. Оновіть або повторіть спробу з пустою категорією.", + "DownloadClientRootFolderHealthCheckMessage": "Клієнт завантаження {downloadClientName} розміщує завантаження в кореневій папці {rootFolderPath}. Ви не повинні завантажувати в кореневу папку.", + "CopyToClipboard": "Копіювати в буфер обміну", + "DeleteQualityProfile": "Видалити профіль якості", + "Calendar": "Календар", + "Daily": "Щодня", + "CustomFormatScore": "Оцінка спеціального формату", + "DotNetVersion": ".NET", + "Download": "Завантажити", + "DownloadClient": "Клієнт завантажувача", + "Component": "Компонент", + "AuthenticationMethodHelpText": "Вимагати ім’я користувача та пароль для доступу до {appName}", + "AuthenticationRequiredHelpText": "Змінити запити, для яких потрібна автентифікація. Не змінюйте, якщо не розумієте ризики.", + "BuiltIn": "Вбудований", + "ChangeFileDate": "Змінити дату файлу", + "ChooseAnotherFolder": "Виберіть іншу папку", + "CloneAutoTag": "Клонувати автоматичний тег", + "CollectionsLoadError": "Не вдалося завантажити колекції", + "CountSeasons": "{count} Сезонів", + "CustomFormatsLoadError": "Не вдалося завантажити спеціальні формати", + "DelayProfilesLoadError": "Неможливо завантажити профілі затримки", + "DelayProfileProtocol": "Протокол: {preferredProtocol}", + "DeleteAutoTag": "Видалити автоматичний тег", + "DeleteAutoTagHelpText": "Ви впевнені, що хочете видалити автоматичний тег \"{name}\"?", + "DeleteDelayProfile": "Видалити профіль затримки", + "DeleteDelayProfileMessageText": "Ви впевнені, що хочете видалити цей профіль затримки?", + "DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{name}'?", + "DeleteSelectedImportLists": "Видалити списки імпорту", + "Deleted": "Видалено", + "DeleteTagMessageText": "Ви впевнені, що хочете видалити тег '{label}'?", + "DestinationRelativePath": "Відносний шлях призначення", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "Немає доступного клієнта для завантаження", + "Connections": "З'єднання", + "Docker": "Docker", + "AddingTag": "Додавання тега", + "AutomaticSearch": "Автоматичний пошук", + "Backup": "Резервне копіювання", + "Automatic": "Автоматично", + "BackupFolderHelpText": "Відносні шляхи будуть у каталозі AppData {appName}", + "CalendarLoadError": "Неможливо завантажити календар", + "ConnectionSettingsUrlBaseHelpText": "Додає префікс до URL-адреси {connectionName}, наприклад {url}", + "CustomFormatUnknownCondition": "Невідома умова спеціального формату '{implementation}'", + "DelayingDownloadUntil": "Завантаження відкладається до {date} о {time}", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} не зміг додати мітку до qBittorrent.", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent повідомляє про помилку", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent завантажує метадані", + "Connection": "Підключення", + "Continuing": "Продовження", + "Database": "База даних", + "AddSeriesWithTitle": "Додати {title}", + "AddToDownloadQueue": "Додати до черги завантажень", + "CalendarLegendEpisodeDownloadingTooltip": "Серія зараз завантажується", + "CalendarLegendEpisodeUnmonitoredTooltip": "Серія не відстежується", + "CalendarLegendEpisodeMissingTooltip": "Серія вийшла в ефір і відсутній на диску", + "CalendarLegendEpisodeUnairedTooltip": "Серія ще не вийшла в ефір", + "CalendarLegendSeriesFinaleTooltip": "Фінал серіалу або сезону", + "CalendarLegendSeriesPremiereTooltip": "Прем'єра серіалу або сезону", + "Category": "Категорія", + "CertificateValidation": "Перевірка сертифіката", + "CertificateValidationHelpText": "Змініть суворість перевірки сертифікації HTTPS. Не змінюйте, якщо не розумієте ризики.", + "ChangeCategory": "Змінити категорію", + "BlackholeFolderHelpText": "Папка, у якій {appName} зберігатиме файл {extension}", + "ChangeFileDateHelpText": "Змінити дату файлу під час імпорту/повторного сканування", + "Clear": "Очистити", + "ClearBlocklist": "Очистити список блокувань", + "CloneProfile": "Клонувати профіль", + "DeleteBackup": "Видалити резервну копію", + "DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію \"{name}\"?", + "DeleteCustomFormat": "Видалити спеціальний формат", + "DeleteDownloadClient": "Видалити клієнт завантаження", + "DeleteEmptyFolders": "Видалити порожні папки", + "DeleteImportListMessageText": "Ви впевнені, що хочете видалити список '{name}'?", + "DeleteEpisodesFilesHelpText": "Видалити файли серій і папку серіалів", + "DeleteEpisodesFiles": "Видалити файли епізодів ({episodeFileCount}).", + "Details": "Подробиці", + "DeleteSelectedIndexers": "Видалити індексатор(и)", + "DownloadClientDelugeTorrentStateError": "Deluge повідомляє про помилку", + "DownloadClientDelugeValidationLabelPluginFailure": "Помилка налаштування мітки", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} не зміг додати мітку до {clientName}.", + "DownloadClientDownloadStationProviderMessage": "{appName} не може підключитися до Download Station, якщо у вашому обліковому записі DSM увімкнено 2-факторну автентифікацію", + "DownloadClientDownloadStationValidationApiVersion": "Версія Download Station API не підтримується, має бути принаймні {requiredVersion}. Він підтримує від {minVersion} до {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "Папка '{downloadDir}' не існує, її потрібно створити вручну в спільній папці '{sharedFolder}'.", + "DownloadClientDownloadStationValidationFolderMissing": "Папка не існує", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Ви повинні ввійти до свого Diskstation як {username} та вручну налаштувати його в налаштуваннях DownloadStation у розділі BT/HTTP/FTP/NZB -> Місцезнаходження.", + "DownloadClientFloodSettingsAdditionalTags": "Додаткові теги", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Спільна папка не існує", + "DownloadClientFloodSettingsRemovalInfo": "{appName} оброблятиме автоматичне видалення торрентів на основі поточних критеріїв заповнення в меню «Налаштування» -> «Індексатори»", + "AutoTaggingSpecificationTag": "Тег", + "BlocklistAndSearch": "Чорний список і пошук", + "CalendarLegendEpisodeDownloadedTooltip": "Серія завантажена та відсортована", + "ClickToChangeReleaseType": "Натисніть, щоб змінити тип випуску", + "CustomFormatsSpecificationRegularExpressionHelpText": "Спеціальний формат RegEx не враховує регістр", + "DailyEpisodeFormat": "Формат щоденних серій", + "DailyEpisodeTypeDescription": "Серії, що випускаються щодня або рідше, у яких використовується рік-місяць-день (2023-08-04)", + "DailyEpisodeTypeFormat": "Дата ({format})", + "DatabaseMigration": "Міграція бази даних", + "Dates": "Дати", + "Day": "День", + "DeleteEmptySeriesFoldersHelpText": "Видаліть порожні папки серіалів і сезонів під час сканування диска та під час видалення файлів серій", + "DeleteEpisodeFromDisk": "Видалити серію з диска", + "DeleteNotification": "Видалити сповіщення", + "DeleteNotificationMessageText": "Ви впевнені, що хочете видалити сповіщення '{name}'?", + "DeleteQualityProfileMessageText": "Ви впевнені, що хочете видалити профіль якості '{name}'?", + "DownloadClientOptionsLoadError": "Не вдалося завантажити параметри клієнта завантаження", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Пріоритет для використання під час захоплення серій, які вийшли в ефір протягом останніх 14 днів", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Чи використовувати налаштований макет вмісту qBittorrent, оригінальний макет із торрента чи завжди створювати вкладену папку (qBittorrent 4.3.2+)", + "DownloadClientSettingsInitialStateHelpText": "Початковий стан для торрентів, доданих до {clientName}", + "CalendarLegendEpisodeOnAirTooltip": "Серія зараз в ефірі", + "AutoRedownloadFailed": "Помилка повторного завантаження", + "AutoRedownloadFailedFromInteractiveSearch": "Помилка повторного завантаження з інтерактивного пошуку", + "BindAddress": "Прив'язати адресу", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Мінімальна оцінка спеціального формату", + "CheckDownloadClientForDetails": "перевірте клієнт завантаження, щоб дізнатися більше", + "BackupsLoadError": "Не вдалося завантажити резервні копії", + "Date": "Дата", + "AutoTaggingSpecificationStatus": "Статус", + "AutoTaggingSpecificationGenre": "Жанр(и)", + "AutoTaggingSpecificationMaximumYear": "Максимальний рік", + "BlackholeWatchFolderHelpText": "Папка, з якої {appName} має імпортувати завершені завантаження", + "BranchUpdate": "Гілка для оновлення {appName}", + "BrowserReloadRequired": "Потрібно перезавантажити браузер", + "AutoTaggingSpecificationMinimumYear": "Мінімальний рік", + "AutoTaggingSpecificationOriginalLanguage": "Мова", + "AutoTaggingSpecificationQualityProfile": "Профіль Якості", + "CouldNotFindResults": "Не вдалося знайти жодних результатів для '{term}'", + "CustomFormatsSpecificationLanguage": "Мова", + "CustomFormatsSpecificationMaximumSize": "Максимальний розмір", + "CustomFormatsSpecificationReleaseGroup": "Група випуску", + "CustomFormatsSpecificationResolution": "Роздільна здатність", + "CustomFormatsSpecificationSource": "Джерело", + "DefaultNotFoundMessage": "Ви, мабуть, заблукали, тут нічого не видно.", + "DeleteSeriesFolder": "Видалити папку серіалу", + "DeleteSeriesFolderConfirmation": "Папку серіалу `{path}` і весь її вміст буде видалено.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Додає префікс до API Flood, наприклад {url}", + "DownloadClientFreeboxApiError": "API Freebox повернув помилку: {errorDescription}", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientSettings": "Налаштування клієнта завантаження", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Пріоритет для використання під час захоплення серій, які вийшли в ефір понад 14 днів тому", + "AddRootFolderError": "Не вдалося додати кореневу папку", + "AppUpdatedVersion": "{appName} оновлено до версії `{version}`, щоб отримати останні зміни, вам потрібно перезавантажити {appName} ", + "Backups": "Резервні копії", + "AutoRedownloadFailedHelpText": "Автоматичний пошук і спроба завантажити інший випуск", + "BackupIntervalHelpText": "Інтервал між автоматичним резервним копіюванням", + "BeforeUpdate": "Перед оновленням", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation не має спільної папки з назвою '{sharedFolder}', ви впевнені, що вказали її правильно?", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Використовувати безпечне з'єднання. Дивіться параметри -> Web UI -> 'Use HTTPS instead of HTTP' в qBittorrent.", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Спочатку завантажте першу та останню частини (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Завантаження в послідовному порядку (qBittorrent 4.1.0+)", + "CustomFormatsSettingsTriggerInfo": "Спеціальний формат буде застосовано до випуску або файлу, якщо він відповідає принаймні одному з кожного з різних типів вибраних умов.", + "CustomFormatsSpecificationMinimumSize": "Мінімальний розмір", + "CustomFormatsSpecificationMaximumSizeHelpText": "Випуск повинен бути меншим або дорівнювати цьому розміру", + "CustomFormatsSpecificationMinimumSizeHelpText": "Випуск повинен бути більше цього розміру", + "Delete": "Видалити", + "DeleteReleaseProfileMessageText": "Ви впевнені, що хочете видалити цей профіль випуску '{name}'?", + "DeleteRootFolderMessageText": "Ви впевнені, що бажаєте видалити кореневу папку '{path}'?", + "DeleteSelectedDownloadClients": "Видалити клієнт(и) завантаження", + "DockerUpdater": "Оновіть контейнер docker, щоб отримати оновлення", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Помилка налаштування категорії", + "ClearBlocklistMessageText": "Ви впевнені, що бажаєте очистити всі елементи зі списку блокування?", + "ClickToChangeEpisode": "Натисніть, щоб змінити серію", + "ClickToChangeLanguage": "Натисніть, щоб змінити мову", + "CollapseAll": "Закрити все", + "ConnectSettingsSummary": "Сповіщення, підключення до медіасерверів/програвачів і спеціальні сценарії", + "CustomFormatsSettings": "Налаштування спеціальних форматів", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Необов’язкове розташування для переміщення завершених завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Необов’язкове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Deluge", + "DownloadClientSettingsCategorySubFolderHelpText": "Додавання спеціальної категорії для {appName} дозволяє уникнути конфліктів із непов’язаними завантаженнями, не пов’язаними з {appName}. Використання категорії необов’язкове, але настійно рекомендується. Створює підкаталог [category] у вихідному каталозі.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Категорія рекомендована", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent налаштовано на видалення торрентів, коли вони досягають ліміту коефіцієнта спільного використання", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent повідомляє про відсутність файлів", + "DownloadClientSettingsCategoryHelpText": "Додавання спеціальної категорії для {appName} дозволяє уникнути конфліктів із непов’язаними завантаженнями, не пов’язаними з {appName}. Використання категорії необов’язкове, але настійно рекомендується.", + "Donations": "Пожертви", + "DownloadClientSeriesTagHelpText": "Використовуйте цей клієнт завантаження лише для серіалів із принаймні одним відповідним тегом. Залиште поле порожнім для використання з усіма серіалами.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Не вдалося зв’язатися з {downloadClientName}. {errorMessage}", + "DownloadClientDelugeValidationLabelPluginInactive": "Плагін міток не активовано", + "DownloadClientAriaSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2", + "CustomFormatHelpText": "{appName} оцінює кожен випуск, використовуючи суму балів для відповідності користувацьких форматів. Якщо новий випуск покращить оцінку, з такою ж або кращою якістю, {appName} схопить його.", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station" } From 3940059ea3325f334e7693b02036ce5137e7f1bc Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Thu, 9 May 2024 01:46:59 +0000 Subject: [PATCH 289/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 4492f1d6a..83c0e8740 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -413,6 +413,27 @@ "schema": { "$ref": "#/components/schemas/SortDirection" } + }, + { + "name": "seriesIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "protocols", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadProtocol" + } + } } ], "responses": { From cac7d239ea6362c5d1f0c0a632ca097ff932dcda Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 8 May 2024 18:50:11 -0700 Subject: [PATCH 290/762] Fixed: Parsing of partial season pack --- src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 65b093c33..4d81e1e74 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)] [TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)] + [TestCase("The.Series.S06.P1.1080p.Blu-Ray.10-Bit.Dual-Audio.TrueHD.x265-iAHD", "The Series", 6, 1)] public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 74f2f4a34..fb483a4a2 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -215,7 +215,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Partial season pack - new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)", + new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e|p)(?<seasonpart>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - 4 digit absolute episode number From 5cb649e9d86629e82c36c80ee7a599d24da1d603 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 8 May 2024 18:53:19 -0700 Subject: [PATCH 291/762] Fixed: Attempt to parse and reject ambiguous dates Closes #6799 --- .../ParserTests/DailyEpisodeParserFixture.cs | 9 ++++ src/NzbDrone.Core/Parser/Parser.cs | 45 ++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 6b72c48c8..6107308b5 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("2019_08_20_1080_all.mp4", "", 2019, 8, 20)] [TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)] [TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)] + [TestCase("Series Title - 30-04-2024 HDTV 1080p H264 AAC", "Series Title", 2024, 4, 30)] // [TestCase("", "", 0, 0, 0)] public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) @@ -100,5 +101,13 @@ namespace NzbDrone.Core.Test.ParserTests Parser.Parser.ParseTitle(title).Should().BeNull(); } + + [TestCase("Tmc - Quotidien - 05-06-2024 HDTV 1080p H264 AAC")] + + // [TestCase("", "", 0, 0, 0)] + public void should_not_parse_ambiguous_daily_episode(string postTitle) + { + Parser.Parser.ParseTitle(postTitle).Should().BeNull(); + } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index fb483a4a2..9fbe43b7a 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -339,7 +339,7 @@ namespace NzbDrone.Core.Parser // 4 digit episode number // Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)", + new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]|\d{1,2}-))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with airdate (2018.04.28) @@ -350,7 +350,11 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[_. ](?<absoluteepisode>\d{1,4})(?:[_. ]+)(?:BLM|B[oö]l[uü]m)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with airdate (04.28.2018) - new Regex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)", + new Regex(@"^(?<title>.+?)?\W*(?<ambiguousairmonth>[0-1][0-9])[-_. ]+(?<ambiguousairday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + // Episodes with airdate (28.04.2018) + new Regex(@"^(?<title>.+?)?\W*(?<ambiguousairday>[0-3][0-9])[-_. ]+(?<ambiguousairmonth>[0-1][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with airdate (20180428) @@ -362,7 +366,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Supports 1103/1113 naming - new Regex(@"^(?<title>.+?)?(?:(?:[-_. ](?<![()\[!]))*(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])(?!p|i|\d+|\)|\]|\W\d+|\W(?:e|ep|x)\d+)))+([-_. ]+|$)(?!\\)", + new Regex(@"^(?<title>.+?)?(?:(?:[-_. ](?<![()\[!]))*(?<!\d{1,2}-)(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])(?!p|i|\d+|\)|\]|\W\d+|\W(?:e|ep|x)\d+)))+([-_. ]+|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Dutch/Flemish release titles @@ -1128,15 +1132,36 @@ namespace NzbDrone.Core.Parser else { // Try to Parse as a daily show - var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); - var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); - // Swap day and month if month is bigger than 12 (scene fail) - if (airmonth > 12) + var airmonth = 0; + var airday = 0; + + if (matchCollection[0].Groups["ambiguousairmonth"].Success && + matchCollection[0].Groups["ambiguousairday"].Success) { - var tempDay = airday; - airday = airmonth; - airmonth = tempDay; + var ambiguousAirMonth = Convert.ToInt32(matchCollection[0].Groups["ambiguousairmonth"].Value); + var ambiguousAirDay = Convert.ToInt32(matchCollection[0].Groups["ambiguousairday"].Value); + + if (ambiguousAirDay <= 12 && ambiguousAirMonth <= 12) + { + throw new InvalidDateException("Ambiguous Date, cannot validate month and day with {0} and {1}", ambiguousAirMonth, ambiguousAirDay); + } + + airmonth = ambiguousAirMonth; + airday = ambiguousAirDay; + } + else + { + airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); + airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); + + // Swap day and month if month is bigger than 12 (scene fail) + if (airmonth > 12) + { + var tempDay = airday; + airday = airmonth; + airmonth = tempDay; + } } DateTime airDate; From 429444d0857c0929b1fc2f4a1f76815c4d10b275 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 9 May 2024 20:58:01 +0300 Subject: [PATCH 292/762] Fixed: Text color for inputs on login page --- frontend/src/login.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/login.html b/frontend/src/login.html index db7262276..8ec1aef51 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -116,6 +116,7 @@ border: 1px solid var(--inputBorderColor); border-radius: 4px; box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); + color: var(--textColor); } .form-input:focus { @@ -296,7 +297,7 @@ var light = { white: '#fff', pageBackground: '#f5f7fa', - textColor: '#656565', + textColor: '#515253', themeDarkColor: '#3a3f51', panelBackground: '#fff', inputBackgroundColor: '#fff', @@ -316,7 +317,7 @@ var dark = { white: '#fff', pageBackground: '#202020', - textColor: '#656565', + textColor: '#ccc', themeDarkColor: '#494949', panelBackground: '#111', inputBackgroundColor: '#333', From c7c1e3ac9e5bffd4d92298fed70916e3808613fd Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 9 Dec 2023 17:21:46 +0200 Subject: [PATCH 293/762] Refactor PasswordInput to use type password --- frontend/src/Components/Form/PasswordInput.css | 5 ----- .../src/Components/Form/PasswordInput.css.d.ts | 7 ------- frontend/src/Components/Form/PasswordInput.js | 9 ++------- frontend/src/Content/Fonts/fonts.css | 11 ----------- .../src/Content/Fonts/text-security-disc.ttf | Bin 12392 -> 0 bytes .../src/Content/Fonts/text-security-disc.woff | Bin 2988 -> 0 bytes frontend/src/Styles/Variables/fonts.js | 1 - 7 files changed, 2 insertions(+), 31 deletions(-) delete mode 100644 frontend/src/Components/Form/PasswordInput.css delete mode 100644 frontend/src/Components/Form/PasswordInput.css.d.ts delete mode 100644 frontend/src/Content/Fonts/text-security-disc.ttf delete mode 100644 frontend/src/Content/Fonts/text-security-disc.woff diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css deleted file mode 100644 index 6cb162784..000000000 --- a/frontend/src/Components/Form/PasswordInput.css +++ /dev/null @@ -1,5 +0,0 @@ -.input { - composes: input from '~Components/Form/TextInput.css'; - - font-family: $passwordFamily; -} diff --git a/frontend/src/Components/Form/PasswordInput.css.d.ts b/frontend/src/Components/Form/PasswordInput.css.d.ts deleted file mode 100644 index 774807ef4..000000000 --- a/frontend/src/Components/Form/PasswordInput.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'input': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js index fef54fd5a..dbc4cfdb4 100644 --- a/frontend/src/Components/Form/PasswordInput.js +++ b/frontend/src/Components/Form/PasswordInput.js @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; import React from 'react'; import TextInput from './TextInput'; -import styles from './PasswordInput.css'; // Prevent a user from copying (or cutting) the password from the input function onCopy(e) { @@ -13,17 +11,14 @@ function PasswordInput(props) { return ( <TextInput {...props} + type="password" onCopy={onCopy} /> ); } PasswordInput.propTypes = { - className: PropTypes.string.isRequired -}; - -PasswordInput.defaultProps = { - className: styles.input + ...TextInput.props }; export default PasswordInput; diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index bf31501dd..e0f1bf5dc 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,14 +25,3 @@ font-family: 'Ubuntu Mono'; src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype'); } - -/* - * text-security-disc - */ - -@font-face { - font-weight: normal; - font-style: normal; - font-family: 'text-security-disc'; - src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype'); -} diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf deleted file mode 100644 index 86038dba89d7e762d5dbc458529c0d20514a3d54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12392 zcmeI&XLMAB8inCAlaL@Npi=CPbVNc^Py|H90E$WzyBNZR1k+3g1Z=3N*bsYf*n1ZX z_Kpn|d+!agcZK`zJs)@J)gS)9B<r31%$Z^`xK;*ol2no;&68P4LDFm3*xk0voX}}z zlBBle=(t_S_Uuz$TD}$6PvrXe@|v>x$`8tR<NDGh={|9C^^A%+A9r{?NqX$T?V~EQ zWs_PwI(i&_m-<z5pxH?U9q>ZxSXq;sUXVda>O(G?R@ar6C0%vB$0#m}YRab9r&=WK zxqTh3XKKr8vh{B-SitR9B}sa2eO+TNJ3CjEB)#<Yl_Y%a#fwprf7bLu9r?>g+DABg z{Es90PG~vshopJ_5id`jy`k>8a?=Y7cjju-Bpr{}Go_QZaa~87@tGtm{&ah@q*pRH zNe?X@IWB2YU6!j&)=LWibo-y3s;-(`#`E*dxkBGF#gSA}wDM?bUTR*_glpL*)BP%V z3m!-&rg^S((xiaf{^4U(#SW8_A>3nyzq{M=-{YTOjr=R*=hky<Mm%HjnEZW;M)K#c zuWK`w?~>z%MH%0xP{-3NR`~n&Wh%b%6t}GUT=fd9dIeU!0;^tu|F>5lN%2!F^?&}6 ztj^EKq+=^x@-8Gl`OExI`ISHI^}{=#j_Zy})_Vjc>nFdG^`2nm`+{PubX@NsR=$f^ z4J#ejdlMx$k7Rv?N?s$9TSRipNY?kF<TWFCtw>%wl3Pb|n@DaO$?YOp@9dPkZX~Z4 z$?He*29d0HY)alJk~fazOeAj-$?YS#LnL>M<W7;?Ig-0Xa@R=i7Rj4Na`#BqJ1r&m zh~$z;?itCwBDr@Y_le}bk-T{%ZxPA;BDsGg>z%Wbd1vqB0g=3QBoB<_L6JN-lDCQE zA(6aoByShV+eh-yNZuimOCxz$BoB||5s|!OB<~c-BO_Vw@RhtvB<~u@qat~9B#(*Y zv5~C5?I@YQ898};B<~){dqnb{k-S$V?;XkeMDo6oyk8_wh~)hvxh#?=Msj&1Pm1Jh zBv(Z8<VdcJ<f=%X63GWda&;uvL~?B;*F|!DBp(>b4Uya!$+<|L8p+cl`JhOi9?3Hz zd1fRZ9La}7@}ZG@SR@}F$+IH)h)6y%l8=hy*^zv7Bp(yW$42s;NIou-kB{UNBKgEf zJ}HvtM)Jv#JTH<@iR4ow`LswrJ(ACe<TE4rtVljPlFy0cb0hh@NIpN3FNov|Bl)68 zzBrOEiRAf_d}$<K7Ri@K@)ePMWh7q}$yZ16HIaO6BwrWF*GKXVk$ht$-xSF=NAfL^ zd}}1%7Rk3q@*R=9Ad>Hl<hvsI?nu5Tk{3quy^(xhB;Oy&4@B~Vk^E33KOD)AMDnAN z{8%JE9?4Hc@{^IgD3YIw<fkL~nMi&%lAnv@#gY7cB)<^JFGli`NPa1jUykHgBKg%w zel3z;kK{KZ`OQdvE0W)i<aZ+Z-AH~flHZTy4<h-)Nd732KaS*2BKgxu{w$I|kK`{R zd1)kn8Oh5c`Kw6&I+DMM<ZmPSyGZ^%l7EQgA0zpvNd7sJe~IK@Bl)*T{ymcah~(vw zyduK7s&tWx>@?TOE?~$mWXNv9kX^)(UCfZ(lp%XHhU{hx+07ZUS7*pxgCV;GLv~As z>{blfYcgc7#gM%=Lw0M1>^2P9Z5guLF=Vg9ki9NL_IeE2>oa6;z>vKmL-s}t*&8!t zXBe_KVaRUJklleHyCXw(Cx-0K4B1^6vb!>5cVo!jlp(u2L-uA2**zGtOBk|yGGzB+ z$nMRM-G?E&FGKd`4B1;SWcOpp?$40DB}4XB4A}!1vbSc)9>|bAh#`A0L-sZd*+Uqz zw`IuQjv;${hU}pX**h>~moj7zW5^!PkUfGSdq;-sofxu5GGy<}ki82-_O1-sqZqPB zGh~lp$R5j(J&qxJH-_x-4B5LgWbeU{y(dHVUJTiLGi2|>ki9QM_I?c66Bx4hXUHyN z$ezfMUCxj_i6J}7kX^x$J((f9k|Dc_A$tl#_5lpp)ePA+4B52|*>w!r^$ghuGGsR} zWH&No=NPi5GGtF<$UcZ6dpbk*42JBP4A}=WWFNwieJDfrVGP-aGi1+V$UcH0`$&fD zqZqPhGh`plkbMk8_OT4va~QIZW5_<9A^QY|>=PNXPh!ZP%aDCCL-ss|>{A%BPi4qH zjUoGVhU_yKvd?75K8qpyY=-P}7_!f0$UcuD`+SD%3mCF5WXQgVA^T#6>`NH3=QCtq z%8-2-L-yqi*;g=RU&)Yt6+`yb4B6K(WM9jWeH}yg^$giJFl67zkbM(F_RS30w=iVi z%8-2<L-y?q*>^BxFJQ>NlOg*qhU~i;vhQKYUdWJrFGKcy4B7WHWIw=={UAg3Lk!su zGh{!)ko_n__G1j$k27RH!I1qVL-rzu?57yApJvE@h9Ub|hV17UvKKRCKhKc;0z>wT z4B1N<vR`7zewiWr6^86r8M0qv$bOw6`wfQdHyN_uV#t1*A^RPM>~|Tm-($#rpCS7L zhU^a+vOi+T{+J>A6Nc<h8L~fP$o`xm`wNEbr3~3$GGs4f$o`5U`)h{mZy2(_Wyt=H zA^UrV>>n7ie`Lu1i6Q%EhU{M$vVUdB{*58~cZTdg7_yf$WUpXIPBY5Zb+S2_)(tvV zkk2l}WH-TN7h$rCG1*Nq*{fl)n_;q>W3pGrWUqnAZh^^eiOFt-$zBtay%r{WZA^A+ zOm-Vgc3Vt#J52UEnCx{i+3R7l*T-aUfXUtvlf4lpdt*#?29v!BCc8Z*y8|Y>BPP2O zCc85xy9*|}D<-=eCVNv%c6UtnW|-_AnCucvc27)pFHClCOm-hkc3({P=9ug)FxmYu z+5IuuTVk@e!ekG?WN(eh9*D^vgvlO^$=(K&Jp_}zEhc+AO!oGe?4g+K9WdFYnCxMg z?BST~5t!^9G1)s|vPWXFcgAGzg2~<$lRXNPJsOie29rG&lRXZTy&EQbJSKa0O!gj_ z>^(8rdttKo#$@k<$=(-}y&oof0w#NZOm-P2dm<*g9Fsi>lbyw6S75RyW3nqT*;SbA zDVXd7Fxl0Z>>5mVEhf7TlU<L=J`j`LfXQyeWalv1Q!&}oFxdxTvZrIRXJE2tVzLj$ zWFLabJ`|IE7$*C0O!h2H_7RxuBQe=WVX|jqvX91OAA`w07Lz>(lYJZ}`*=+D37G5? zG1(_!vgcy5PsU`=!(^X=$vzd6eHteFbWHXcnCvq#*=J$0&&Fh*gULP@lYJg0`+Q9H z1(@s$G1(VkvM<JDUxLY=kIB9ilYJQ``*KY76`1TRG1*sPvaiNuUxUfM7L$D)Ci{9! z_6?Zq8!_28VX|+=WZ#0xz7>;w8z%d9O!gg^>;;(YJ2BaJVY2VWWZ#3yUWm!Q7n6M- zCi{L&_5+yg2Qk?XVX_~_WIuw*eiW1a7$*C1O!gC)>?bkVi!j+wVX~jbWIuz+eioDc z9431)Ci{6z_6wNo7ctpOFxf9*vR}qzzk<nr6_foMCi`_v_8XY&H!<07VY1)GWWR&S zeixJd9wz&JO!fzu><=;7A7QdT#$<nj$^I0R{TU|vb4>OZnCzvP>@P9d%P`qrVY0u* zWPgLn{uYz{9VYvGO!g0$>>n}NKVh<e#$^A3$^I3S{Tn9xcTDylnC#`4>=l?~e{|(C zEyY>IZMbQZ*6rH1Yul%PzyAF;@6x$T=k~=##YL%k%a?TPI<$0t>Cmp-{)>Xl=)YXo z4Ln&ZekYdyFQj}nPTuLHm}_|#pmoEAxO=z>X9Nuj7jb-exEc4@E8Lv3dX7#w3%GDK zj&$Vrw0WmFQ)pqhfa8n8g?L%GNz#_z4dm}%lyv0Z$MbHMjOX88^KPDWPR-9{r{{V! zX3M8GROM#$m{iqRJ|;VPYIRw|-*5Q)k@4Av#;Us7Oz)Cjf4^~9wl>>Pmdj4cOq`Kv zoHn^nE?1GMXsD~nl-AYevengfnfivhDcSN|No6iqKcHvNia1_UURT3e1X<1u$Z>{F zBP!=if(FiR$Z>{156+ON;%GT%fMhunVk&1elyU37dBnfD&v>4-f#<K{EQs19!#O`C zoCEZ49yyFh*80(9zW*d1J&~_C!?R7}EA7M49OrRlsDWp%L8bn$=-I3Jtm8<%Z=1rA ta_(Ki`5k&xJ?8`U<g?;`?q9;a^i1*pH`|*2=_8#=^E<9W{Tb`#{{YZ<<v9QV diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff deleted file mode 100644 index bc4cc324b07cbf7b531017800d7392e89e4b93fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2988 zcmeHJ{Z|ub8XcgD0vemx6|4f*sxYa5q6Mn~!2xutrU)b;NJ0?BDg^{rcH9IaD&iCo z*O7xts8dHw_y`s?YD_{u=?5jwsI^sus;kVju4sL&btI&t?b&no57^)OoO91S_s;Wv zKR?{9^29{Q0}{fw74m)ljmZClKhOU+l2X#*A;@Elpe_`U8jR1t<SF5E1q~5+i~vP$ zV(x<6f}A2j+W|piJRwM`$-kYGwtmw;)<MwN-9k;AfOmG^oqjDpFK4Zw^$0vnz?<Vg zPfpDjL_r%C_!I%J`_y<%&o3w~gCHM&fj?h+j8`xOZ7R$a>U@L@8vOzx5LA#;Camit zWKf`hV2|<8YwzV0<O$kRVL!s&J@*#%TP#I|C8ff7-xq483JA;FQM#e*NUnOVTK%IY zLfw(8-dy}>Y|fA0EPLat+<h8#P6f1Y(cHsxW<|>C^j=?heSNZG!}#EJmAgHmnF~iP zg<gz3(iN@O6MFqL;`DaCKG`!qGDy8tqsdXLHR`-|T|S;nR*L_W3V5Vb1#PxeEc?JC z*b}NcHdeT`vLpZbKJQY*m4<|ygLgiky4@;?4?de&WZ5xc`^KWJLRelB-4|z7TCLXW z9apcV%%5LA?TEF#{Yvx7^84+9{{H^&s@3WT;W{@y&3p8&jAHf0@c9vdaoiiPDSy!Q z;GSp!c>PGg+-FOMhaWi?bZ>dbFnovqdH5|yuldo>!%@9m9~>=zsIrev?8z+NlkA;d z={@3{4BURnl~_^Hwl7gp8h2DGTTWHAfg&Q%&P-3mK0|9$xjLx~rIOk}J`reR97f<z z1a>mHY1loqHiave%Cf0$GkA$GbTE=M>=r6t#VwP{GN@T*Fp)5P#H6HQKcVh3%%xO* zD`7Y@Os>MtqjD7&n~Lp1o#|WyEGwbb8$l~!Xk$8+*k06`!JUI;Td6@KxIh@p%p)bH zN8KkGUj=p)4bS2dU|T*lsRi63vPtG`1!hFU*Km2TZ4*`B0)8Q~k1|pPFDFJyD5VLg zVB2QOy_!pdT^7{5n!5tqFzV_FaEQomVKyqUM%0|iJ%(*MD(VCnklJ#nC=>XC=s(6B zR$!fIVKz4#w&hV46ZnqkKh9iNU{<tH#Z856xztt@=q36o#;U+9=ukE%gKg`nCnnHO z^fxol6qo}YQgL3eR!xaa;A0}@7*nO-JJ6tPZaS=8OVyeHO~f2$x)uCcG)TosV6BFd zo4{8@48=4n_)}<8HWvwN*HKOr_<@LNW(F1f1++=U`NG;9%AL%WD|ixBWpQuA+6`2A z3%EnrNycpg1YtKZN+o{?Rb_HN!&)s>cmmWAb|Vv|<PV|)tGVm2b{jQx0yw1F_0*6F z+#u}D%o7D~M+a1#H|$bVK_>7ip*zO7jo?E<*UHo?`D!#agKLIerBss<bP~FD#;N3Y zqK<T~9(HY}R7TK3=-L>wlCMD>8Jq=n{hb;x0xO|2Geb(g5-m^XYGGF~6>9{|#7HaC zq~vk5JcBdCt}T?q2rR@%J2Rl<t5A13*95yt#{KdpOIA&grGtHBl~{I)%|S#`ESmll z4~zr5$t1DtBQ_bSg|T_`hxkYhI|r!^#onNE@xUZ-mh6_u?Ce-X9)i6_OK<}Mj*+t@ zvU6-GA`iu2It3qTV>ck~mEaUv9*(_5U&2Q!*#N{Dg3Y8OaYF)VCD%)2R(1j648_9e z65NmkE|7x~nS&jLxRb$la+28A!6qQ#FgAzA@$7i8pR5<#PO*7NxD;DJe}iYofjy-A zb>2xT#jZ)T6d!3}(-8LxU?P<g+j(|AVh+V-(;M;ZBygU*DzVwwafo{j*hNN(Z710k zNFj_x(ueW>cyN%kh;66Y0;EuiEu^pG{c&I~xm9fIWY-`=Fcw8y@%|;iKt2)MEUXq8 zl47y+GdyN7(32vuwu4=Q1i^d+U4_TQgF3QStUbl%B0*9<n(oG9;y^Vi7i&LaQ;;T@ zpGP<2F-t%L=@e_vuq8;7lwU*-;`YU0C+VKXH<96D?Me1+L<RG6>3ZBA4;+Xpl!s|0 zZchZKNwY+2V_!uELim|<6mCxdZDgTDYh@QA?o7}~4vDoEb{jGv<zwh4xNb41B!k4R z4mJsKCxH*iAc^ZNI}3>o<wI#Xu1f@+WRt{oft`dnLiibUIIc?oEu>1~I?qNSj!=F! zU5M+FfR!AOxa_P5DG%YN(?R$M0-DKKiR&C2j+BS;Qd)(NBmxWRkhpAY5aJHuXVI}z zm&5Ut`>VzNYO$vOSBq^_Uvxuv_rE;rxd+HqYaq|#vN!CD(F1~kNJg@jXZ3v9*VEU7 ztXZ*UMRj9MV@>fV4(XxEVejF6S@Y)I^jo)c@x++j9?)-#L>j@!{OMt!TkYn~_isNf zu+J?L%*LLGENMYq!tsV*JAU&F9Y1>J1Q9gv@z{_5Tkq(CUh44)-oHWQHQftVdqPs+ zVH2i<p8Kp9Avmf(=T>VbcxgsQ;Cj99m;nEg%BOyi&t;D}&;Qr#wXOU#V&cx)XI1$# zcI?^l+1;b@OHXjl^y<@j>Ypy^XRkQ&X4>SWy_u&)T?-=~P3iGl<mc?Feu&gx+yD4R zNw4iH)c$ak{--geH!>S1ho!rU8%qbz#pysF;w?91UT%_E8p<B`eLSu|I&NN=5j?nm zZkP0*SbK{5*S=tP*?YGXr83J!iO6t0efK2i)IMC)GX9Lpl)rw}wtL+h0=~WSZvD=? zm!s4E_Voc{@0}2T!=Xq*Dp~!?H+PZ`KD+Y$HDqRD;hoe$MP9?fo%E}({Gcu<m=oY9 zHhb6ge-eMQ%Tl|4Vvcv^fyZ^|a@hDS*UGLP!x=6WuUr?`y>C_izxVl1>w3`I8_?Ld zr7A3_bK6%tj#&rg-Kn$1-;Ir&oXK1+Ffsw=<Ex?zZ=XrOpKWdTl=zkXtNdw2=t_Rn m!g`(jk2}^>KrfHJS5aM8UDu$WF{3lOLGJ<iF7fCV9^!Y|i>#sm diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index 3b0077c5a..def48f28e 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,7 +2,6 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', - passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', From 9734c2d14447ba1b0bd09c2d3294771d27100f95 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 10 May 2024 00:49:08 +0300 Subject: [PATCH 294/762] Fixed: Notifications with only On Rename enabled ignore-downstream --- src/NzbDrone.Core/Notifications/NotificationDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index a18376d0d..5095caa3a 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -30,6 +30,6 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnApplicationUpdate { get; set; } public bool SupportsOnManualInteractionRequired { get; set; } - public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; + public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnRename || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; } } From 627b2a4289ecdd5558d37940624289708e01e10a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 9 May 2024 17:25:49 -0700 Subject: [PATCH 295/762] New: Parse 480i Bluray/Remux as Bluray 480p Closes #6801 --- .../ParserTests/QualityParserFixture.cs | 5 +++ src/NzbDrone.Core/Parser/QualityParser.cs | 31 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index b9c434f18..a4f9ec8d3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -109,6 +109,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] [TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)] + [TestCase("Adventures.of.Sonic.the.Hedgehog.S01.BluRay.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)] + [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)] public void should_parse_bluray480p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray480p, proper); @@ -309,6 +311,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Sans.Series.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)] [TestCase("Series.Black.1x01.Selezione.Naturale.ITA.720p.BDMux.x264-NovaRip", false)] [TestCase("Series.Hunter.S02.720p.Blu-ray.Remux.AVC.FLAC.2.0-SiCFoI", false)] + [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.720p.DD.2.0.AVC.REMUX-FraMeSToR", false)] public void should_parse_bluray720p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray720p, proper); @@ -340,6 +343,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.Title.S03E01.The.Calm.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR", false)] [TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)] [TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)] + [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.1080p.DD.2.0.AVC.REMUX-FraMeSToR", false)] public void should_parse_bluray1080p_remux_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper); @@ -360,6 +364,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.Title.S01E08.The.Sonarr.BluRay.2160p.AVC.DTS-HD.MA.5.1.REMUX-FraMeSToR", false)] [TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)] [TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)] + [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.2160p.DD.2.0.AVC.REMUX-FraMeSToR", false)] public void should_parse_bluray2160p_remux_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 7631887cd..d466054e6 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex RealRegex = new (@"\b(?<real>REAL)\b", RegexOptions.Compiled); - private static readonly Regex ResolutionRegex = new (@"\b(?:(?<R360p>360p)|(?<R480p>480p|640x480|848x480)|(?<R540p>540p)|(?<R576p>576p)|(?<R720p>720p|1280x720|960p)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H265)|(?:UHD|HEVC|BD|H265)[-_. ]4k))\b", + private static readonly Regex ResolutionRegex = new (@"\b(?:(?<R360p>360p)|(?<R480p>480p|480i|640x480|848x480)|(?<R540p>540p)|(?<R576p>576p)|(?<R720p>720p|1280x720|960p)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H265)|(?:UHD|HEVC|BD|H265)[-_. ]4k))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); // Handle cases where no resolution is in the release name (assume if UHD then 4k) or resolution is non-standard @@ -311,6 +311,35 @@ namespace NzbDrone.Core.Parser } } + if (sourceMatch == null && remuxMatch && resolution != Resolution.Unknown) + { + result.SourceDetectionSource = QualityDetectionSource.Unknown; + + if (resolution == Resolution.R480P) + { + result.Quality = Quality.Bluray480p; + return result; + } + + if (resolution == Resolution.R720p) + { + result.Quality = Quality.Bluray720p; + return result; + } + + if (resolution == Resolution.R2160p) + { + result.Quality = Quality.Bluray2160pRemux; + return result; + } + + if (resolution == Resolution.R1080p) + { + result.Quality = Quality.Bluray1080pRemux; + return result; + } + } + // Anime Bluray matching if (AnimeBlurayRegex.Match(normalizedName).Success) { From 536ff142c3f8cd5e7fddec46afcc4d9998d607c1 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 18 May 2024 18:25:10 +0000 Subject: [PATCH 296/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dani Talens <databio@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Ransack6086 <servarr.jubilant150@slmail.me> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Yi Cao <caoyi06@qq.com> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: topnew <sznetim@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 11 +++- src/NzbDrone.Core/Localization/Core/es.json | 38 +++++++------- src/NzbDrone.Core/Localization/Core/fr.json | 2 +- src/NzbDrone.Core/Localization/Core/pt.json | 25 ++++++++- .../Localization/Core/pt_BR.json | 52 +++++++++---------- src/NzbDrone.Core/Localization/Core/tr.json | 51 +++++++++++++++--- .../Localization/Core/zh_CN.json | 7 ++- 7 files changed, 128 insertions(+), 58 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 77decb65e..b0936c991 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -743,5 +743,14 @@ "IndexerHDBitsSettingsMediums": "Mitjans", "UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.", "UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission" + "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission", + "Mixed": "Combinat", + "Proxy": "Servidor intermediari", + "Mapping": "Mapejat", + "More": "Més", + "MultiLanguages": "Multi-idioma", + "Organize": "Organitza", + "Search": "Cerca", + "SelectDropdown": "Seleccioneu...", + "Shutdown": "Apaga" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 858696d42..08b3c49a6 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -57,7 +57,7 @@ "Condition": "Condición", "Component": "Componente", "Custom": "Personalizado", - "Cutoff": "Umbral", + "Cutoff": "Límite", "Dates": "Fechas", "Debug": "Debug", "Date": "Fecha", @@ -181,7 +181,7 @@ "AddNewSeriesError": "Falló al cargar los resultados de la búsqueda, inténtelo de nuevo.", "AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empiece escribiendo el nombre de la serie que desea añadir.", "AddNewSeriesRootFolderHelpText": "La subcarpeta '{folder}' será creada automáticamente", - "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con umbrales no alcanzados", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con límites no alcanzados", "AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes", "AddQualityProfile": "Añadir Perfil de Calidad", "AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.", @@ -272,7 +272,7 @@ "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", "MonitorPilotEpisode": "Episodio Piloto", "MonitorRecentEpisodesDescription": "Monitorizar episodios emitidos en los últimos 90 días y los episodios futuros", - "MonitorSelected": "Monitorizar Seleccionados", + "MonitorSelected": "Monitorizar seleccionados", "MonitorSeries": "Monitorizar Series", "NoHistory": "Sin historial", "NoHistoryFound": "No se encontró historial", @@ -436,7 +436,7 @@ "CustomFormatsLoadError": "No se pudo cargar Formatos Personalizados", "CustomFormatsSettings": "Configuración de formatos personalizados", "CustomFormatsSettingsSummary": "Formatos y configuraciones personalizados", - "CutoffUnmet": "Umbrales no alcanzados", + "CutoffUnmet": "Límite no alcanzado", "DailyEpisodeFormat": "Formato de episodio diario", "Database": "Base de datos", "DelayMinutes": "{delay} minutos", @@ -463,14 +463,14 @@ "InteractiveImportLoadError": "No se pueden cargar elementos de la importación manual", "InteractiveImportNoFilesFound": "No se han encontrado archivos de vídeo en la carpeta seleccionada", "InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado", - "InteractiveSearchModalHeader": "Búsqueda Interactiva", + "InteractiveSearchModalHeader": "Búsqueda interactiva", "InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración", "ChownGroup": "chown grupo", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo", "ContinuingSeriesDescription": "Se esperan más episodios u otra temporada", - "CutoffUnmetLoadError": "Error cargando elementos con umbrales no alcanzados", - "CutoffUnmetNoItems": "No hay elementos con umbrales no alcanzados", + "CutoffUnmetLoadError": "Error cargando elementos con límites no alcanzados", + "CutoffUnmetNoItems": "Ningún elemento con límites no alcanzados", "DelayProfile": "Perfil de retardo", "Delete": "Eliminar", "DeleteDelayProfile": "Eliminar Perfil de Retardo", @@ -530,7 +530,7 @@ "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, 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", + "IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de parar, 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": "La configuración de etiqueta falló", @@ -816,7 +816,7 @@ "FirstDayOfWeek": "Primer día de la semana", "Forums": "Foros", "FreeSpace": "Espacio libre", - "MissingNoItems": "No hay elementos faltantes", + "MissingNoItems": "Ningún elemento faltante", "ImportListRootFolderMissingRootHealthCheckMessage": "Carpeta raíz faltante para importar lista(s): {rootFolderInfo}", "MissingLoadError": "Error cargando elementos faltantes", "RemoveSelectedBlocklistMessageText": "¿Estás seguro que quieres eliminar los elementos seleccionados de la lista de bloqueos?", @@ -890,8 +890,8 @@ "ImportListSettings": "Importar ajustes de lista", "ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o listas de Trakt y gestionar exclusiones de lista", "IncludeCustomFormatWhenRenamingHelpText": "Incluir en formato de renombrado {Custom Formats}", - "QualityCutoffNotMet": "Calidad del umbral que no ha sido alcanzado", - "SearchForCutoffUnmetEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios en Umbrales no alcanzados?", + "QualityCutoffNotMet": "No se ha alcanzado el límite de calidad", + "SearchForCutoffUnmetEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios con límites no alcanzados?", "IndexerOptionsLoadError": "No se pudieron cargar las opciones del indexador", "IndexerIPTorrentsSettingsFeedUrl": "URL del canal", "ICalFeed": "Canal de iCal", @@ -991,7 +991,7 @@ "ImportScriptPathHelpText": "La ruta al script a usar para importar", "ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)", "Importing": "Importando", - "IncludeUnmonitored": "Incluir sin monitorizar", + "IncludeUnmonitored": "Incluir no monitorizadas", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas", "IRC": "IRC", "ICalShowAsAllDayEvents": "Mostrar como eventos para todo el día", @@ -1041,7 +1041,7 @@ "ImportedTo": "Importar a", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado cuando se renombra", "CleanLibraryLevel": "Limpiar el nivel de la librería", - "SearchForCutoffUnmetEpisodes": "Buscar todos los episodios en Umbrales no alcanzados", + "SearchForCutoffUnmetEpisodes": "Buscar todos los episodios con límites no alcanzados", "IconForSpecials": "Icono para Especiales", "ImportListExclusions": "Importar lista de exclusiones", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas las listas requieren interacción manual debido a posibles búsquedas parciales", @@ -1057,8 +1057,8 @@ "HourShorthand": "h", "ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o pulsa para suscribirse si tu navegador soporta Webcal", "ICalTagsSeriesHelpText": "El canal 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", + "IconForCutoffUnmet": "Icono de límite no alcanzado", + "IconForCutoffUnmetHelpText": "Muestra un icono para archivos cuando el límite no haya sido alcanzado", "EpisodeCount": "Recuento de episodios", "IndexerSettings": "Opciones del indexador", "AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, por favor inténtalo de nuevo.", @@ -1102,7 +1102,7 @@ "IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}", "IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}", "IndexerHDBitsSettingsMediums": "Medios", - "IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vació usa el predeterminado del cliente de descarga", + "IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de parar, vacío 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", @@ -1226,7 +1226,7 @@ "MetadataSourceSettings": "Opciones de fuente de metadatos", "MetadataSettingsSeriesSummary": "Crea archivos de metadatos cuando los episodios son importados o las series son refrescadas", "Metadata": "Metadatos", - "MassSearchCancelWarning": "Esto no puede ser cancelado una vez empiece sin reiniciar {appName} o deshabilitar todos tus indexadores.", + "MassSearchCancelWarning": "No puede ser cancelado una vez comience sin reiniciar {appName} o deshabilitar todos tus indexadores.", "MaximumSingleEpisodeAgeHelpText": "Durante una búsqueda completa de temporada, solo serán permitidos los paquetes de temporada cuando el último episodio de temporada sea más antiguo que esta configuración. Solo series estándar. Usa 0 para deshabilitar.", "MaximumSize": "Tamaño máximo", "IndexerValidationNoResultsInConfiguredCategories": "Petición con éxito, pero no se devolvió ningún resultado en las categorías configuradas de tu indexador. Esto puede ser un problema con el indexador o tus ajustes de categoría de tu indexador.", @@ -1710,7 +1710,7 @@ "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", + "UnmonitorSelected": "No 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", @@ -2073,6 +2073,6 @@ "IndexerSettingsMultiLanguageRelease": "Múltiples idiomas", "IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está reportando archivos faltantes", - "BlocklistFilterHasNoItems": "El filtro de lista de bloqueo seleccionado no contiene ningún elemento", + "BlocklistFilterHasNoItems": "El filtro de lista de bloqueo seleccionado no contiene elementos", "HasUnmonitoredSeason": "Tiene temporada sin monitorizar" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 10fd9c8eb..409e01ae3 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1884,7 +1884,7 @@ "CustomFormatsSpecificationSource": "Source", "ImportListsAniListSettingsAuthenticateWithAniList": "S'authentifier avec AniList", "ImportListsAniListSettingsImportCancelled": "Importation annulée", - "ImportListsAniListSettingsImportCancelledHelpText": "Médias : La série est annulée", + "ImportListsAniListSettingsImportCancelledHelpText": "Médias : Série annulée", "ImportListsAniListSettingsImportCompleted": "Importation terminée", "ImportListsAniListSettingsImportFinished": "Importation terminée", "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur pour la liste à importer", diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 95931000a..bf795d86d 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -186,5 +186,28 @@ "AutomaticSearch": "Pesquisa automática", "AutoTaggingRequiredHelpText": "Esta condição de {implementationName} deve corresponder para que a regra de marcação automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} é suficiente.", "Backup": "Backup", - "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmar nova senha" + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmar nova senha", + "Conditions": "Condições", + "ClickToChangeIndexerFlags": "Clique para alterar os sinalizadores do indexador", + "CustomFormatsSpecificationRegularExpression": "Expressão regular (regex)", + "Dash": "Traço", + "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", + "CustomFormatsSpecificationFlag": "Sinalizar", + "CustomFormatsSpecificationRegularExpressionHelpText": "O regex do formato personalizado não diferencia maiúsculas e minúsculas", + "AutoTaggingSpecificationTag": "Etiqueta", + "ClearBlocklist": "Limpar lista de bloqueio", + "ClearBlocklistMessageText": "Tem a certeza de que deseja limpar todos os itens da lista de bloqueio?", + "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a uma versão ou ficheiro quando corresponder a pelo menos um dos diferentes tipos de condição escolhidos.", + "BlocklistOnly": "Apenas adicionar à lista de bloqueio", + "BlocklistAndSearch": "Adicionar à lista de bloqueio e pesquisar", + "BlocklistAndSearchHint": "Iniciar uma pesquisa por um substituto após adicionar à lista de bloqueio", + "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar à lista de bloqueio", + "BlocklistMultipleOnlyHint": "Adicionar à lista de bloqueio sem procurar por substitutos", + "BlocklistOnlyHint": "Adicionar à lista de bloqueio sem procurar por um substituto", + "ChangeCategory": "Alterar categoria", + "ChangeCategoryHint": "Altera o download para a \"Categoria pós-importação\" do cliente de download", + "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", + "ChownGroup": "Fazer chown em grupo", + "Clone": "Clonar", + "ContinuingOnly": "Continuando apenas" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 6ef21a7f7..a491440c8 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -465,7 +465,7 @@ "ChmodFolder": "chmod Pasta", "ChmodFolderHelpText": "Octal, aplicado durante a importação/renomeação de pastas e arquivos de mídia (sem bits de execução)", "ChmodFolderHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download defina as permissões corretamente.", - "ChownGroup": "chown Grupo", + "ChownGroup": "Fazer chown em grupo", "ChownGroupHelpText": "Nome do grupo ou gid. Use gid para sistemas de arquivos remotos.", "ChownGroupHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download use o mesmo grupo que {appName}.", "ClientPriority": "Prioridade do Cliente", @@ -683,7 +683,7 @@ "OnApplicationUpdate": "Na Atualização do Aplicativo", "OnEpisodeFileDelete": "Ao Excluir o Arquivo do Episódio", "OnEpisodeFileDeleteForUpgrade": "No Arquivo do Episódio Excluir para Atualização", - "OnGrab": "Ao Baixar", + "OnGrab": "Ao obter", "OnHealthIssue": "Ao Problema de Saúde", "OnHealthRestored": "Com a Saúde Restaurada", "OnImport": "Ao Importar", @@ -1447,12 +1447,12 @@ "MissingLoadError": "Erro ao carregar itens ausentes", "MissingNoItems": "Nenhum item ausente", "SearchAll": "Pesquisar Todos", - "UnmonitorSelected": "Não Monitorar Selecionado", - "CutoffUnmetNoItems": "Nenhum item com limite não atendido", - "MonitorSelected": "Monitorar selecionados", + "UnmonitorSelected": "Não Monitorar os Selecionados", + "CutoffUnmetNoItems": "Nenhum item com corte não atingido", + "MonitorSelected": "Monitorar Selecionados", "SearchForAllMissingEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", "SearchSelected": "Pesquisar Selecionado", - "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atendido", + "CutoffUnmetLoadError": "Erro ao carregar itens de corte não atingidos", "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar {appName} ou desabilitar todos os seus indexadores.", "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", "SearchForCutoffUnmetEpisodes": "Pesquise todos os episódios que o corte não foi atingido", @@ -1503,11 +1503,11 @@ "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status de descompactação: {unpackStatus} - Status de movimentação: {moveStatus} - Status do script: {scriptStatus} - Status de exclusão: {deleteStatus} - Status de marcação: {markStatus}", "PostImportCategory": "Categoria Pós-Importação", "SecretToken": "Token Secreto", - "TorrentBlackhole": "Torrent Blackhole", + "TorrentBlackhole": "Blackhole para torrent", "TorrentBlackholeSaveMagnetFiles": "Salvar Arquivos Magnets", "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnet se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", "UnknownDownloadState": "Estado de download desconhecido: {state}", - "UsenetBlackhole": "Usenet Blackhole", + "UsenetBlackhole": "Blackhole para Usenet", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de semeação", "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desabilitado para este torrent ou 'Layout de conteúdo de torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "As categorias não são suportadas até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", @@ -1645,8 +1645,8 @@ "IndexerSettingsPasskey": "Passkey", "IndexerSettingsRssUrl": "URL do RSS", "IndexerSettingsSeasonPackSeedTime": "Tempo de Seed de Pack de Temporada", - "IndexerSettingsSeedRatio": "Proporção de Semeação", - "IndexerSettingsSeedTime": "Tempo de Semeação", + "IndexerSettingsSeedRatio": "Proporção de semeação", + "IndexerSettingsSeedTime": "Tempo de semeação", "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser semeado antes de parar, vazio usa o padrão do cliente de download", "IndexerSettingsWebsiteUrl": "URL do Website", "IndexerValidationCloudFlareCaptchaExpired": "O token CloudFlare CAPTCHA expirou, atualize-o.", @@ -1892,8 +1892,8 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "O lançamento deve ser menor ou igual a este tamanho", "CustomFormatsSpecificationMinimumSize": "Tamanho Mínimo", "CustomFormatsSpecificationMinimumSizeHelpText": "O lançamento deve ser maior que esse tamanho", - "CustomFormatsSpecificationRegularExpression": "Expressão Regular", - "CustomFormatsSpecificationRegularExpressionHelpText": "RegEx do Formato Personalizado Não Diferencia Maiúsculas de Minúsculas", + "CustomFormatsSpecificationRegularExpression": "Expressão regular (regex)", + "CustomFormatsSpecificationRegularExpressionHelpText": "O regex do formato personalizado não diferencia maiúsculas e minúsculas", "CustomFormatsSpecificationReleaseGroup": "Grupo do Lançamento", "CustomFormatsSpecificationResolution": "Resolução", "CustomFormatsSpecificationSource": "Fonte", @@ -2010,24 +2010,24 @@ "IgnoreDownloadsHint": "Impede que {appName} processe ainda mais esses downloads", "RemoveFromDownloadClientHint": "Remove download e arquivo(s) do cliente de download", "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.", - "BlocklistMultipleOnlyHint": "Adiciona a Lista de bloqueio sem procurar substitutos", - "BlocklistOnly": "Apenas Adicionar a Lista de Bloqueio", - "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar a lista de bloqueio", + "BlocklistMultipleOnlyHint": "Adicionar à lista de bloqueio sem procurar por substitutos", + "BlocklistOnly": "Apenas adicionar à lista de bloqueio", + "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar à lista de bloqueio", "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente por {appName} via RSS ou Pesquisa Automática", - "ChangeCategoryHint": "Altera o download para a 'Categoria Pós-Importação' do Cliente de Download", - "ChangeCategoryMultipleHint": "Altera os downloads para a 'Categoria Pós-Importação' do Cliente de Download", + "ChangeCategoryHint": "Altera o download para a \"Categoria pós-importação\" do cliente de download", + "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", "DatabaseMigration": "Migração de Banco de Dados", "DoNotBlocklistHint": "Remover sem colocar na lista de bloqueio", - "ChangeCategory": "Alterar Categoria", + "ChangeCategory": "Alterar categoria", "DoNotBlocklist": "Não coloque na lista de bloqueio", "IgnoreDownloads": "Ignorar Downloads", "IgnoreDownloadHint": "Impede que {appName} processe ainda mais este download", "RemoveMultipleFromDownloadClientHint": "Remove downloads e arquivos do cliente de download", "RemoveQueueItemRemovalMethod": "Método de Remoção", "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remover do Cliente de Download' removerá os downloads e os arquivos do cliente de download.", - "BlocklistAndSearch": "Lista de Bloqueio e Pesquisa", - "BlocklistAndSearchHint": "Inicie uma busca por um substituto após adicionar a lista de bloqueio", - "BlocklistOnlyHint": "Adiciona a Lista de bloqueio sem procurar um substituto", + "BlocklistAndSearch": "Adicionar à lista de bloqueio e pesquisar", + "BlocklistAndSearchHint": "Iniciar uma pesquisa por um substituto após adicionar à lista de bloqueio", + "BlocklistOnlyHint": "Adicionar à lista de bloqueio sem procurar por um substituto", "IgnoreDownload": "Ignorar Download", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais", "KeepAndTagSeries": "Manter e Etiquetar Séries", @@ -2038,10 +2038,10 @@ "LogOnly": "Só Registro", "CleanLibraryLevel": "Limpar Nível da Biblioteca", "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente.", - "ClickToChangeIndexerFlags": "Clique para alterar sinalizadores do indexador", + "ClickToChangeIndexerFlags": "Clique para alterar os sinalizadores do indexador", "SelectIndexerFlags": "Selecionar Sinalizadores do Indexador", "SetIndexerFlagsModalTitle": "{modalTitle} - Definir Sinalizadores do Indexador", - "CustomFormatsSpecificationFlag": "Sinalizador", + "CustomFormatsSpecificationFlag": "Sinalizar", "IndexerFlags": "Sinalizadores do Indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", @@ -2050,7 +2050,7 @@ "Filters": "Filtros", "Label": "Rótulo", "LabelIsRequired": "Rótulo é requerido", - "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo a URL {connectionName}, como {url}", + "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", "ReleaseType": "Tipo de Lançamento", "DownloadClientDelugeSettingsDirectory": "Diretório de Download", "DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído", @@ -2061,7 +2061,7 @@ "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.", + "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos 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.`).", @@ -2073,5 +2073,5 @@ "AutoTaggingSpecificationTag": "Etiqueta", "IndexerSettingsMultiLanguageRelease": "Multi Idiomas", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está relatando arquivos perdidos", - "BlocklistFilterHasNoItems": "O filtro da lista de bloqueio selecionado não contém itens" + "BlocklistFilterHasNoItems": "O filtro selecionado para a lista de bloqueio não contém itens" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 74b90b989..3a595cec5 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -60,7 +60,7 @@ "AddDownloadClientError": "Yeni bir indirme istemcisi eklenemiyor, lütfen tekrar deneyin.", "AddCustomFilter": "Özel Filtre Ekleyin", "AddDownloadClientImplementation": "İndirme İstemcisi Ekle - {implementationName}", - "AddExclusion": "Hariç Tutma Ekleme", + "AddExclusion": "Dışlananlara Ekle", "AddList": "Liste Ekleyin", "AddListError": "Yeni bir liste eklenemiyor, lütfen tekrar deneyin.", "AddNew": "Yeni Ekle", @@ -134,11 +134,11 @@ "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", + "AuthenticationRequired": "Kimlik Doğrulama", "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 sayısı kadar güncelleyiniz. Bunu ayarlar veya yapılandırma dosyası üzerinden 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", + "AutomaticUpdatesDisabledDocker": "Docker güncelleme mekanizması kullanıldığında otomatik güncellemeler doğrudan desteklenmez. Konteyner 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", @@ -196,7 +196,7 @@ "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", "DeleteImportList": "İçe Aktarma Listesini Sil", "IndexerPriorityHelpText": "Dizinleyici Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", - "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", + "DisabledForLocalAddresses": "Yerel Adreslerde Devre Dışı Bırak", "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.", @@ -383,7 +383,7 @@ "NotificationsKodiSettingAlwaysUpdate": "Daima Güncelle", "NotificationsKodiSettingsCleanLibrary": "Kütüphaneyi Temizle", "NotificationsKodiSettingsCleanLibraryHelpText": "Güncellemeden sonra kitaplığı temizle", - "Unmonitored": "İzlenmeyen", + "Unmonitored": "Takip Edilmiyor", "FormatAgeHour": "saat", "FormatAgeHours": "saat", "NoHistory": "Geçmiş yok", @@ -549,7 +549,7 @@ "VideoDynamicRange": "Video Dinamik Aralığı", "WouldYouLikeToRestoreBackup": "'{name}' yedeğini geri yüklemek ister misiniz?", "NotificationsTwitterSettingsDirectMessage": "Direk mesaj", - "PasswordConfirmation": "Şifre onayı", + "PasswordConfirmation": "Şifre Tekrarı", "OrganizeRenamingDisabled": "Yeniden adlandırma devre dışı bırakıldı, yeniden adlandırılacak bir şey yok", "ResetQualityDefinitionsMessageText": "Kalite tanımlarını sıfırlamak istediğinizden emin misiniz?", "SelectFolderModalTitle": "{modalTitle} - Klasör seç", @@ -689,7 +689,7 @@ "NotificationsValidationUnableToConnectToService": "{serviceName} hizmetine bağlanılamıyor", "OrganizeLoadError": "Önizlemeler yüklenirken hata oluştu", "ParseModalHelpTextDetails": "{appName}, başlığı ayrıştırmaya ve size konuyla ilgili ayrıntıları göstermeye çalışacak", - "NegateHelpText": "İşaretlenirse, bu {implementationName} koşulu eşleşirse özel format uygulanmayacaktır.", + "NegateHelpText": "İşaretlenirse, {implementationName} koşulu eşleşirse özel format uygulanmayacaktır.", "ParseModalHelpText": "Yukarıdaki girişe bir yayın başlığı girin", "Period": "Periyot", "NotificationsPushoverSettingsSound": "Ses", @@ -802,5 +802,40 @@ "MustContain": "İçermeli", "MustNotContain": "İçermemeli", "RssSyncIntervalHelpTextWarning": "Bu, tüm dizinleyiciler için geçerli olacaktır, lütfen onlar tarafından belirlenen kurallara uyun", - "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor" + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor", + "ImportListExclusions": "İçe Aktarma Listesinden Hariç Bırakılan(lar)", + "UiLanguage": "Arayüz Dili", + "IndexerSettingsSeedTimeHelpText": "Bir torrentin durmadan önce seed edilmesi gereken süre. Boş bırakılırsa indirme istemcisinin varsayılan ayarını kullanır", + "IndexerSettingsSeedRatioHelpText": "Bir torrentin durmadan önce ulaşması gereken oran. Boş bırakılırsa indirme istemcisinin varsayılan değerini kullanır. Oran en az 1,0 olmalı ve indeksleyici kurallarına uygun olmalıdır", + "EditImportListExclusion": "Hariç Tutulanlar Listesini Düzenle", + "UiLanguageHelpText": "{appName}'ın arayüz için kullanacağı dil", + "IndexerSettingsSeedTime": "Seed Süresi", + "BlocklistFilterHasNoItems": "Seçilen engelleme listesi filtresi hiçbir öğe içermiyor", + "IndexerSettingsSeedRatio": "Seed Oranı", + "Password": "Şifre", + "CutoffUnmetNoItems": "Karşılanmayan son öğe yok", + "MissingLoadError": "Eksik öğeler yüklenirken hata oluştu", + "MissingNoItems": "Eksik öğe yok", + "CutoffUnmet": "Kesinti Karşılanmayan", + "Monitor": "Takip", + "CutoffUnmetLoadError": "Karşılanmamış kesinti öğeleri yüklenirken hata oluştu", + "Monitored": "Takip Ediliyor", + "MonitoredOnly": "Sadece Takip Edilen", + "External": "Harici", + "MassSearchCancelWarning": "Bu işlem, {appName} yeniden başlatılmadan veya tüm dizin oluşturucularınız devre dışı bırakılmadan başlatılır ise iptal edilemez.", + "IncludeUnmonitored": "Takip Edilmeyenleri Dahil Et", + "MonitorSelected": "Takip Edilen Seçildi", + "MonitoredStatus": "Takip Edilen/Durum", + "UnmonitorSelected": "Seçili Takipleri Kaldır", + "ShowMonitored": "Takip Edilenleri Göster", + "ShowMonitoredHelpText": "Posterin altında takip durumu göster", + "Ui": "Arayüz", + "Mechanism": "İşleyiş", + "UiSettings": "Arayüz Ayarları", + "Script": "Hazır Metin", + "UiSettingsLoadError": "Arayüz ayarları yüklenemiyor", + "UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz", + "Wanted": "Arananlar", + "Cutoff": "Kesinti", + "Required": "Gerekli" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 385dc081b..ab24051f9 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -518,7 +518,7 @@ "AnEpisodeIsDownloading": "集正在下载", "AuthenticationRequiredWarning": "为了防止未经身份验证的远程访问,{appName} 现在需要启用身份验证。您可以禁用本地地址的身份验证。", "AutomaticSearch": "自动搜索", - "BackupFolderHelpText": "相对路径将在{appName}的AppData目录下", + "BackupFolderHelpText": "相对路径将在 {appName} 的 AppData 目录下", "BindAddress": "绑定地址", "BindAddressHelpText": "有效的 IP 地址、localhost、或以'*'代表所有接口", "BlocklistLoadError": "无法加载黑名单", @@ -1827,5 +1827,8 @@ "CustomFormatsSpecificationMaximumSizeHelpText": "必须小于或等于该尺寸时才会发布", "AutoTaggingSpecificationGenre": "类型", "AutoTaggingSpecificationSeriesType": "系列类型", - "AutoTaggingSpecificationStatus": "状态" + "AutoTaggingSpecificationStatus": "状态", + "ClickToChangeIndexerFlags": "点击修改索引器标志", + "ConnectionSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}", + "BlocklistFilterHasNoItems": "所选的黑名单过滤器没有项目" } From 084fcc2295ee739958761d79d031dc6f40b673f7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:16:26 +0300 Subject: [PATCH 297/762] Implement equality checks for providers --- .../NotificationBaseFixture.cs | 5 +- .../Download/Clients/Aria2/Aria2Settings.cs | 7 ++- .../Blackhole/TorrentBlackholeSettings.cs | 7 ++- .../Blackhole/UsenetBlackholeSettings.cs | 9 ++-- .../Download/Clients/Deluge/DelugeSettings.cs | 7 ++- .../Clients/DownloadClientSettingsBase.cs | 30 ++++++++++++ .../DownloadStationSettings.cs | 9 ++-- .../Download/Clients/Flood/FloodSettings.cs | 7 ++- .../FreeboxDownloadSettings.cs | 7 ++- .../Clients/Hadouken/HadoukenSettings.cs | 9 ++-- .../Clients/NzbVortex/NzbVortexSettings.cs | 9 ++-- .../Download/Clients/Nzbget/NzbgetSettings.cs | 7 ++- .../Clients/Pneumatic/PneumaticSettings.cs | 9 ++-- .../QBittorrent/QBittorrentSettings.cs | 7 ++- .../Clients/Sabnzbd/SabnzbdSettings.cs | 9 ++-- .../Clients/TorrentSeedConfiguration.cs | 4 +- .../Transmission/TransmissionSettings.cs | 7 ++- .../Clients/rTorrent/RTorrentSettings.cs | 7 ++- .../Clients/uTorrent/UTorrentSettings.cs | 7 ++- .../Download/DownloadClientDefinition.cs | 25 +++++++++- .../AniList/AniListSettingsBase.cs | 9 ++-- .../AniList/List/AniListSettings.cs | 36 +++++++------- .../ImportLists/Custom/CustomSettings.cs | 13 ++--- .../ImportLists/Imdb/ImdbListSettings.cs | 8 ++-- .../ImportLists/ImportListDefinition.cs | 26 +++++++++- .../ImportLists/ImportListSettingsBase.cs | 31 ++++++++++++ .../MyAnimeList/MyAnimeListSettings.cs | 13 ++--- .../ImportLists/Plex/PlexListSettings.cs | 8 ++-- .../Rss/Plex/PlexRssImportSettings.cs | 4 +- .../ImportLists/Rss/RssImportBase.cs | 4 +- .../ImportLists/Rss/RssImportBaseSettings.cs | 14 +++--- .../Rss/RssImportRequestGenerator.cs | 9 ++-- .../ImportLists/Simkl/SimklSettingsBase.cs | 9 ++-- .../Simkl/User/SimklUserSettings.cs | 9 +++- .../ImportLists/Sonarr/SonarrSettings.cs | 9 ++-- .../Trakt/List/TraktListSettings.cs | 9 +++- .../Trakt/Popular/TraktPopularSettings.cs | 9 +++- .../ImportLists/Trakt/TraktSettingsBase.cs | 9 ++-- .../Trakt/User/TraktUserSettings.cs | 9 +++- .../BroadcastheNet/BroadcastheNetSettings.cs | 3 +- .../Indexers/Fanzub/FanzubSettings.cs | 5 +- .../Indexers/FileList/FileListSettings.cs | 7 +-- .../Indexers/HDBits/HDBitsSettings.cs | 3 +- .../Indexers/IPTorrents/IPTorrentsSettings.cs | 7 +-- .../Indexers/IndexerDefinition.cs | 35 ++++++++++++-- .../Indexers/Newznab/NewznabSettings.cs | 7 +-- .../Indexers/Nyaa/NyaaSettings.cs | 7 +-- .../Indexers/SeedCriteriaSettings.cs | 3 +- .../TorrentRss/TorrentRssIndexerSettings.cs | 7 +-- .../Torrentleech/TorrentleechSettings.cs | 7 +-- .../Indexers/Torznab/TorznabSettings.cs | 26 ++++++++-- .../Notifications/Apprise/AppriseSettings.cs | 5 +- .../CustomScript/CustomScriptSettings.cs | 9 ++-- .../Notifications/Discord/DiscordSettings.cs | 5 +- .../Notifications/Email/EmailSettings.cs | 7 ++- .../Notifications/Gotify/GotifySettings.cs | 7 ++- .../Notifications/Join/JoinSettings.cs | 9 ++-- .../Notifications/Mailgun/MailgunSettings.cs | 7 ++- .../MediaBrowser/MediaBrowserSettings.cs | 7 ++- .../Notifiarr/NotifiarrSettings.cs | 7 ++- .../Notifications/NotificationBase.cs | 2 +- .../Notifications/NotificationDefinition.cs | 48 ++++++++++++++++++- .../Notifications/NotificationSettingsBase.cs | 30 ++++++++++++ .../Notifications/Ntfy/NtfySettings.cs | 9 ++-- .../Plex/Server/PlexServerSettings.cs | 7 ++- .../Notifications/Prowl/ProwlSettings.cs | 9 ++-- .../PushBullet/PushBulletSettings.cs | 7 ++- .../Notifications/Pushcut/PushcutSettings.cs | 5 +- .../Pushover/PushoverSettings.cs | 7 ++- .../SendGrid/SendGridSettings.cs | 9 ++-- .../Notifications/Signal/SignalSettings.cs | 5 +- .../Simplepush/SimplepushSettings.cs | 7 ++- .../Notifications/Slack/SlackSettings.cs | 7 ++- .../Synology/SynologyIndexerSettings.cs | 7 ++- .../Telegram/TelegramSettings.cs | 7 ++- .../Notifications/Trakt/TraktSettings.cs | 7 ++- .../Notifications/Twitter/TwitterSettings.cs | 9 ++-- .../Notifications/Webhook/WebhookBase.cs | 3 +- .../Notifications/Webhook/WebhookSettings.cs | 9 ++-- .../Notifications/Xbmc/XbmcSettings.cs | 5 +- src/NzbDrone.Core/Sonarr.Core.csproj | 3 +- .../ThingiProvider/ProviderDefinition.cs | 11 +++-- .../Notifications/NotificationResource.cs | 6 +-- src/Sonarr.Api.V3/ProviderControllerBase.cs | 4 +- 84 files changed, 517 insertions(+), 312 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/DownloadClientSettingsBase.cs create mode 100644 src/NzbDrone.Core/ImportLists/ImportListSettingsBase.cs create mode 100644 src/NzbDrone.Core/Notifications/NotificationSettingsBase.cs diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs index b773ada75..2660b90fe 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs @@ -5,7 +5,6 @@ using FluentValidation.Results; using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; using NzbDrone.Test.Common; @@ -15,9 +14,9 @@ namespace NzbDrone.Core.Test.NotificationTests [TestFixture] public class NotificationBaseFixture : TestBase { - private class TestSetting : IProviderConfig + private class TestSetting : NotificationSettingsBase<TestSetting> { - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(); } diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs index f90ea6306..d4d01fb69 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Aria2 @@ -13,9 +12,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2 } } - public class Aria2Settings : IProviderConfig + public class Aria2Settings : DownloadClientSettingsBase<Aria2Settings> { - private static readonly Aria2SettingsValidator Validator = new Aria2SettingsValidator(); + private static readonly Aria2SettingsValidator Validator = new (); public Aria2Settings() { @@ -44,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")] public string Directory { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs index ad3010a60..95e1ad385 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs @@ -2,7 +2,6 @@ using System.ComponentModel; using FluentValidation; using Newtonsoft.Json; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -18,7 +17,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole } } - public class TorrentBlackholeSettings : IProviderConfig + public class TorrentBlackholeSettings : DownloadClientSettingsBase<TorrentBlackholeSettings> { public TorrentBlackholeSettings() { @@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole ReadOnly = true; } - private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); + private static readonly TorrentBlackholeSettingsValidator Validator = new (); [FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] [FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")] @@ -48,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole [FieldDefinition(4, Label = "TorrentBlackholeSaveMagnetFilesReadOnly", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText")] public bool ReadOnly { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs index ae9603b61..7e97c7ae7 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs @@ -1,6 +1,5 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole } } - public class UsenetBlackholeSettings : IProviderConfig + public class UsenetBlackholeSettings : DownloadClientSettingsBase<UsenetBlackholeSettings> { - private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); + private static readonly UsenetBlackholeSettingsValidator Validator = new (); [FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] [FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")] @@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole [FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")] public string WatchFolder { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index f18643510..b5108cf24 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Deluge @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge } } - public class DelugeSettings : IProviderConfig + public class DelugeSettings : DownloadClientSettingsBase<DelugeSettings> { - private static readonly DelugeSettingsValidator Validator = new DelugeSettingsValidator(); + private static readonly DelugeSettingsValidator Validator = new (); public DelugeSettings() { @@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")] public string CompletedDirectory { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadClientSettingsBase.cs b/src/NzbDrone.Core/Download/Clients/DownloadClientSettingsBase.cs new file mode 100644 index 000000000..fa68844e1 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/DownloadClientSettingsBase.cs @@ -0,0 +1,30 @@ +using System; +using Equ; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients +{ + public abstract class DownloadClientSettingsBase<TSettings> : IProviderConfig, IEquatable<TSettings> + where TSettings : DownloadClientSettingsBase<TSettings> + { + private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties; + + public abstract NzbDroneValidationResult Validate(); + + public bool Equals(TSettings other) + { + return Comparer.Equals(this as TSettings, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as TSettings); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this as TSettings); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index 37a3b0148..735d5f2c8 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -1,8 +1,7 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.DownloadStation @@ -26,9 +25,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } } - public class DownloadStationSettings : IProviderConfig + public class DownloadStationSettings : DownloadClientSettingsBase<DownloadStationSettings> { - private static readonly DownloadStationSettingsValidator Validator = new DownloadStationSettingsValidator(); + private static readonly DownloadStationSettingsValidator Validator = new (); [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] public string Host { get; set; } @@ -58,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation this.Port = 5000; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs index f26e19512..4d582f48f 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs @@ -3,7 +3,6 @@ using System.Linq; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Download.Clients.Flood.Models; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Flood @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Flood } } - public class FloodSettings : IProviderConfig + public class FloodSettings : DownloadClientSettingsBase<FloodSettings> { - private static readonly FloodSettingsValidator Validator = new FloodSettingsValidator(); + private static readonly FloodSettingsValidator Validator = new (); public FloodSettings() { @@ -69,7 +68,7 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(10, Label = "DownloadClientFloodSettingsStartOnAdd", Type = FieldType.Checkbox)] public bool StartOnAdd { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs index 42b645848..b2ae0d160 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -34,9 +33,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload } } - public class FreeboxDownloadSettings : IProviderConfig + public class FreeboxDownloadSettings : DownloadClientSettingsBase<FreeboxDownloadSettings> { - private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator(); + private static readonly FreeboxDownloadSettingsValidator Validator = new (); public FreeboxDownloadSettings() { @@ -84,7 +83,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index 8e560720e..390993df8 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -1,7 +1,6 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Hadouken @@ -22,9 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken } } - public class HadoukenSettings : IProviderConfig + public class HadoukenSettings : DownloadClientSettingsBase<HadoukenSettings> { - private static readonly HadoukenSettingsValidator Validator = new HadoukenSettingsValidator(); + private static readonly HadoukenSettingsValidator Validator = new (); public HadoukenSettings() { @@ -57,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string Category { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 81a72004e..27da7065d 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -1,7 +1,6 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.NzbVortex @@ -23,9 +22,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } } - public class NzbVortexSettings : IProviderConfig + public class NzbVortexSettings : DownloadClientSettingsBase<NzbVortexSettings> { - private static readonly NzbVortexSettingsValidator Validator = new NzbVortexSettingsValidator(); + private static readonly NzbVortexSettingsValidator Validator = new (); public NzbVortexSettings() { @@ -59,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(6, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 067867b08..035dd0265 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -1,7 +1,6 @@ using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Nzbget @@ -21,9 +20,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } - public class NzbgetSettings : IProviderConfig + public class NzbgetSettings : DownloadClientSettingsBase<NzbgetSettings> { - private static readonly NzbgetSettingsValidator Validator = new NzbgetSettingsValidator(); + private static readonly NzbgetSettingsValidator Validator = new (); public NzbgetSettings() { @@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")] public bool AddPaused { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs index 6cd8a2b89..a18ad3a4b 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -1,6 +1,5 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic } } - public class PneumaticSettings : IProviderConfig + public class PneumaticSettings : DownloadClientSettingsBase<PneumaticSettings> { - private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); + private static readonly PneumaticSettingsValidator Validator = new (); [FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")] public string NzbFolder { get; set; } @@ -25,7 +24,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic [FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")] public string StrmFolder { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 2ad3ae163..d87df8f9c 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -1,7 +1,6 @@ using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.QBittorrent @@ -19,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public class QBittorrentSettings : IProviderConfig + public class QBittorrentSettings : DownloadClientSettingsBase<QBittorrentSettings> { - private static readonly QBittorrentSettingsValidator Validator = new QBittorrentSettingsValidator(); + private static readonly QBittorrentSettingsValidator Validator = new (); public QBittorrentSettings() { @@ -74,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] public int ContentLayout { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 33d2e3266..98a499ace 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -1,7 +1,6 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Sabnzbd @@ -32,9 +31,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } - public class SabnzbdSettings : IProviderConfig + public class SabnzbdSettings : DownloadClientSettingsBase<SabnzbdSettings> { - private static readonly SabnzbdSettingsValidator Validator = new SabnzbdSettingsValidator(); + private static readonly SabnzbdSettingsValidator Validator = new (); public SabnzbdSettings() { @@ -78,7 +77,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/TorrentSeedConfiguration.cs b/src/NzbDrone.Core/Download/Clients/TorrentSeedConfiguration.cs index 9c4b279dc..8b26f2033 100644 --- a/src/NzbDrone.Core/Download/Clients/TorrentSeedConfiguration.cs +++ b/src/NzbDrone.Core/Download/Clients/TorrentSeedConfiguration.cs @@ -1,10 +1,10 @@ -using System; +using System; namespace NzbDrone.Core.Download.Clients { public class TorrentSeedConfiguration { - public static TorrentSeedConfiguration DefaultConfiguration = new TorrentSeedConfiguration(); + public static TorrentSeedConfiguration DefaultConfiguration = new (); public double? Ratio { get; set; } public TimeSpan? SeedTime { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index dae92b1b1..4a34df4aa 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Transmission @@ -24,9 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission } } - public class TransmissionSettings : IProviderConfig + public class TransmissionSettings : DownloadClientSettingsBase<TransmissionSettings> { - private static readonly TransmissionSettingsValidator Validator = new TransmissionSettingsValidator(); + private static readonly TransmissionSettingsValidator Validator = new (); public TransmissionSettings() { @@ -72,7 +71,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 45822a9f0..df2cbf963 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.RTorrent @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } } - public class RTorrentSettings : IProviderConfig + public class RTorrentSettings : DownloadClientSettingsBase<RTorrentSettings> { - private static readonly RTorrentSettingsValidator Validator = new RTorrentSettingsValidator(); + private static readonly RTorrentSettingsValidator Validator = new (); public RTorrentSettings() { @@ -70,7 +69,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(11, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")] public bool AddStopped { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 6c77dd873..5bbc89d6e 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -1,7 +1,6 @@ using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.UTorrent @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } } - public class UTorrentSettings : IProviderConfig + public class UTorrentSettings : DownloadClientSettingsBase<UTorrentSettings> { - private static readonly UTorrentSettingsValidator Validator = new UTorrentSettingsValidator(); + private static readonly UTorrentSettingsValidator Validator = new (); public UTorrentSettings() { @@ -65,7 +64,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")] public int IntialState { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs index 6de6a4f14..e6941479d 100644 --- a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs +++ b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs @@ -1,14 +1,35 @@ -using NzbDrone.Core.Indexers; +using System; +using Equ; +using NzbDrone.Core.Indexers; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Download { - public class DownloadClientDefinition : ProviderDefinition + public class DownloadClientDefinition : ProviderDefinition, IEquatable<DownloadClientDefinition> { + private static readonly MemberwiseEqualityComparer<DownloadClientDefinition> Comparer = MemberwiseEqualityComparer<DownloadClientDefinition>.ByProperties; + + [MemberwiseEqualityIgnore] public DownloadProtocol Protocol { get; set; } + public int Priority { get; set; } = 1; public bool RemoveCompletedDownloads { get; set; } = true; public bool RemoveFailedDownloads { get; set; } = true; + + public bool Equals(DownloadClientDefinition other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as DownloadClientDefinition); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } } } diff --git a/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs b/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs index 208a70bf0..1f6b387cc 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/AniListSettingsBase.cs @@ -29,18 +29,17 @@ namespace NzbDrone.Core.ImportLists.AniList } } - public class AniListSettingsBase<TSettings> : IImportListSettings + public class AniListSettingsBase<TSettings> : ImportListSettingsBase<TSettings> where TSettings : AniListSettingsBase<TSettings> { - protected virtual AbstractValidator<TSettings> Validator => new AniListSettingsBaseValidator<TSettings>(); + private static readonly AniListSettingsBaseValidator<TSettings> Validator = new (); public AniListSettingsBase() { - BaseUrl = "https://graphql.anilist.co"; SignIn = "startOAuth"; } - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } = "https://graphql.anilist.co"; [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } @@ -54,7 +53,7 @@ namespace NzbDrone.Core.ImportLists.AniList [FieldDefinition(99, Label = "ImportListsAniListSettingsAuthenticateWithAniList", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); } diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs index 3f6c34b14..3a2511be0 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListSettings.cs @@ -1,12 +1,12 @@ using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.AniList.List { public class AniListSettingsValidator : AniListSettingsBaseValidator<AniListSettings> { public AniListSettingsValidator() - : base() { RuleFor(c => c.Username).NotEmpty(); @@ -18,10 +18,11 @@ namespace NzbDrone.Core.ImportLists.AniList.List public class AniListSettings : AniListSettingsBase<AniListSettings> { - public const string sectionImport = "Import List Status"; + public const string SectionImport = "Import List Status"; + + private static readonly AniListSettingsValidator Validator = new (); public AniListSettings() - : base() { ImportCurrent = true; ImportPlanning = true; @@ -29,42 +30,45 @@ namespace NzbDrone.Core.ImportLists.AniList.List ImportFinished = true; } - protected override AbstractValidator<AniListSettings> Validator => new AniListSettingsValidator(); - [FieldDefinition(1, Label = "Username", HelpText = "ImportListsAniListSettingsUsernameHelpText")] public string Username { get; set; } - [FieldDefinition(2, Label = "ImportListsAniListSettingsImportWatching", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportWatchingHelpText")] + [FieldDefinition(2, Label = "ImportListsAniListSettingsImportWatching", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportWatchingHelpText")] public bool ImportCurrent { get; set; } - [FieldDefinition(3, Label = "ImportListsAniListSettingsImportPlanning", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPlanningHelpText")] + [FieldDefinition(3, Label = "ImportListsAniListSettingsImportPlanning", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportPlanningHelpText")] public bool ImportPlanning { get; set; } - [FieldDefinition(4, Label = "ImportListsAniListSettingsImportCompleted", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCompletedHelpText")] + [FieldDefinition(4, Label = "ImportListsAniListSettingsImportCompleted", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportCompletedHelpText")] public bool ImportCompleted { get; set; } - [FieldDefinition(5, Label = "ImportListsAniListSettingsImportDropped", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportDroppedHelpText")] + [FieldDefinition(5, Label = "ImportListsAniListSettingsImportDropped", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportDroppedHelpText")] public bool ImportDropped { get; set; } - [FieldDefinition(6, Label = "ImportListsAniListSettingsImportPaused", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPausedHelpText")] + [FieldDefinition(6, Label = "ImportListsAniListSettingsImportPaused", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportPausedHelpText")] public bool ImportPaused { get; set; } - [FieldDefinition(7, Label = "ImportListsAniListSettingsImportRepeating", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportRepeatingHelpText")] + [FieldDefinition(7, Label = "ImportListsAniListSettingsImportRepeating", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportRepeatingHelpText")] public bool ImportRepeating { get; set; } - [FieldDefinition(8, Label = "ImportListsAniListSettingsImportFinished", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportFinishedHelpText")] + [FieldDefinition(8, Label = "ImportListsAniListSettingsImportFinished", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportFinishedHelpText")] public bool ImportFinished { get; set; } - [FieldDefinition(9, Label = "ImportListsAniListSettingsImportReleasing", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportReleasingHelpText")] + [FieldDefinition(9, Label = "ImportListsAniListSettingsImportReleasing", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportReleasingHelpText")] public bool ImportReleasing { get; set; } - [FieldDefinition(10, Label = "ImportListsAniListSettingsImportNotYetReleased", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportNotYetReleasedHelpText")] + [FieldDefinition(10, Label = "ImportListsAniListSettingsImportNotYetReleased", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportNotYetReleasedHelpText")] public bool ImportUnreleased { get; set; } - [FieldDefinition(11, Label = "ImportListsAniListSettingsImportCancelled", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCancelledHelpText")] + [FieldDefinition(11, Label = "ImportListsAniListSettingsImportCancelled", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportCancelledHelpText")] public bool ImportCancelled { get; set; } - [FieldDefinition(12, Label = "ImportListsAniListSettingsImportHiatus", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportHiatusHelpText")] + [FieldDefinition(12, Label = "ImportListsAniListSettingsImportHiatus", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportHiatusHelpText")] public bool ImportHiatus { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs index ef3e98f76..86aec5148 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomSettings.cs @@ -13,19 +13,14 @@ namespace NzbDrone.Core.ImportLists.Custom } } - public class CustomSettings : IImportListSettings + public class CustomSettings : ImportListSettingsBase<CustomSettings> { - private static readonly CustomSettingsValidator Validator = new CustomSettingsValidator(); - - public CustomSettings() - { - BaseUrl = ""; - } + private static readonly CustomSettingsValidator Validator = new (); [FieldDefinition(0, Label = "ImportListsCustomListSettingsUrl", HelpText = "ImportListsCustomListSettingsUrlHelpText")] - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } = string.Empty; - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs index 50a7934c0..7ab193e34 100644 --- a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListSettings.cs @@ -14,16 +14,16 @@ namespace NzbDrone.Core.ImportLists.Imdb } } - public class ImdbListSettings : IImportListSettings + public class ImdbListSettings : ImportListSettingsBase<ImdbListSettings> { - private static readonly ImdbSettingsValidator Validator = new ImdbSettingsValidator(); + private static readonly ImdbSettingsValidator Validator = new (); - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } [FieldDefinition(1, Label = "ImportListsImdbSettingsListId", HelpText = "ImportListsImdbSettingsListIdHelpText")] public string ListId { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs index 31b99c23c..5ce3e9f7a 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs @@ -1,11 +1,14 @@ using System; +using Equ; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; namespace NzbDrone.Core.ImportLists { - public class ImportListDefinition : ProviderDefinition + public class ImportListDefinition : ProviderDefinition, IEquatable<ImportListDefinition> { + private static readonly MemberwiseEqualityComparer<ImportListDefinition> Comparer = MemberwiseEqualityComparer<ImportListDefinition>.ByProperties; + public bool EnableAutomaticAdd { get; set; } public bool SearchForMissingEpisodes { get; set; } public MonitorTypes ShouldMonitor { get; set; } @@ -15,10 +18,31 @@ namespace NzbDrone.Core.ImportLists public bool SeasonFolder { get; set; } public string RootFolderPath { get; set; } + [MemberwiseEqualityIgnore] public override bool Enable => EnableAutomaticAdd; + [MemberwiseEqualityIgnore] public ImportListStatus Status { get; set; } + + [MemberwiseEqualityIgnore] public ImportListType ListType { get; set; } + + [MemberwiseEqualityIgnore] public TimeSpan MinRefreshInterval { get; set; } + + public bool Equals(ImportListDefinition other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as ImportListDefinition); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSettingsBase.cs b/src/NzbDrone.Core/ImportLists/ImportListSettingsBase.cs new file mode 100644 index 000000000..d94726e70 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/ImportListSettingsBase.cs @@ -0,0 +1,31 @@ +using System; +using Equ; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists +{ + public abstract class ImportListSettingsBase<TSettings> : IImportListSettings, IEquatable<TSettings> + where TSettings : ImportListSettingsBase<TSettings> + { + private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties; + + public abstract string BaseUrl { get; set; } + + public abstract NzbDroneValidationResult Validate(); + + public bool Equals(TSettings other) + { + return Comparer.Equals(this as TSettings, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as TSettings); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this as TSettings); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs index aad6257c8..e4a76733d 100644 --- a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs @@ -24,16 +24,11 @@ namespace NzbDrone.Core.ImportLists.MyAnimeList } } - public class MyAnimeListSettings : IImportListSettings + public class MyAnimeListSettings : ImportListSettingsBase<MyAnimeListSettings> { - public string BaseUrl { get; set; } + private static readonly MalSettingsValidator Validator = new (); - protected AbstractValidator<MyAnimeListSettings> Validator => new MalSettingsValidator(); - - public MyAnimeListSettings() - { - BaseUrl = "https://api.myanimelist.net/v2"; - } + public override string BaseUrl { get; set; } = "https://api.myanimelist.net/v2"; [FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")] public int ListStatus { get; set; } @@ -50,7 +45,7 @@ namespace NzbDrone.Core.ImportLists.MyAnimeList [FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs index 0ffbdba2a..db5bee7d5 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexListSettings.cs @@ -14,9 +14,9 @@ namespace NzbDrone.Core.ImportLists.Plex } } - public class PlexListSettings : IImportListSettings + public class PlexListSettings : ImportListSettingsBase<PlexListSettings> { - protected virtual PlexListSettingsValidator Validator => new PlexListSettingsValidator(); + private static readonly PlexListSettingsValidator Validator = new (); public PlexListSettings() { @@ -25,7 +25,7 @@ namespace NzbDrone.Core.ImportLists.Plex public virtual string Scope => ""; - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } @@ -33,7 +33,7 @@ namespace NzbDrone.Core.ImportLists.Plex [FieldDefinition(99, Label = "ImportListsPlexSettingsAuthenticateWithPlex", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs index d6d2e1709..1df33858f 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs @@ -12,9 +12,9 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex } } - public class PlexRssImportSettings : RssImportBaseSettings + public class PlexRssImportSettings : RssImportBaseSettings<PlexRssImportSettings> { - private PlexRssImportSettingsValidator Validator => new (); + private static readonly PlexRssImportSettingsValidator Validator = new (); [FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")] public override string Url { get; set; } diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs index f00a66253..ca711b2e2 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Parser; namespace NzbDrone.Core.ImportLists.Rss { public class RssImportBase<TSettings> : HttpImportListBase<TSettings> - where TSettings : RssImportBaseSettings, new() + where TSettings : RssImportBaseSettings<TSettings>, new() { public override string Name => "RSS List Base"; public override ImportListType ListType => ImportListType.Advanced; @@ -36,7 +36,7 @@ namespace NzbDrone.Core.ImportLists.Rss public override IImportListRequestGenerator GetRequestGenerator() { - return new RssImportRequestGenerator + return new RssImportRequestGenerator<TSettings> { Settings = Settings }; diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs index 9df9d4dd0..caf37e4cc 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs @@ -4,7 +4,8 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Rss { - public class RssImportSettingsValidator : AbstractValidator<RssImportBaseSettings> + public class RssImportSettingsValidator<TSettings> : AbstractValidator<TSettings> + where TSettings : RssImportBaseSettings<TSettings> { public RssImportSettingsValidator() { @@ -12,18 +13,19 @@ namespace NzbDrone.Core.ImportLists.Rss } } - public class RssImportBaseSettings : IImportListSettings + public class RssImportBaseSettings<TSettings> : ImportListSettingsBase<TSettings> + where TSettings : RssImportBaseSettings<TSettings> { - private RssImportSettingsValidator Validator => new (); + private static readonly RssImportSettingsValidator<TSettings> Validator = new (); - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } [FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox)] public virtual string Url { get; set; } - public virtual NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { - return new NzbDroneValidationResult(Validator.Validate(this)); + return new NzbDroneValidationResult(Validator.Validate(this as TSettings)); } } } diff --git a/src/NzbDrone.Core/ImportLists/Rss/RssImportRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Rss/RssImportRequestGenerator.cs index f55c181a7..7f35ac58e 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/RssImportRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/RssImportRequestGenerator.cs @@ -3,9 +3,10 @@ using NzbDrone.Common.Http; namespace NzbDrone.Core.ImportLists.Rss { - public class RssImportRequestGenerator : IImportListRequestGenerator + public class RssImportRequestGenerator<TSettings> : IImportListRequestGenerator + where TSettings : RssImportBaseSettings<TSettings>, new() { - public RssImportBaseSettings Settings { get; set; } + public RssImportBaseSettings<TSettings> Settings { get; set; } public virtual ImportListPageableRequestChain GetListItems() { @@ -18,9 +19,7 @@ namespace NzbDrone.Core.ImportLists.Rss private IEnumerable<ImportListRequest> GetSeriesRequest() { - var request = new ImportListRequest(Settings.Url, HttpAccept.Rss); - - yield return request; + yield return new ImportListRequest(Settings.Url, HttpAccept.Rss); } } } diff --git a/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs index 13deba893..d0b4cd3e3 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/SimklSettingsBase.cs @@ -24,18 +24,17 @@ namespace NzbDrone.Core.ImportLists.Simkl } } - public class SimklSettingsBase<TSettings> : IImportListSettings + public class SimklSettingsBase<TSettings> : ImportListSettingsBase<TSettings> where TSettings : SimklSettingsBase<TSettings> { - protected virtual AbstractValidator<TSettings> Validator => new SimklSettingsBaseValidator<TSettings>(); + private static readonly SimklSettingsBaseValidator<TSettings> Validator = new (); public SimklSettingsBase() { - BaseUrl = "https://api.simkl.com"; SignIn = "startOAuth"; } - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } = "https://api.simkl.com"; [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } @@ -52,7 +51,7 @@ namespace NzbDrone.Core.ImportLists.Simkl [FieldDefinition(99, Label = "ImportListsSimklSettingsAuthenticatewithSimkl", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); } diff --git a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs index d5342c578..7ce5f34ee 100644 --- a/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Simkl/User/SimklUserSettings.cs @@ -1,12 +1,12 @@ using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Simkl.User { public class SimklUserSettingsValidator : SimklSettingsBaseValidator<SimklUserSettings> { public SimklUserSettingsValidator() - : base() { RuleFor(c => c.ListType).NotNull(); } @@ -14,7 +14,7 @@ namespace NzbDrone.Core.ImportLists.Simkl.User public class SimklUserSettings : SimklSettingsBase<SimklUserSettings> { - protected override AbstractValidator<SimklUserSettings> Validator => new SimklUserSettingsValidator(); + private static readonly SimklUserSettingsValidator Validator = new (); public SimklUserSettings() { @@ -27,5 +27,10 @@ namespace NzbDrone.Core.ImportLists.Simkl.User [FieldDefinition(1, Label = "ImportListsSimklSettingsShowType", Type = FieldType.Select, SelectOptions = typeof(SimklUserShowType), HelpText = "ImportListsSimklSettingsShowTypeHelpText")] public int ShowType { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs index 77772f003..af1bbd0f7 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrSettings.cs @@ -15,13 +15,12 @@ namespace NzbDrone.Core.ImportLists.Sonarr } } - public class SonarrSettings : IImportListSettings + public class SonarrSettings : ImportListSettingsBase<SonarrSettings> { - private static readonly SonarrSettingsValidator Validator = new SonarrSettingsValidator(); + private static readonly SonarrSettingsValidator Validator = new (); public SonarrSettings() { - BaseUrl = ""; ApiKey = ""; ProfileIds = Array.Empty<int>(); LanguageProfileIds = Array.Empty<int>(); @@ -30,7 +29,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr } [FieldDefinition(0, Label = "ImportListsSonarrSettingsFullUrl", HelpText = "ImportListsSonarrSettingsFullUrlHelpText")] - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } = string.Empty; [FieldDefinition(1, Label = "ApiKey", HelpText = "ImportListsSonarrSettingsApiKeyHelpText")] public string ApiKey { get; set; } @@ -51,7 +50,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr [FieldDefinition(6, Type = FieldType.Select, SelectOptionsProviderAction = "getLanguageProfiles", Label = "Language Profiles", HelpText = "Language Profiles from the source instance to import from")] public IEnumerable<int> LanguageProfileIds { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs index d6fa02405..62da0dd78 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs @@ -1,12 +1,12 @@ using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Trakt.List { public class TraktListSettingsValidator : TraktSettingsBaseValidator<TraktListSettings> { public TraktListSettingsValidator() - : base() { RuleFor(c => c.Username).NotEmpty(); RuleFor(c => c.Listname).NotEmpty(); @@ -15,12 +15,17 @@ namespace NzbDrone.Core.ImportLists.Trakt.List public class TraktListSettings : TraktSettingsBase<TraktListSettings> { - protected override AbstractValidator<TraktListSettings> Validator => new TraktListSettingsValidator(); + private static readonly TraktListSettingsValidator Validator = new (); [FieldDefinition(1, Label = "Username", HelpText = "ImportListsTraktSettingsUsernameHelpText")] public string Username { get; set; } [FieldDefinition(2, Label = "ImportListsTraktSettingsListName", HelpText = "ImportListsTraktSettingsListNameHelpText")] public string Listname { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs index 6d2fef5fb..43e2e994e 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs @@ -2,13 +2,13 @@ using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Trakt.Popular { public class TraktPopularSettingsValidator : TraktSettingsBaseValidator<TraktPopularSettings> { public TraktPopularSettingsValidator() - : base() { RuleFor(c => c.TraktListType).NotNull(); @@ -28,7 +28,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular public class TraktPopularSettings : TraktSettingsBase<TraktPopularSettings> { - protected override AbstractValidator<TraktPopularSettings> Validator => new TraktPopularSettingsValidator(); + private static readonly TraktPopularSettingsValidator Validator = new (); public TraktPopularSettings() { @@ -46,5 +46,10 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsHelpText")] public string Years { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs index 91ebf19be..5b87ef7cf 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs @@ -34,19 +34,18 @@ namespace NzbDrone.Core.ImportLists.Trakt } } - public class TraktSettingsBase<TSettings> : IImportListSettings + public class TraktSettingsBase<TSettings> : ImportListSettingsBase<TSettings> where TSettings : TraktSettingsBase<TSettings> { - protected virtual AbstractValidator<TSettings> Validator => new TraktSettingsBaseValidator<TSettings>(); + private static readonly TraktSettingsBaseValidator<TSettings> Validator = new (); public TraktSettingsBase() { - BaseUrl = "https://api.trakt.tv"; SignIn = "startOAuth"; Limit = 100; } - public string BaseUrl { get; set; } + public override string BaseUrl { get; set; } = "https://api.trakt.tv"; [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AccessToken { get; set; } @@ -69,7 +68,7 @@ namespace NzbDrone.Core.ImportLists.Trakt [FieldDefinition(99, Label = "ImportListsTraktSettingsAuthenticateWithTrakt", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs index 87becf6f0..9272a358d 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -1,12 +1,12 @@ using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.Trakt.User { public class TraktUserSettingsValidator : TraktSettingsBaseValidator<TraktUserSettings> { public TraktUserSettingsValidator() - : base() { RuleFor(c => c.TraktListType).NotNull(); RuleFor(c => c.TraktWatchedListType).NotNull(); @@ -16,7 +16,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.User public class TraktUserSettings : TraktSettingsBase<TraktUserSettings> { - protected override AbstractValidator<TraktUserSettings> Validator => new TraktUserSettingsValidator(); + private static readonly TraktUserSettingsValidator Validator = new (); public TraktUserSettings() { @@ -36,6 +36,11 @@ namespace NzbDrone.Core.ImportLists.Trakt.User [FieldDefinition(4, Label = "Username", HelpText = "ImportListsTraktSettingsUserListUsernameHelpText")] public string Username { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } public enum TraktUserWatchSorting diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index e424a46f8..af0169502 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -18,7 +19,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet } } - public class BroadcastheNetSettings : ITorrentIndexerSettings + public class BroadcastheNetSettings : PropertywiseEquatable<BroadcastheNetSettings>, ITorrentIndexerSettings { private static readonly BroadcastheNetSettingsValidator Validator = new (); diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index 57abc672e..fe46ab0dd 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -15,9 +16,9 @@ namespace NzbDrone.Core.Indexers.Fanzub } } - public class FanzubSettings : IIndexerSettings + public class FanzubSettings : PropertywiseEquatable<FanzubSettings>, IIndexerSettings { - private static readonly FanzubSettingsValidator Validator = new FanzubSettingsValidator(); + private static readonly FanzubSettingsValidator Validator = new (); public FanzubSettings() { diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 13846a25f..1c8c7477d 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -19,9 +20,9 @@ namespace NzbDrone.Core.Indexers.FileList } } - public class FileListSettings : ITorrentIndexerSettings + public class FileListSettings : PropertywiseEquatable<FileListSettings>, ITorrentIndexerSettings { - private static readonly FileListSettingsValidator Validator = new FileListSettingsValidator(); + private static readonly FileListSettingsValidator Validator = new (); public FileListSettings() { @@ -61,7 +62,7 @@ namespace NzbDrone.Core.Indexers.FileList public int MinimumSeeders { get; set; } [FieldDefinition(7)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index 7d90cfa40..8bab1adde 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -18,7 +19,7 @@ namespace NzbDrone.Core.Indexers.HDBits } } - public class HDBitsSettings : ITorrentIndexerSettings + public class HDBitsSettings : PropertywiseEquatable<HDBitsSettings>, ITorrentIndexerSettings { private static readonly HDBitsSettingsValidator Validator = new (); diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index 841c98ebf..f9db8deec 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using Equ; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -25,9 +26,9 @@ namespace NzbDrone.Core.Indexers.IPTorrents } } - public class IPTorrentsSettings : ITorrentIndexerSettings + public class IPTorrentsSettings : PropertywiseEquatable<IPTorrentsSettings>, ITorrentIndexerSettings { - private static readonly IPTorrentsSettingsValidator Validator = new IPTorrentsSettingsValidator(); + private static readonly IPTorrentsSettingsValidator Validator = new (); public IPTorrentsSettings() { @@ -42,7 +43,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents public int MinimumSeeders { get; set; } [FieldDefinition(2)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(3, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs index 882624ab4..b6e0326d6 100644 --- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs +++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs @@ -1,9 +1,13 @@ +using System; +using Equ; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers { - public class IndexerDefinition : ProviderDefinition + public class IndexerDefinition : ProviderDefinition, IEquatable<IndexerDefinition> { + private static readonly MemberwiseEqualityComparer<IndexerDefinition> Comparer = MemberwiseEqualityComparer<IndexerDefinition>.ByProperties; + public const int DefaultPriority = 25; public IndexerDefinition() @@ -11,18 +15,41 @@ namespace NzbDrone.Core.Indexers Priority = DefaultPriority; } + [MemberwiseEqualityIgnore] + public DownloadProtocol Protocol { get; set; } + + [MemberwiseEqualityIgnore] + public bool SupportsRss { get; set; } + + [MemberwiseEqualityIgnore] + public bool SupportsSearch { get; set; } + public bool EnableRss { get; set; } public bool EnableAutomaticSearch { get; set; } public bool EnableInteractiveSearch { get; set; } public int DownloadClientId { get; set; } - public DownloadProtocol Protocol { get; set; } - public bool SupportsRss { get; set; } - public bool SupportsSearch { get; set; } public int Priority { get; set; } public int SeasonSearchMaximumSingleEpisodeAge { get; set; } + [MemberwiseEqualityIgnore] public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch; + [MemberwiseEqualityIgnore] public IndexerStatus Status { get; set; } + + public bool Equals(IndexerDefinition other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as IndexerDefinition); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index b329140ea..36240529d 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Equ; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -28,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Newznab return settings.BaseUrl != null && ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); } - private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled); + private static readonly Regex AdditionalParametersRegex = new (@"(&.+?\=.+?)+", RegexOptions.Compiled); public NewznabSettingsValidator() { @@ -48,9 +49,9 @@ namespace NzbDrone.Core.Indexers.Newznab } } - public class NewznabSettings : IIndexerSettings + public class NewznabSettings : PropertywiseEquatable<NewznabSettings>, IIndexerSettings { - private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator(); + private static readonly NewznabSettingsValidator Validator = new (); public NewznabSettings() { diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 516c34604..d960a77cc 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -19,9 +20,9 @@ namespace NzbDrone.Core.Indexers.Nyaa } } - public class NyaaSettings : ITorrentIndexerSettings + public class NyaaSettings : PropertywiseEquatable<NyaaSettings>, ITorrentIndexerSettings { - private static readonly NyaaSettingsValidator Validator = new NyaaSettingsValidator(); + private static readonly NyaaSettingsValidator Validator = new (); public NyaaSettings() { @@ -44,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Nyaa public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs index ff3d593fb..144b69f1a 100644 --- a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -1,3 +1,4 @@ +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -46,7 +47,7 @@ namespace NzbDrone.Core.Indexers } } - public class SeedCriteriaSettings + public class SeedCriteriaSettings : PropertywiseEquatable<SeedCriteriaSettings> { [FieldDefinition(0, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")] public double? SeedRatio { get; set; } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 5b3d4f3ef..baefcb04b 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -17,9 +18,9 @@ namespace NzbDrone.Core.Indexers.TorrentRss } } - public class TorrentRssIndexerSettings : ITorrentIndexerSettings + public class TorrentRssIndexerSettings : PropertywiseEquatable<TorrentRssIndexerSettings>, ITorrentIndexerSettings { - private static readonly TorrentRssIndexerSettingsValidator Validator = new TorrentRssIndexerSettingsValidator(); + private static readonly TorrentRssIndexerSettingsValidator Validator = new (); public TorrentRssIndexerSettings() { @@ -42,7 +43,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index 47713230d..12415e24a 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Equ; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; @@ -18,9 +19,9 @@ namespace NzbDrone.Core.Indexers.Torrentleech } } - public class TorrentleechSettings : ITorrentIndexerSettings + public class TorrentleechSettings : PropertywiseEquatable<TorrentleechSettings>, ITorrentIndexerSettings { - private static readonly TorrentleechSettingsValidator Validator = new TorrentleechSettingsValidator(); + private static readonly TorrentleechSettingsValidator Validator = new (); public TorrentleechSettings() { @@ -39,7 +40,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech public int MinimumSeeders { get; set; } [FieldDefinition(3)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 6a84b59cd..1936529a7 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Text.RegularExpressions; +using Equ; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -18,7 +19,7 @@ namespace NzbDrone.Core.Indexers.Torznab return settings.BaseUrl != null && ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); } - private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled); + private static readonly Regex AdditionalParametersRegex = new (@"(&.+?\=.+?)+", RegexOptions.Compiled); public TorznabSettingsValidator() { @@ -40,9 +41,11 @@ namespace NzbDrone.Core.Indexers.Torznab } } - public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings + public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings, IEquatable<TorznabSettings> { - private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator(); + private static readonly TorznabSettingsValidator Validator = new (); + + private static readonly MemberwiseEqualityComparer<TorznabSettings> Comparer = MemberwiseEqualityComparer<TorznabSettings>.ByProperties; public TorznabSettings() { @@ -53,7 +56,7 @@ namespace NzbDrone.Core.Indexers.Torznab public int MinimumSeeders { get; set; } [FieldDefinition(9)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); [FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } @@ -62,5 +65,20 @@ namespace NzbDrone.Core.Indexers.Torznab { return new NzbDroneValidationResult(Validator.Validate(this)); } + + public bool Equals(TorznabSettings other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as TorznabSettings); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } } } diff --git a/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs b/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs index 3b86e20ed..978305318 100644 --- a/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs +++ b/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Apprise @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Apprise } } - public class AppriseSettings : IProviderConfig + public class AppriseSettings : NotificationSettingsBase<AppriseSettings> { private static readonly AppriseSettingsValidator Validator = new (); @@ -66,7 +65,7 @@ namespace NzbDrone.Core.Notifications.Apprise [FieldDefinition(7, Label = "Password", Type = FieldType.Password, HelpText = "NotificationsAppriseSettingsPasswordHelpText", Privacy = PrivacyLevel.Password)] public string AuthPassword { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs index 8c52308e5..ecd23d76b 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs @@ -1,6 +1,5 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -16,9 +15,9 @@ namespace NzbDrone.Core.Notifications.CustomScript } } - public class CustomScriptSettings : IProviderConfig + public class CustomScriptSettings : NotificationSettingsBase<CustomScriptSettings> { - private static readonly CustomScriptSettingsValidator Validator = new CustomScriptSettingsValidator(); + private static readonly CustomScriptSettingsValidator Validator = new (); [FieldDefinition(0, Label = "Path", Type = FieldType.FilePath)] public string Path { get; set; } @@ -26,7 +25,7 @@ namespace NzbDrone.Core.Notifications.CustomScript [FieldDefinition(1, Label = "NotificationsCustomScriptSettingsArguments", HelpText = "NotificationsCustomScriptSettingsArgumentsHelpText", Hidden = HiddenType.HiddenIfNotSet)] public string Arguments { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs index 8dbb68ba4..ab6e53dcf 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Discord @@ -14,7 +13,7 @@ namespace NzbDrone.Core.Notifications.Discord } } - public class DiscordSettings : IProviderConfig + public class DiscordSettings : NotificationSettingsBase<DiscordSettings> { public DiscordSettings() { @@ -89,7 +88,7 @@ namespace NzbDrone.Core.Notifications.Discord [FieldDefinition(6, Label = "NotificationsDiscordSettingsOnManualInteractionFields", Advanced = true, SelectOptions = typeof(DiscordManualInteractionFieldType), HelpText = "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText", Type = FieldType.Select)] public IEnumerable<int> ManualInteractionFields { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index f6b23caad..d01feda74 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Email @@ -26,9 +25,9 @@ namespace NzbDrone.Core.Notifications.Email } } - public class EmailSettings : IProviderConfig + public class EmailSettings : NotificationSettingsBase<EmailSettings> { - private static readonly EmailSettingsValidator Validator = new EmailSettingsValidator(); + private static readonly EmailSettingsValidator Validator = new (); public EmailSettings() { @@ -66,7 +65,7 @@ namespace NzbDrone.Core.Notifications.Email [FieldDefinition(8, Label = "NotificationsEmailSettingsBccAddress", HelpText = "NotificationsEmailSettingsBccAddressHelpText", Advanced = true)] public IEnumerable<string> Bcc { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs b/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs index 580ae41d1..b152ef5fe 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Gotify @@ -14,9 +13,9 @@ namespace NzbDrone.Core.Notifications.Gotify } } - public class GotifySettings : IProviderConfig + public class GotifySettings : NotificationSettingsBase<GotifySettings> { - private static readonly GotifySettingsValidator Validator = new GotifySettingsValidator(); + private static readonly GotifySettingsValidator Validator = new (); public GotifySettings() { @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Gotify [FieldDefinition(3, Label = "NotificationsGotifySettingIncludeSeriesPoster", Type = FieldType.Checkbox, HelpText = "NotificationsGotifySettingIncludeSeriesPosterHelpText")] public bool IncludeSeriesPoster { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Join/JoinSettings.cs b/src/NzbDrone.Core/Notifications/Join/JoinSettings.cs index bd3046240..9bc429014 100644 --- a/src/NzbDrone.Core/Notifications/Join/JoinSettings.cs +++ b/src/NzbDrone.Core/Notifications/Join/JoinSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Join @@ -14,15 +13,15 @@ namespace NzbDrone.Core.Notifications.Join } } - public class JoinSettings : IProviderConfig + public class JoinSettings : NotificationSettingsBase<JoinSettings> { + private static readonly JoinSettingsValidator Validator = new (); + public JoinSettings() { Priority = (int)JoinPriority.Normal; } - private static readonly JoinSettingsValidator Validator = new JoinSettingsValidator(); - [FieldDefinition(0, Label = "ApiKey", HelpText = "NotificationsJoinSettingsApiKeyHelpText", HelpLink = "https://joinjoaomgcd.appspot.com/")] public string ApiKey { get; set; } @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Join [FieldDefinition(3, Label = "NotificationsJoinSettingsNotificationPriority", Type = FieldType.Select, SelectOptions = typeof(JoinPriority))] public int Priority { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Mailgun/MailgunSettings.cs b/src/NzbDrone.Core/Notifications/Mailgun/MailgunSettings.cs index 42802abc0..2e267cddd 100644 --- a/src/NzbDrone.Core/Notifications/Mailgun/MailgunSettings.cs +++ b/src/NzbDrone.Core/Notifications/Mailgun/MailgunSettings.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Mailgun @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Notifications.Mailgun } } - public class MailgunSettings : IProviderConfig + public class MailgunSettings : NotificationSettingsBase<MailgunSettings> { - private static readonly MailGunSettingsValidator Validator = new MailGunSettingsValidator(); + private static readonly MailGunSettingsValidator Validator = new (); public MailgunSettings() { @@ -41,7 +40,7 @@ namespace NzbDrone.Core.Notifications.Mailgun [FieldDefinition(4, Label = "NotificationsEmailSettingsRecipientAddress", Type = FieldType.Tag)] public IEnumerable<string> Recipients { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index 1d04e9054..369888ceb 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -2,7 +2,6 @@ using FluentValidation; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Emby @@ -19,9 +18,9 @@ namespace NzbDrone.Core.Notifications.Emby } } - public class MediaBrowserSettings : IProviderConfig + public class MediaBrowserSettings : NotificationSettingsBase<MediaBrowserSettings> { - private static readonly MediaBrowserSettingsValidator Validator = new MediaBrowserSettingsValidator(); + private static readonly MediaBrowserSettingsValidator Validator = new (); public MediaBrowserSettings() { @@ -65,7 +64,7 @@ namespace NzbDrone.Core.Notifications.Emby public bool IsValid => !string.IsNullOrWhiteSpace(Host) && Port > 0; - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrSettings.cs b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrSettings.cs index dd610b3ba..e0faaede2 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrSettings.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Notifiarr @@ -13,14 +12,14 @@ namespace NzbDrone.Core.Notifications.Notifiarr } } - public class NotifiarrSettings : IProviderConfig + public class NotifiarrSettings : NotificationSettingsBase<NotifiarrSettings> { - private static readonly NotifiarrSettingsValidator Validator = new NotifiarrSettingsValidator(); + private static readonly NotifiarrSettingsValidator Validator = new (); [FieldDefinition(0, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "NotificationsNotifiarrSettingsApiKeyHelpText", HelpLink = "https://notifiarr.com")] public string ApiKey { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs index 95fabd545..7c5806833 100644 --- a/src/NzbDrone.Core/Notifications/NotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications { public abstract class NotificationBase<TSettings> : INotification - where TSettings : IProviderConfig, new() + where TSettings : NotificationSettingsBase<TSettings>, new() { protected const string EPISODE_GRABBED_TITLE = "Episode Grabbed"; protected const string EPISODE_DOWNLOADED_TITLE = "Episode Downloaded"; diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index 5095caa3a..d848777e1 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -1,9 +1,13 @@ +using System; +using Equ; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Notifications { - public class NotificationDefinition : ProviderDefinition + public class NotificationDefinition : ProviderDefinition, IEquatable<NotificationDefinition> { + private static readonly MemberwiseEqualityComparer<NotificationDefinition> Comparer = MemberwiseEqualityComparer<NotificationDefinition>.ByProperties; + public bool OnGrab { get; set; } public bool OnDownload { get; set; } public bool OnUpgrade { get; set; } @@ -13,23 +17,63 @@ namespace NzbDrone.Core.Notifications public bool OnEpisodeFileDelete { get; set; } public bool OnEpisodeFileDeleteForUpgrade { get; set; } public bool OnHealthIssue { get; set; } + public bool IncludeHealthWarnings { get; set; } public bool OnHealthRestored { get; set; } public bool OnApplicationUpdate { get; set; } public bool OnManualInteractionRequired { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnGrab { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnDownload { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnUpgrade { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnRename { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnSeriesAdd { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnSeriesDelete { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnEpisodeFileDelete { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnHealthIssue { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnHealthRestored { get; set; } - public bool IncludeHealthWarnings { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnApplicationUpdate { get; set; } + + [MemberwiseEqualityIgnore] public bool SupportsOnManualInteractionRequired { get; set; } + [MemberwiseEqualityIgnore] public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnRename || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; + + public bool Equals(NotificationDefinition other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as NotificationDefinition); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } } } diff --git a/src/NzbDrone.Core/Notifications/NotificationSettingsBase.cs b/src/NzbDrone.Core/Notifications/NotificationSettingsBase.cs new file mode 100644 index 000000000..98b5d34b7 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/NotificationSettingsBase.cs @@ -0,0 +1,30 @@ +using System; +using Equ; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications +{ + public abstract class NotificationSettingsBase<TSettings> : IProviderConfig, IEquatable<TSettings> + where TSettings : NotificationSettingsBase<TSettings> + { + private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties; + + public abstract NzbDroneValidationResult Validate(); + + public bool Equals(TSettings other) + { + return Comparer.Equals(this as TSettings, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as TSettings); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this as TSettings); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs b/src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs index 93ccba5ed..0c6075f01 100644 --- a/src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs +++ b/src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Ntfy @@ -21,12 +20,12 @@ namespace NzbDrone.Core.Notifications.Ntfy RuleForEach(c => c.Topics).NotEmpty().Matches("[a-zA-Z0-9_-]+").Must(c => !InvalidTopics.Contains(c)).WithMessage("Invalid topic"); } - private static List<string> InvalidTopics => new List<string> { "announcements", "app", "docs", "settings", "stats", "mytopic-rw", "mytopic-ro", "mytopic-wo" }; + private static List<string> InvalidTopics => new () { "announcements", "app", "docs", "settings", "stats", "mytopic-rw", "mytopic-ro", "mytopic-wo" }; } - public class NtfySettings : IProviderConfig + public class NtfySettings : NotificationSettingsBase<NtfySettings> { - private static readonly NtfySettingsValidator Validator = new NtfySettingsValidator(); + private static readonly NtfySettingsValidator Validator = new (); public NtfySettings() { @@ -59,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Ntfy [FieldDefinition(7, Label = "NotificationsNtfySettingsClickUrl", Type = FieldType.Url, HelpText = "NotificationsNtfySettingsClickUrlHelpText")] public string ClickUrl { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index de76f9eef..50ba7f757 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -1,7 +1,6 @@ using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Plex.Server @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Notifications.Plex.Server } } - public class PlexServerSettings : IProviderConfig + public class PlexServerSettings : NotificationSettingsBase<PlexServerSettings> { - private static readonly PlexServerSettingsValidator Validator = new PlexServerSettingsValidator(); + private static readonly PlexServerSettingsValidator Validator = new (); public PlexServerSettings() { @@ -62,7 +61,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public bool IsValid => !string.IsNullOrWhiteSpace(Host); - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Prowl/ProwlSettings.cs b/src/NzbDrone.Core/Notifications/Prowl/ProwlSettings.cs index 4add832cb..0973f202e 100644 --- a/src/NzbDrone.Core/Notifications/Prowl/ProwlSettings.cs +++ b/src/NzbDrone.Core/Notifications/Prowl/ProwlSettings.cs @@ -1,6 +1,5 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Prowl @@ -13,9 +12,9 @@ namespace NzbDrone.Core.Notifications.Prowl } } - public class ProwlSettings : IProviderConfig + public class ProwlSettings : NotificationSettingsBase<ProwlSettings> { - private static readonly ProwlSettingsValidator Validator = new ProwlSettingsValidator(); + private static readonly ProwlSettingsValidator Validator = new (); [FieldDefinition(0, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.prowlapp.com/api_settings.php")] public string ApiKey { get; set; } @@ -25,7 +24,7 @@ namespace NzbDrone.Core.Notifications.Prowl public bool IsValid => !string.IsNullOrWhiteSpace(ApiKey) && Priority >= -2 && Priority <= 2; - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs index a50aee65d..67fb8f5a2 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.PushBullet @@ -15,9 +14,9 @@ namespace NzbDrone.Core.Notifications.PushBullet } } - public class PushBulletSettings : IProviderConfig + public class PushBulletSettings : NotificationSettingsBase<PushBulletSettings> { - private static readonly PushBulletSettingsValidator Validator = new PushBulletSettingsValidator(); + private static readonly PushBulletSettingsValidator Validator = new (); public PushBulletSettings() { @@ -37,7 +36,7 @@ namespace NzbDrone.Core.Notifications.PushBullet [FieldDefinition(3, Label = "NotificationsPushBulletSettingSenderId", HelpText = "NotificationsPushBulletSettingSenderIdHelpText")] public string SenderId { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs index cb4d44ce7..5b94fcef4 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Pushcut @@ -14,7 +13,7 @@ namespace NzbDrone.Core.Notifications.Pushcut } } - public class PushcutSettings : IProviderConfig + public class PushcutSettings : NotificationSettingsBase<PushcutSettings> { private static readonly PushcutSettingsValidator Validator = new (); @@ -27,7 +26,7 @@ namespace NzbDrone.Core.Notifications.Pushcut [FieldDefinition(2, Label = "NotificationsPushcutSettingsTimeSensitive", Type = FieldType.Checkbox, HelpText = "NotificationsPushcutSettingsTimeSensitiveHelpText")] public bool TimeSensitive { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs b/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs index 7b6c78953..d5450860f 100644 --- a/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs +++ b/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Pushover @@ -17,9 +16,9 @@ namespace NzbDrone.Core.Notifications.Pushover } } - public class PushoverSettings : IProviderConfig + public class PushoverSettings : NotificationSettingsBase<PushoverSettings> { - private static readonly PushoverSettingsValidator Validator = new PushoverSettingsValidator(); + private static readonly PushoverSettingsValidator Validator = new (); public PushoverSettings() { @@ -51,7 +50,7 @@ namespace NzbDrone.Core.Notifications.Pushover public bool IsValid => !string.IsNullOrWhiteSpace(UserKey) && Priority >= -1 && Priority <= 2; - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/SendGrid/SendGridSettings.cs b/src/NzbDrone.Core/Notifications/SendGrid/SendGridSettings.cs index 0add1e793..75b269510 100644 --- a/src/NzbDrone.Core/Notifications/SendGrid/SendGridSettings.cs +++ b/src/NzbDrone.Core/Notifications/SendGrid/SendGridSettings.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.SendGrid @@ -18,9 +17,9 @@ namespace NzbDrone.Core.Notifications.SendGrid } } - public class SendGridSettings : IProviderConfig + public class SendGridSettings : NotificationSettingsBase<SendGridSettings> { - private static readonly SendGridSettingsValidator Validator = new SendGridSettingsValidator(); + private static readonly SendGridSettingsValidator Validator = new (); public SendGridSettings() { @@ -39,7 +38,7 @@ namespace NzbDrone.Core.Notifications.SendGrid [FieldDefinition(3, Label = "NotificationsEmailSettingsRecipientAddress", Type = FieldType.Tag)] public IEnumerable<string> Recipients { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Signal/SignalSettings.cs b/src/NzbDrone.Core/Notifications/Signal/SignalSettings.cs index 500215730..320f505b2 100644 --- a/src/NzbDrone.Core/Notifications/Signal/SignalSettings.cs +++ b/src/NzbDrone.Core/Notifications/Signal/SignalSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Signal @@ -16,7 +15,7 @@ namespace NzbDrone.Core.Notifications.Signal } } - public class SignalSettings : IProviderConfig + public class SignalSettings : NotificationSettingsBase<SignalSettings> { private static readonly SignalSettingsValidator Validator = new (); @@ -42,7 +41,7 @@ namespace NzbDrone.Core.Notifications.Signal [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "NotificationsSignalSettingsPasswordHelpText")] public string AuthPassword { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs index 2e285aeed..3768b71a5 100644 --- a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs +++ b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Simplepush @@ -13,9 +12,9 @@ namespace NzbDrone.Core.Notifications.Simplepush } } - public class SimplepushSettings : IProviderConfig + public class SimplepushSettings : NotificationSettingsBase<SimplepushSettings> { - private static readonly SimplepushSettingsValidator Validator = new SimplepushSettingsValidator(); + private static readonly SimplepushSettingsValidator Validator = new (); [FieldDefinition(0, Label = "NotificationsSimplepushSettingsKey", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://simplepush.io/features")] public string Key { get; set; } @@ -25,7 +24,7 @@ namespace NzbDrone.Core.Notifications.Simplepush public bool IsValid => !string.IsNullOrWhiteSpace(Key); - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs b/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs index bd7f10c33..07b62e438 100644 --- a/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs +++ b/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Slack @@ -14,9 +13,9 @@ namespace NzbDrone.Core.Notifications.Slack } } - public class SlackSettings : IProviderConfig + public class SlackSettings : NotificationSettingsBase<SlackSettings> { - private static readonly SlackSettingsValidator Validator = new SlackSettingsValidator(); + private static readonly SlackSettingsValidator Validator = new (); [FieldDefinition(0, Label = "NotificationsSettingsWebhookUrl", HelpText = "NotificationsSlackSettingsWebhookUrlHelpText", Type = FieldType.Url, HelpLink = "https://my.slack.com/services/new/incoming-webhook/")] public string WebHookUrl { get; set; } @@ -30,7 +29,7 @@ namespace NzbDrone.Core.Notifications.Slack [FieldDefinition(3, Label = "NotificationsSlackSettingsChannel", HelpText = "NotificationsSlackSettingsChannelHelpText", Type = FieldType.Textbox)] public string Channel { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs index 8bad70a04..966b3b4cc 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Synology @@ -9,9 +8,9 @@ namespace NzbDrone.Core.Notifications.Synology { } - public class SynologyIndexerSettings : IProviderConfig + public class SynologyIndexerSettings : NotificationSettingsBase<SynologyIndexerSettings> { - private static readonly SynologyIndexerSettingsValidator Validator = new SynologyIndexerSettingsValidator(); + private static readonly SynologyIndexerSettingsValidator Validator = new (); public SynologyIndexerSettings() { @@ -21,7 +20,7 @@ namespace NzbDrone.Core.Notifications.Synology [FieldDefinition(0, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox, HelpText = "NotificationsSynologySettingsUpdateLibraryHelpText")] public bool UpdateLibrary { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index 2b768ce45..f3e4d2499 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Telegram @@ -16,9 +15,9 @@ namespace NzbDrone.Core.Notifications.Telegram } } - public class TelegramSettings : IProviderConfig + public class TelegramSettings : NotificationSettingsBase<TelegramSettings> { - private static readonly TelegramSettingsValidator Validator = new TelegramSettingsValidator(); + private static readonly TelegramSettingsValidator Validator = new (); [FieldDefinition(0, Label = "NotificationsTelegramSettingsBotToken", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://core.telegram.org/bots")] public string BotToken { get; set; } @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] public bool IncludeAppNameInTitle { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs index 0be5677da..cee627621 100644 --- a/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs @@ -1,7 +1,6 @@ using System; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Trakt @@ -16,9 +15,9 @@ namespace NzbDrone.Core.Notifications.Trakt } } - public class TraktSettings : IProviderConfig + public class TraktSettings : NotificationSettingsBase<TraktSettings> { - private static readonly TraktSettingsValidator Validator = new TraktSettingsValidator(); + private static readonly TraktSettingsValidator Validator = new (); public TraktSettings() { @@ -40,7 +39,7 @@ namespace NzbDrone.Core.Notifications.Trakt [FieldDefinition(4, Label = "NotificationsTraktSettingsAuthenticateWithTrakt", Type = FieldType.OAuth)] public string SignIn { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs index 448c0976e..d63aac122 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs @@ -1,7 +1,6 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Twitter @@ -28,9 +27,9 @@ namespace NzbDrone.Core.Notifications.Twitter } } - public class TwitterSettings : IProviderConfig + public class TwitterSettings : NotificationSettingsBase<TwitterSettings> { - private static readonly TwitterSettingsValidator Validator = new TwitterSettingsValidator(); + private static readonly TwitterSettingsValidator Validator = new (); public TwitterSettings() { @@ -59,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Twitter [FieldDefinition(6, Label = "NotificationsTwitterSettingsConnectToTwitter", Type = FieldType.OAuth)] public string AuthorizeNotification { get; set; } - public NzbDroneValidationResult Validate() + public override 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 df2d29355..c02fb5828 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -6,13 +6,12 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tags; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Webhook { public abstract class WebhookBase<TSettings> : NotificationBase<TSettings> - where TSettings : IProviderConfig, new() + where TSettings : NotificationSettingsBase<TSettings>, new() { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs index f244c7d88..565f454e2 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs @@ -1,7 +1,6 @@ -using System; +using System; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Webhook @@ -14,9 +13,9 @@ namespace NzbDrone.Core.Notifications.Webhook } } - public class WebhookSettings : IProviderConfig + public class WebhookSettings : NotificationSettingsBase<WebhookSettings> { - private static readonly WebhookSettingsValidator Validator = new WebhookSettingsValidator(); + private static readonly WebhookSettingsValidator Validator = new (); public WebhookSettings() { @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Webhook [FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs index 97331f333..efe8741e4 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs @@ -3,7 +3,6 @@ using FluentValidation; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Xbmc @@ -18,7 +17,7 @@ namespace NzbDrone.Core.Notifications.Xbmc } } - public class XbmcSettings : IProviderConfig + public class XbmcSettings : NotificationSettingsBase<XbmcSettings> { private static readonly XbmcSettingsValidator Validator = new (); @@ -69,7 +68,7 @@ namespace NzbDrone.Core.Notifications.Xbmc [JsonIgnore] public string Address => $"{Host.ToUrlHost()}:{Port}{UrlBase}"; - public NzbDroneValidationResult Validate() + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index bcc1f7331..c124b94dd 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -1,10 +1,11 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Diacritical.Net" Version="1.0.4" /> + <PackageReference Include="Equ" Version="2.3.0" /> <PackageReference Include="MailKit" Version="3.6.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" /> <PackageReference Include="Polly" Version="8.3.1" /> diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs index 292bc4bc5..a4c5db45b 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Equ; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ThingiProvider @@ -16,20 +17,22 @@ namespace NzbDrone.Core.ThingiProvider public string Name { get; set; } [JsonIgnore] + [MemberwiseEqualityIgnore] public string ImplementationName { get; set; } public string Implementation { get; set; } public string ConfigContract { get; set; } public virtual bool Enable { get; set; } + + [MemberwiseEqualityIgnore] public ProviderMessage Message { get; set; } + public HashSet<int> Tags { get; set; } + [MemberwiseEqualityIgnore] public IProviderConfig Settings { - get - { - return _settings; - } + get => _settings; set { _settings = value; diff --git a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs index e377bfe3a..6e31390cd 100644 --- a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs +++ b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs @@ -14,6 +14,7 @@ namespace Sonarr.Api.V3.Notifications public bool OnEpisodeFileDelete { get; set; } public bool OnEpisodeFileDeleteForUpgrade { get; set; } public bool OnHealthIssue { get; set; } + public bool IncludeHealthWarnings { get; set; } public bool OnHealthRestored { get; set; } public bool OnApplicationUpdate { get; set; } public bool OnManualInteractionRequired { get; set; } @@ -29,7 +30,6 @@ namespace Sonarr.Api.V3.Notifications public bool SupportsOnHealthRestored { get; set; } public bool SupportsOnApplicationUpdate { get; set; } public bool SupportsOnManualInteractionRequired { get; set; } - public bool IncludeHealthWarnings { get; set; } public string TestCommand { get; set; } } @@ -53,6 +53,7 @@ namespace Sonarr.Api.V3.Notifications resource.OnEpisodeFileDelete = definition.OnEpisodeFileDelete; resource.OnEpisodeFileDeleteForUpgrade = definition.OnEpisodeFileDeleteForUpgrade; resource.OnHealthIssue = definition.OnHealthIssue; + resource.IncludeHealthWarnings = definition.IncludeHealthWarnings; resource.OnHealthRestored = definition.OnHealthRestored; resource.OnApplicationUpdate = definition.OnApplicationUpdate; resource.OnManualInteractionRequired = definition.OnManualInteractionRequired; @@ -66,7 +67,6 @@ namespace Sonarr.Api.V3.Notifications resource.SupportsOnEpisodeFileDeleteForUpgrade = definition.SupportsOnEpisodeFileDeleteForUpgrade; resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue; resource.SupportsOnHealthRestored = definition.SupportsOnHealthRestored; - resource.IncludeHealthWarnings = definition.IncludeHealthWarnings; resource.SupportsOnApplicationUpdate = definition.SupportsOnApplicationUpdate; resource.SupportsOnManualInteractionRequired = definition.SupportsOnManualInteractionRequired; @@ -91,6 +91,7 @@ namespace Sonarr.Api.V3.Notifications definition.OnEpisodeFileDelete = resource.OnEpisodeFileDelete; definition.OnEpisodeFileDeleteForUpgrade = resource.OnEpisodeFileDeleteForUpgrade; definition.OnHealthIssue = resource.OnHealthIssue; + definition.IncludeHealthWarnings = resource.IncludeHealthWarnings; definition.OnHealthRestored = resource.OnHealthRestored; definition.OnApplicationUpdate = resource.OnApplicationUpdate; definition.OnManualInteractionRequired = resource.OnManualInteractionRequired; @@ -104,7 +105,6 @@ namespace Sonarr.Api.V3.Notifications definition.SupportsOnEpisodeFileDeleteForUpgrade = resource.SupportsOnEpisodeFileDeleteForUpgrade; definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue; definition.SupportsOnHealthRestored = resource.SupportsOnHealthRestored; - definition.IncludeHealthWarnings = resource.IncludeHealthWarnings; definition.SupportsOnApplicationUpdate = resource.SupportsOnApplicationUpdate; definition.SupportsOnManualInteractionRequired = resource.SupportsOnManualInteractionRequired; diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index c80c41981..53586a251 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -90,10 +90,8 @@ namespace Sonarr.Api.V3 var existingDefinition = _providerFactory.Find(providerResource.Id); var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false); - // Comparing via JSON string to eliminate the need for every provider implementation to implement equality checks. // Compare settings separately because they are not serialized with the definition. - var hasDefinitionChanged = STJson.ToJson(existingDefinition) != STJson.ToJson(providerDefinition) || - STJson.ToJson(existingDefinition.Settings) != STJson.ToJson(providerDefinition.Settings); + var hasDefinitionChanged = !existingDefinition.Equals(providerDefinition) || !existingDefinition.Settings.Equals(providerDefinition.Settings); // Only test existing definitions if it is enabled and forceSave isn't set and the definition has changed. if (providerDefinition.Enable && !forceSave && hasDefinitionChanged) From 4440aa3cac545e48549b30256c2d193b23feb55f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 9 May 2024 20:09:45 +0300 Subject: [PATCH 298/762] New: Root folder exists validation for import lists --- .../Paths/RootFolderExistsValidator.cs | 25 +++++++++++++++++++ .../ImportLists/ImportListController.cs | 12 ++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs diff --git a/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs new file mode 100644 index 000000000..8b4c4e7a0 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.RootFolders; + +namespace NzbDrone.Core.Validation.Paths +{ + public class RootFolderExistsValidator : PropertyValidator + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderExistsValidator(IRootFolderService rootFolderService) + { + _rootFolderService = rootFolderService; + } + + protected override string GetDefaultMessageTemplate() => "Root folder '{path}' does not exist"; + + protected override bool IsValid(PropertyValidatorContext context) + { + context.MessageFormatter.AppendArgument("path", context.PropertyValue?.ToString()); + + return context.PropertyValue == null || _rootFolderService.All().Exists(r => r.Path.PathEquals(context.PropertyValue.ToString())); + } + } +} diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs index 542158d6a..499043f33 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs @@ -1,3 +1,4 @@ +using FluentValidation; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -11,13 +12,16 @@ namespace Sonarr.Api.V3.ImportLists public static readonly ImportListResourceMapper ResourceMapper = new (); public static readonly ImportListBulkResourceMapper BulkResourceMapper = new (); - public ImportListController(IImportListFactory importListFactory, QualityProfileExistsValidator qualityProfileExistsValidator) + public ImportListController(IImportListFactory importListFactory, RootFolderExistsValidator rootFolderExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator) : base(importListFactory, "importlist", ResourceMapper, BulkResourceMapper) { - Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); + SharedValidator.RuleFor(c => c.RootFolderPath).Cascade(CascadeMode.Stop) + .IsValidPath() + .SetValidator(rootFolderExistsValidator); - SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath(); - SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator); + SharedValidator.RuleFor(c => c.QualityProfileId).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(qualityProfileExistsValidator); } } } From 05edd44ed6dfe73c25da021ef2600e609852f7e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 9 May 2024 22:33:27 -0700 Subject: [PATCH 299/762] New: Include time for episode/season/series history --- .../Table/Cells/RelativeDateCell.js | 5 ++++- .../src/Episode/History/EpisodeHistoryRow.js | 2 ++ .../src/Series/History/SeriesHistoryModal.js | 2 +- .../src/Series/History/SeriesHistoryRow.js | 2 ++ .../src/Utilities/Date/getRelativeDate.js | 20 ++++++++++++------- src/NzbDrone.Core/Localization/Core/en.json | 6 +++++- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js index 207b97752..ed95e3014 100644 --- a/frontend/src/Components/Table/Cells/RelativeDateCell.js +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js @@ -15,6 +15,7 @@ class RelativeDateCell extends PureComponent { className, date, includeSeconds, + includeTime, showRelativeDates, shortDateFormat, longDateFormat, @@ -39,7 +40,7 @@ class RelativeDateCell extends PureComponent { title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })} {...otherProps} > - {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} + {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, includeTime, timeForToday: true })} </Component> ); } @@ -49,6 +50,7 @@ RelativeDateCell.propTypes = { className: PropTypes.string.isRequired, date: PropTypes.string, includeSeconds: PropTypes.bool.isRequired, + includeTime: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired, @@ -60,6 +62,7 @@ RelativeDateCell.propTypes = { RelativeDateCell.defaultProps = { className: styles.cell, includeSeconds: false, + includeTime: false, component: TableRowCell }; diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js index d49f2c963..93cdb7c26 100644 --- a/frontend/src/Episode/History/EpisodeHistoryRow.js +++ b/frontend/src/Episode/History/EpisodeHistoryRow.js @@ -111,6 +111,8 @@ class EpisodeHistoryRow extends Component { <RelativeDateCellConnector date={date} + includeSeconds={true} + includeTime={true} /> <TableRowCell className={styles.actions}> diff --git a/frontend/src/Series/History/SeriesHistoryModal.js b/frontend/src/Series/History/SeriesHistoryModal.js index 5d8c2c6d7..0cd7ef9d0 100644 --- a/frontend/src/Series/History/SeriesHistoryModal.js +++ b/frontend/src/Series/History/SeriesHistoryModal.js @@ -14,7 +14,7 @@ function SeriesHistoryModal(props) { return ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} onModalClose={onModalClose} > <SeriesHistoryModalContentConnector diff --git a/frontend/src/Series/History/SeriesHistoryRow.js b/frontend/src/Series/History/SeriesHistoryRow.js index 0213d9679..743c8c869 100644 --- a/frontend/src/Series/History/SeriesHistoryRow.js +++ b/frontend/src/Series/History/SeriesHistoryRow.js @@ -135,6 +135,8 @@ class SeriesHistoryRow extends Component { <RelativeDateCellConnector date={date} + includeSeconds={true} + includeTime={true} /> <TableRowCell className={styles.actions}> diff --git a/frontend/src/Utilities/Date/getRelativeDate.js b/frontend/src/Utilities/Date/getRelativeDate.js index 812064272..a606f8aed 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.js +++ b/frontend/src/Utilities/Date/getRelativeDate.js @@ -5,16 +5,18 @@ import isToday from 'Utilities/Date/isToday'; import isTomorrow from 'Utilities/Date/isTomorrow'; import isYesterday from 'Utilities/Date/isYesterday'; import translate from 'Utilities/String/translate'; +import formatDateTime from './formatDateTime'; -function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { +function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false, includeTime = false } = {}) { if (!date) { return null; } const isTodayDate = isToday(date); + const time = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); if (isTodayDate && timeForToday && timeFormat) { - return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); + return time; } if (!showRelativeDates) { @@ -22,22 +24,26 @@ function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, } if (isYesterday(date)) { - return translate('Yesterday'); + return includeTime ? translate('YesterdayAt', { time } ): translate('Yesterday'); } if (isTodayDate) { - return translate('Today'); + return includeTime ? translate('TodayAt', { time } ): translate('Today'); } if (isTomorrow(date)) { - return translate('Tomorrow'); + return includeTime ? translate('TomorrowAt', { time } ): translate('Tomorrow'); } if (isInNextWeek(date)) { - return moment(date).format('dddd'); + const day = moment(date).format('dddd'); + + return includeTime ? translate('DayOfWeekAt', { day, time }) : day; } - return moment(date).format(shortDateFormat); + return includeTime ? + formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds }) : + moment(date).format(shortDateFormat); } export default getRelativeDate; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 46546a8ca..73a556b40 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -308,6 +308,7 @@ "Date": "Date", "Dates": "Dates", "Day": "Day", + "DayOfWeekAt": "{day} at {time}", "Debug": "Debug", "Default": "Default", "DefaultCase": "Default Case", @@ -1949,10 +1950,12 @@ "Title": "Title", "Titles": "Titles", "Today": "Today", + "TodayAt": "Today at {time}", "ToggleMonitoredSeriesUnmonitored ": "Cannot toggle monitored state when series is unmonitored", "ToggleMonitoredToUnmonitored": "Monitored, click to unmonitor", "ToggleUnmonitoredToMonitored": "Unmonitored, click to monitor", "Tomorrow": "Tomorrow", + "TomorrowAt": "Tomorrow at {time}", "TorrentBlackhole": "Torrent Blackhole", "TorrentBlackholeSaveMagnetFiles": "Save Magnet Files", "TorrentBlackholeSaveMagnetFilesExtension": "Save Magnet Files Extension", @@ -2074,5 +2077,6 @@ "Year": "Year", "Yes": "Yes", "YesCancel": "Yes, Cancel", - "Yesterday": "Yesterday" + "Yesterday": "Yesterday", + "YesterdayAt": "Yesterday at {time}" } From aea50fa47e40e3a380ddd8caa5596c403816f7cb Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 13 May 2024 15:45:51 +0300 Subject: [PATCH 300/762] Bump Npgsql to 7.0.7 ignore-downstream --- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index c124b94dd..e66e1483c 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -25,7 +25,7 @@ <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.9" /> - <PackageReference Include="Npgsql" Version="7.0.4" /> + <PackageReference Include="Npgsql" Version="7.0.7" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> From 9b4ff657af41e67aeb5866ee3056f1a8f2a901ea Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 14 May 2024 00:24:37 +0300 Subject: [PATCH 301/762] Update the wanted section for missing and cutoff unmet --- frontend/src/Components/SignalRConnector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 3d290032a..a928105d8 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -244,7 +244,7 @@ class SignalRConnector extends Component { handleWantedCutoff = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'cutoffUnmet', + section: 'wanted.cutoffUnmet', updateOnly: true, ...body.resource }); @@ -254,7 +254,7 @@ class SignalRConnector extends Component { handleWantedMissing = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'missing', + section: 'wanted.missing', updateOnly: true, ...body.resource }); From cc5b5463f225604a29820e026a43d7dc33f0ffa5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 14 May 2024 00:48:01 +0300 Subject: [PATCH 302/762] Ignore `Grabbed` with STJson --- src/Sonarr.Api.V3/Episodes/EpisodeResource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs index d89020b74..551d9bb69 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; using Sonarr.Api.V3.EpisodeFiles; @@ -39,7 +39,7 @@ namespace Sonarr.Api.V3.Episodes public List<MediaCover> Images { get; set; } // Hiding this so people don't think its usable (only used to set the initial state) - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool Grabbed { get; set; } } From d7ceb11a64c3926f35aabf67c935680cf031bd0e Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 18 May 2024 19:05:24 +0300 Subject: [PATCH 303/762] Fixed: Trimming slashes from UrlBase when using environment variable --- src/NzbDrone.Core/Configuration/ConfigFileProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 4dc2e1fc2..80b4e30fe 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -242,7 +242,7 @@ namespace NzbDrone.Core.Configuration { get { - var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/'); + var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/'); if (urlBase.IsNullOrWhiteSpace()) { From a2e0002a08cd3c61df5b43aa1a6fafac717c22cf Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 18 May 2024 19:07:04 +0300 Subject: [PATCH 304/762] Replace multiple occurrences in branch env variable ignore-downstream --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd0f3ffe4..760db1bb7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV" echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV" - echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV" + echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV" echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT" echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT" From 70bc26dc19ca240da24e9636eaa97cdabbc36ff8 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 18 May 2024 19:08:34 +0300 Subject: [PATCH 305/762] Disable workflows on forks ignore-downstream --- .github/workflows/labeler.yml | 1 + .github/workflows/lock.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index ab2292824..df54c0fff 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -8,5 +8,6 @@ jobs: contents: read pull-requests: write runs-on: ubuntu-latest + if: github.repository == 'Sonarr/Sonarr' steps: - uses: actions/labeler@v5 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 03ec90954..d775234db 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -8,6 +8,7 @@ on: jobs: lock: runs-on: ubuntu-latest + if: github.repository == 'Sonarr/Sonarr' steps: - uses: dessant/lock-threads@v5 with: From 0904a0737e1a2f6af7d755a51c1026ea18474e50 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Wed, 22 May 2024 00:08:57 +0000 Subject: [PATCH 306/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 83c0e8740..ff1d2403b 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10132,6 +10132,9 @@ "onHealthIssue": { "type": "boolean" }, + "includeHealthWarnings": { + "type": "boolean" + }, "onHealthRestored": { "type": "boolean" }, @@ -10177,9 +10180,6 @@ "supportsOnManualInteractionRequired": { "type": "boolean" }, - "includeHealthWarnings": { - "type": "boolean" - }, "testCommand": { "type": "string", "nullable": true From ca372bee258523339aff2b868f8f9a619d44dbca Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 22 May 2024 06:54:53 -0700 Subject: [PATCH 307/762] Fixed: Queue and Calendar not loading --- frontend/src/Activity/Queue/TimeleftCell.js | 12 ++- frontend/src/Calendar/Day/DayOfWeek.js | 2 +- .../Table/Cells/RelativeDateCell.js | 2 +- .../Overview/SeriesIndexOverviewInfo.tsx | 22 +++-- .../Index/Posters/SeriesIndexPoster.tsx | 5 +- .../Index/Posters/SeriesIndexPosterInfo.tsx | 17 ++-- .../src/Utilities/Date/getRelativeDate.js | 49 ----------- .../src/Utilities/Date/getRelativeDate.tsx | 83 +++++++++++++++++++ 8 files changed, 121 insertions(+), 71 deletions(-) delete mode 100644 frontend/src/Utilities/Date/getRelativeDate.js create mode 100644 frontend/src/Utilities/Date/getRelativeDate.tsx diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.js index b280b5a06..0a39b7edc 100644 --- a/frontend/src/Activity/Queue/TimeleftCell.js +++ b/frontend/src/Activity/Queue/TimeleftCell.js @@ -24,7 +24,11 @@ function TimeleftCell(props) { } = props; if (status === 'delay') { - const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const date = getRelativeDate({ + date: estimatedCompletionTime, + shortDateFormat, + showRelativeDates + }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( @@ -40,7 +44,11 @@ function TimeleftCell(props) { } if (status === 'downloadClientUnavailable') { - const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const date = getRelativeDate({ + date: estimatedCompletionTime, + shortDateFormat, + showRelativeDates + }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( diff --git a/frontend/src/Calendar/Day/DayOfWeek.js b/frontend/src/Calendar/Day/DayOfWeek.js index 39e40fce8..0f1d38f0b 100644 --- a/frontend/src/Calendar/Day/DayOfWeek.js +++ b/frontend/src/Calendar/Day/DayOfWeek.js @@ -28,7 +28,7 @@ class DayOfWeek extends Component { if (view === calendarViews.WEEK) { formatedDate = momentDate.format(calendarWeekColumnHeader); } else if (view === calendarViews.FORECAST) { - formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates); + formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates }); } return ( diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js index ed95e3014..37d23e8f9 100644 --- a/frontend/src/Components/Table/Cells/RelativeDateCell.js +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js @@ -40,7 +40,7 @@ class RelativeDateCell extends PureComponent { title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })} {...otherProps} > - {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, includeTime, timeForToday: true })} + {getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })} </Component> ); } diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 4c3c85555..5bd4dd7c2 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -138,7 +138,10 @@ function getInfoRowProps( }), iconName: icons.CALENDAR, label: - getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, { + getRelativeDate({ + date: previousAiring, + shortDateFormat, + showRelativeDates, timeFormat, timeForToday: true, }) ?? '', @@ -156,7 +159,10 @@ function getInfoRowProps( }), iconName: icons.ADD, label: - getRelativeDate(added, shortDateFormat, showRelativeDates, { + getRelativeDate({ + date: added, + shortDateFormat, + showRelativeDates, timeFormat, timeForToday: true, }) ?? '', @@ -232,15 +238,13 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) { <SeriesIndexOverviewInfoRow title={formatDateTime(nextAiring, longDateFormat, timeFormat)} iconName={icons.SCHEDULED} - label={getRelativeDate( - nextAiring, + label={getRelativeDate({ + date: nextAiring, shortDateFormat, showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} + timeFormat, + timeForToday: true, + })} /> )} diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index 0a0a385eb..b2015eaf5 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -217,7 +217,10 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { timeFormat )}`} > - {getRelativeDate(nextAiring, shortDateFormat, showRelativeDates, { + {getRelativeDate({ + date: nextAiring, + shortDateFormat, + showRelativeDates, timeFormat, timeForToday: true, })} diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx index f1605cd05..9a4265324 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx @@ -80,7 +80,10 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { timeFormat )}`} > - {getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, { + {getRelativeDate({ + date: previousAiring, + shortDateFormat, + showRelativeDates, timeFormat, timeForToday: true, })} @@ -89,15 +92,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { } if (sortKey === 'added' && added) { - const addedDate = getRelativeDate( - added, + const addedDate = getRelativeDate({ + date: added, shortDateFormat, showRelativeDates, - { - timeFormat, - timeForToday: false, - } - ); + timeFormat, + timeForToday: false, + }); return ( <div diff --git a/frontend/src/Utilities/Date/getRelativeDate.js b/frontend/src/Utilities/Date/getRelativeDate.js deleted file mode 100644 index a606f8aed..000000000 --- a/frontend/src/Utilities/Date/getRelativeDate.js +++ /dev/null @@ -1,49 +0,0 @@ -import moment from 'moment'; -import formatTime from 'Utilities/Date/formatTime'; -import isInNextWeek from 'Utilities/Date/isInNextWeek'; -import isToday from 'Utilities/Date/isToday'; -import isTomorrow from 'Utilities/Date/isTomorrow'; -import isYesterday from 'Utilities/Date/isYesterday'; -import translate from 'Utilities/String/translate'; -import formatDateTime from './formatDateTime'; - -function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false, includeTime = false } = {}) { - if (!date) { - return null; - } - - const isTodayDate = isToday(date); - const time = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); - - if (isTodayDate && timeForToday && timeFormat) { - return time; - } - - if (!showRelativeDates) { - return moment(date).format(shortDateFormat); - } - - if (isYesterday(date)) { - return includeTime ? translate('YesterdayAt', { time } ): translate('Yesterday'); - } - - if (isTodayDate) { - return includeTime ? translate('TodayAt', { time } ): translate('Today'); - } - - if (isTomorrow(date)) { - return includeTime ? translate('TomorrowAt', { time } ): translate('Tomorrow'); - } - - if (isInNextWeek(date)) { - const day = moment(date).format('dddd'); - - return includeTime ? translate('DayOfWeekAt', { day, time }) : day; - } - - return includeTime ? - formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds }) : - moment(date).format(shortDateFormat); -} - -export default getRelativeDate; diff --git a/frontend/src/Utilities/Date/getRelativeDate.tsx b/frontend/src/Utilities/Date/getRelativeDate.tsx new file mode 100644 index 000000000..cc0b76b72 --- /dev/null +++ b/frontend/src/Utilities/Date/getRelativeDate.tsx @@ -0,0 +1,83 @@ +import moment from 'moment'; +import formatTime from 'Utilities/Date/formatTime'; +import isInNextWeek from 'Utilities/Date/isInNextWeek'; +import isToday from 'Utilities/Date/isToday'; +import isTomorrow from 'Utilities/Date/isTomorrow'; +import isYesterday from 'Utilities/Date/isYesterday'; +import translate from 'Utilities/String/translate'; +import formatDateTime from './formatDateTime'; + +interface GetRelativeDateOptions { + date?: string; + shortDateFormat: string; + showRelativeDates: boolean; + timeFormat?: string; + includeSeconds?: boolean; + timeForToday?: boolean; + includeTime?: boolean; +} + +function getRelativeDate({ + date, + shortDateFormat, + showRelativeDates, + timeFormat, + includeSeconds = false, + timeForToday = false, + includeTime = false, +}: GetRelativeDateOptions) { + if (!date) { + return null; + } + + if (includeTime && !timeFormat) { + throw new Error( + "getRelativeDate: 'timeFormat' is required when 'includeTime' is true" + ); + } + + const isTodayDate = isToday(date); + const time = + includeTime && timeFormat + ? formatTime(date, timeFormat, { + includeMinuteZero: true, + includeSeconds, + }) + : ''; + + if (isTodayDate && timeForToday && timeFormat) { + return time; + } + + if (!showRelativeDates) { + return moment(date).format(shortDateFormat); + } + + if (isYesterday(date)) { + return includeTime + ? translate('YesterdayAt', { time }) + : translate('Yesterday'); + } + + if (isTodayDate) { + return includeTime ? translate('TodayAt', { time }) : translate('Today'); + } + + if (isTomorrow(date)) { + return includeTime + ? translate('TomorrowAt', { time }) + : translate('Tomorrow'); + } + + if (isInNextWeek(date)) { + const day = moment(date).format('dddd'); + + return includeTime ? translate('DayOfWeekAt', { day, time }) : day; + } + + return includeTime + ? formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds }) + : moment(date).format(shortDateFormat); +} + +export default getRelativeDate; From 62a9c2519bd5950f8ee43e9b1e5b40d6556ac112 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 22 May 2024 00:06:24 +0000 Subject: [PATCH 308/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: mm519897405 <baiya@vip.qq.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/zh_CN.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index ab24051f9..e33dcd88c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -161,7 +161,7 @@ "CountIndexersSelected": "已选择 {count} 个索引器", "CurrentlyInstalled": "已安装", "CustomFormats": "自定义命名格式", - "CutoffUnmet": "未达截止条件", + "CutoffUnmet": "未达设定标准", "Date": "日期", "DeleteBackup": "删除备份", "DeleteCustomFormat": "删除自定义命名格式", @@ -293,7 +293,7 @@ "RootFolderMultipleMissingHealthCheckMessage": "多个根目录缺失:{rootFolderPaths}", "SkipRedownloadHelpText": "阻止{appName}尝试下载此项目的替代版本", "Tasks": "任务", - "Wanted": "已追踪", + "Wanted": "待获取", "Yes": "确定", "AbsoluteEpisodeNumbers": "准确的集数", "RemoveCompleted": "移除已完成", @@ -688,7 +688,7 @@ "ICalShowAsAllDayEventsHelpText": "事件将以全天事件的形式显示在日历中", "ICalSeasonPremieresOnlyHelpText": "每季中只有第一集会出现在订阅中", "IconForFinalesHelpText": "根据可用的集信息为完结的剧集或季显示图标", - "IconForCutoffUnmet": "未达截止条件的图标", + "IconForCutoffUnmet": "未达设定标准的图标", "IconForCutoffUnmetHelpText": "终止监控条件未满足前为文件显示图标", "IconForFinales": "剧集或季完结的图标", "Images": "图像", @@ -1459,8 +1459,8 @@ "TotalFileSize": "文件总大小", "TotalRecords": "记录总数: {totalRecords}", "Trace": "追踪", - "CutoffUnmetLoadError": "加载未达截止条件项目错误", - "CutoffUnmetNoItems": "没有未达截止条件的项目", + "CutoffUnmetLoadError": "加载未达设定标准项目时出错", + "CutoffUnmetNoItems": "没有未达设定标准的项目", "DeleteSeriesFolderHelpText": "删除剧集文件夹及其所含文件", "DeleteSeriesModalHeader": "删除 - {title}", "DeletedSeriesDescription": "剧集已从 TheTVDB 移除", @@ -1830,5 +1830,11 @@ "AutoTaggingSpecificationStatus": "状态", "ClickToChangeIndexerFlags": "点击修改索引器标志", "ConnectionSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}", - "BlocklistFilterHasNoItems": "所选的黑名单过滤器没有项目" + "BlocklistFilterHasNoItems": "所选的黑名单过滤器没有项目", + "CustomFormatsSpecificationReleaseGroup": "发布组", + "MetadataSettingsSeriesMetadata": "季元数据", + "CustomFormatsSpecificationResolution": "分辨率", + "CustomFormatsSpecificationSource": "来源", + "ClickToChangeReleaseType": "点击更改发布类型", + "CustomFormatsSettingsTriggerInfo": "当一个发布版本或文件至少匹配其中一个条件时,自定义格式将会被应用到这个版本或文件上。" } From 2a662afaef0bed9f8cd153d60eee5ecbe85508ba Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 23 May 2024 06:50:25 -0700 Subject: [PATCH 309/762] Fixed: Time for episodes airing today being blank --- .../src/Utilities/Date/getRelativeDate.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/frontend/src/Utilities/Date/getRelativeDate.tsx b/frontend/src/Utilities/Date/getRelativeDate.tsx index cc0b76b72..3a7f4f143 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.tsx +++ b/frontend/src/Utilities/Date/getRelativeDate.tsx @@ -30,22 +30,21 @@ function getRelativeDate({ return null; } - if (includeTime && !timeFormat) { + if ((includeTime || timeForToday) && !timeFormat) { throw new Error( - "getRelativeDate: 'timeFormat' is required when 'includeTime' is true" + "getRelativeDate: 'timeFormat' is required when 'includeTime' or 'timeForToday' is true" ); } const isTodayDate = isToday(date); - const time = - includeTime && timeFormat - ? formatTime(date, timeFormat, { - includeMinuteZero: true, - includeSeconds, - }) - : ''; + const time = timeFormat + ? formatTime(date, timeFormat, { + includeMinuteZero: true, + includeSeconds, + }) + : ''; - if (isTodayDate && timeForToday && timeFormat) { + if (isTodayDate && timeForToday) { return time; } From 66940b283b4a08a37c66298113a6968c746b0e17 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 23 May 2024 03:07:37 +0000 Subject: [PATCH 310/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: mm519897405 <baiya@vip.qq.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 6 +++++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 08b3c49a6..2fe54a8ee 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2074,5 +2074,9 @@ "IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está reportando archivos faltantes", "BlocklistFilterHasNoItems": "El filtro de lista de bloqueo seleccionado no contiene elementos", - "HasUnmonitoredSeason": "Tiene temporada sin monitorizar" + "HasUnmonitoredSeason": "Tiene temporada sin monitorizar", + "TomorrowAt": "Mañana a las {time}", + "YesterdayAt": "Ayer a las {time}", + "TodayAt": "Hoy a las {time}", + "DayOfWeekAt": "{day} a las {time}" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index a491440c8..adb4750e0 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2073,5 +2073,10 @@ "AutoTaggingSpecificationTag": "Etiqueta", "IndexerSettingsMultiLanguageRelease": "Multi Idiomas", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está relatando arquivos perdidos", - "BlocklistFilterHasNoItems": "O filtro selecionado para a lista de bloqueio não contém itens" + "BlocklistFilterHasNoItems": "O filtro selecionado para a lista de bloqueio não contém itens", + "DayOfWeekAt": "{day} às {time}", + "TodayAt": "Hoje às {time}", + "TomorrowAt": "Amanhã às {time}", + "HasUnmonitoredSeason": "Tem Temporada Não Monitorada", + "YesterdayAt": "Ontem às {time}" } From 39a439eb4c530be5e66c12fbbf7a972796ddc986 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 28 May 2024 03:25:08 +0000 Subject: [PATCH 311/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Bao Trinh <servarr@baodtrinh.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: mm519897405 <baiya@vip.qq.com> Co-authored-by: thegamingcat13 <sandervanbeek2004@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/vi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/nl.json | 4 +++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 12 ++++++------ src/NzbDrone.Core/Localization/Core/vi.json | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 082114cd8..7e7e23623 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -205,5 +205,7 @@ "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" + "ChownGroup": "chown groep", + "AutoTaggingSpecificationTag": "Tag", + "AddDelayProfileError": "Mislukt om vertragingsprofiel toe te voegen, probeer het later nog eens." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index adb4750e0..1d04b124e 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -169,7 +169,7 @@ "Calendar": "Calendário", "Connect": "Conectar", "CustomFormats": "Formatos personalizados", - "CutoffUnmet": "Corte não alcançado", + "CutoffUnmet": "Limite não alcançado", "DownloadClients": "Clientes de download", "Events": "Eventos", "General": "Geral", @@ -493,8 +493,8 @@ "CustomFormatsLoadError": "Não foi possível carregar Formatos Personalizados", "CustomFormatsSettings": "Configurações de Formatos Personalizados", "CustomFormatsSettingsSummary": "Configurações e Formatos Personalizados", - "DailyEpisodeFormat": "Formato do Episódio Diário", - "Cutoff": "Corte", + "DailyEpisodeFormat": "Formato do episódio diário", + "Cutoff": "Limite", "Dash": "Traço", "Dates": "Datas", "Debug": "Depuração", @@ -1448,11 +1448,11 @@ "MissingNoItems": "Nenhum item ausente", "SearchAll": "Pesquisar Todos", "UnmonitorSelected": "Não Monitorar os Selecionados", - "CutoffUnmetNoItems": "Nenhum item com corte não atingido", + "CutoffUnmetNoItems": "Nenhum item com limite não atingido", "MonitorSelected": "Monitorar Selecionados", "SearchForAllMissingEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", "SearchSelected": "Pesquisar Selecionado", - "CutoffUnmetLoadError": "Erro ao carregar itens de corte não atingidos", + "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atingido", "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar {appName} ou desabilitar todos os seus indexadores.", "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", "SearchForCutoffUnmetEpisodes": "Pesquise todos os episódios que o corte não foi atingido", @@ -2016,7 +2016,7 @@ "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente por {appName} via RSS ou Pesquisa Automática", "ChangeCategoryHint": "Altera o download para a \"Categoria pós-importação\" do cliente de download", "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", - "DatabaseMigration": "Migração de Banco de Dados", + "DatabaseMigration": "Migração de banco de dados", "DoNotBlocklistHint": "Remover sem colocar na lista de bloqueio", "ChangeCategory": "Alterar categoria", "DoNotBlocklist": "Não coloque na lista de bloqueio", diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 493d34b23..4ecb88042 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -5,5 +5,7 @@ "ApiKeyValidationHealthCheckMessage": "Hãy cập nhật mã API để dài ít nhất {length} kí tự. Bạn có thể làm điều này trong cài đặt hoặc trong tập config", "AppDataLocationHealthCheckMessage": "Việc cập nhật sẽ không xảy ra để tránh xóa AppData khi cập nhật", "ApplyChanges": "Áp dụng thay đổi", - "AutomaticAdd": "Tự động thêm" + "AutomaticAdd": "Tự động thêm", + "CalendarOptions": "Tùy chọn lịch", + "UpdateMechanismHelpText": "Sử dụng trình cập nhật tích hợp của {appName} hoặc một tập lệnh" } From af0e55aef4454acc7618e8ef258f04b772f7567e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 29 May 2024 16:03:40 -0700 Subject: [PATCH 312/762] Bump version to 4.0.5 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 760db1bb7..aa85cf5dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.4 + VERSION: 4.0.5 jobs: backend: From 48f02918846290da6761c2c9c026eb36f2a4586e Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 31 May 2024 13:25:09 +0000 Subject: [PATCH 313/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ano10 <arnaudthommeray+github@ik.me> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: r0bertreh <Robert.reh@live.de> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 97 +++++++++++++++++++-- src/NzbDrone.Core/Localization/Core/fr.json | 4 +- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 6bb656d91..d276f1f82 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -128,7 +128,7 @@ "AddConnectionImplementation": "Verbindung hinzufügen - {implementationName}", "AddDownloadClientImplementation": "Download-Client hinzufügen - {implementationName}", "AddIndexerImplementation": "Indexer hinzufügen - {implementationName}", - "AddNotificationError": "Neue Benachrichtigung konnte nicht hinzugefügt werden, bitte versuchen Sie es erneut.", + "AddNotificationError": "Die neue Benachrichtigung konnte nicht hinzugefügt werden, bitte erneut probieren.", "AddQualityProfileError": "Qualitätsprofil konnte nicht hinzugefügt werden. Bitte versuchen Sie es erneut.", "AddNewSeriesRootFolderHelpText": "Unterordner '{folder}' wird automatisch erstellt", "AddNewSeriesSearchForMissingEpisodes": "Suche für fehlende Episoden starten", @@ -144,7 +144,7 @@ "AuthenticationRequiredPasswordHelpTextWarning": "Gib ein neues Passwort ein", "AuthenticationRequiredUsernameHelpTextWarning": "Gib einen neuen Benutzernamen ein", "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.", + "AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec aus Datien erkennen. Dazu ist es erforderlich, dass {appName} Teile der Datei liest, was zu hoher Festplatten- oder Netzwerkaktivität während der Scans 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": "Falls aktiviert wird die Auto Tagging Regel nicht angewendet, solange diese Bedingung {implementationName} 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", @@ -164,7 +164,7 @@ "RestartReloadNote": "Hinweis: {appName} startet während des Wiederherstellungsvorgangs automatisch neu und lädt die Benutzeroberfläche neu.", "AutoRedownloadFailedHelpText": "Suchen Sie automatisch nach einer anderen Version und versuchen Sie, sie herunterzuladen", "AirDate": "Ausstrahlungsdatum", - "AgeWhenGrabbed": "Alter (zum Zeitpunkt der Entführung)", + "AgeWhenGrabbed": "Alter (bei Erfassung)", "ApplyTagsHelpTextHowToApplySeries": "So wenden Sie Tags auf die ausgewählte Serie an", "ApiKey": "API-Schlüssel", "AutoTaggingLoadError": "Automatisches Tagging konnte nicht geladen werden", @@ -272,7 +272,7 @@ "ContinuingOnly": "Nur fortlaufend", "ContinuingSeriesDescription": "Weitere Episoden/eine weitere Staffel werden erwartet", "CopyToClipboard": "In die Zwischenablage kopieren", - "CouldNotFindResults": "Es konnten keine Ergebnisse für „{term}“ gefunden werden.", + "CouldNotFindResults": "Es konnten keine Ergebnisse für „{term}“ gefunden werden", "CountSeriesSelected": "{count} Serie ausgewählt", "CreateEmptySeriesFoldersHelpText": "Erstellen Sie beim Festplatten-Scan Ordner für fehlende Serien", "CreateGroup": "Gruppe erstellen", @@ -416,7 +416,7 @@ "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deaktivieren Sie die Datumssortierung", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sie müssen die Datumssortierung für die von {appName} verwendete Kategorie deaktivieren, um Importprobleme zu vermeiden. Gehen Sie zu Sabnzbd, um das Problem zu beheben.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deaktivieren Sie die Filmsortierung", - "AllResultsAreHiddenByTheAppliedFilter": "Alle Resultate werden wegen des angewandten Filters nicht angezeigt", + "AllResultsAreHiddenByTheAppliedFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet", "RegularExpressionsCanBeTested": "Reguläre Ausdrücke können [hier] getestet werden ({url}).", "ReleaseSceneIndicatorUnknownSeries": "Unbekannte Folge oder Serie.", "RemoveFilter": "Filter entfernen", @@ -549,7 +549,7 @@ "CountImportListsSelected": "{count} Importliste(n) ausgewählt", "CountIndexersSelected": "{count} Indexer ausgewählt", "CountSelectedFiles": "{selectedCount} ausgewählte Dateien", - "CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“", + "CustomFormatUnknownConditionOption": "Unbekannte Option '{key}' für die Bedingung '{implementation}'", "CustomFormatsSettings": "Einstellungen für eigene Formate", "Daily": "Täglich", "Dash": "Bindestrich", @@ -580,7 +580,7 @@ "SslCertPassword": "SSL-Zertifikatskennwort", "SpecialsFolderFormat": "Specials-Ordnerformat", "SourceTitle": "Quellentitel", - "Agenda": "Tagesordnung", + "Agenda": "Agenda", "AnEpisodeIsDownloading": "Eine Episode wird heruntergeladen", "CollapseMultipleEpisodesHelpText": "Reduzieren Sie mehrere Episoden, die am selben Tag ausgestrahlt werden", "Connect": "Verbinden", @@ -792,5 +792,86 @@ "MediaManagement": "Medienverwaltung", "StartupDirectory": "Start-Verzeichnis", "OnRename": "Bei Umbenennung", - "MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details" + "MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details", + "BlocklistRelease": "Release sperren", + "BranchUpdateMechanism": "Git-Branch für den externen Updateablauf", + "AutoTaggingSpecificationGenre": "Genre(s)", + "AutoTaggingSpecificationOriginalLanguage": "Sprache", + "AutoTaggingSpecificationQualityProfile": "Qualitätsprofil", + "AutoTaggingSpecificationRootFolder": "Stammverzeichnis", + "AutoTaggingSpecificationStatus": "Status", + "ConnectionSettingsUrlBaseHelpText": "Fügt ein Präfix zur {connectionName} URL hinzu, z. B. {url}", + "DeleteImportListExclusion": "Importlisten Ausschluss löschen", + "DeleteTag": "Tag löschen", + "DoNotBlocklistHint": "Entfernen ohne Sperren", + "DownloadClientPriorityHelpText": "Download-Client-Priorität von 1 (Höchste) bis 50 (Niedrigste). Standard: 1. Round-Robin wird für Clients mit der gleichen Priorität verwendet.", + "DownloadClientSettingsRecentPriority": "Neueste Priorität", + "DownloadClientValidationApiKeyIncorrect": "API-Key fehlerhaft", + "ClientPriority": "Priorität", + "Cutoff": "Schwelle", + "DownloadClient": "Downloader", + "DownloadClientSabnzbdValidationUnknownVersion": "Unbekannte Version: {rawVersion}", + "CutoffUnmet": "Schwelle nicht erreicht", + "DownloadClientSettingsInitialState": "Ausgangszustand", + "DownloadClientValidationApiKeyRequired": "API-Key benötigt", + "CustomFormatsSpecificationRegularExpressionHelpText": "Benutzerdefiniertes Format RegEx ist nicht groß-/kleinschreibungssensitiv", + "CustomFormatsSpecificationMinimumSize": "Mindestgröße", + "CustomFormatsSpecificationRegularExpression": "Regulären Ausdruck", + "CustomFormatsSpecificationReleaseGroup": "Release-Gruppe", + "CustomFormatsSpecificationResolution": "Auflösung", + "DeleteImportListExclusionMessageText": "Bist du sicher, dass du diesen Importlisten Ausschluss löschen willst?", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "TV-Sortierung deaktivieren", + "CustomFormatUnknownCondition": "Unbekannte Eigene Formatbedingung '{implementation}'", + "ReleaseGroups": "Release Gruppen", + "DownloadClientSettingsUseSslHelpText": "Sichere Verbindung verwenden, wenn Verbindung zu {clientName} hergestellt wird", + "ReleaseRejected": "Release abgelehnt", + "Clear": "Leeren", + "DownloadClientValidationCategoryMissing": "Kategorie existiert nicht", + "DownloadClientValidationAuthenticationFailure": "Authentifizierung fehlgeschlagen", + "DownloadClientValidationErrorVersion": "{clientName} Version sollte mindestens {requiredVersion} sein. Die gemeldete Version ist {reportedVersion}", + "DownloadClientValidationGroupMissing": "Gruppe existiert nicht", + "DownloadClientValidationSslConnectFailure": "Verbindung über SSL nicht möglich", + "ReleaseProfilesLoadError": "Release-Profile können nicht geladen werden", + "DownloadClientDelugeSettingsDirectory": "Download Verzeichnis", + "DownloadClientDelugeSettingsDirectoryCompleted": "Verschieben, wenn Verzeichnis abgeschlossen", + "DownloadClientSettings": "Downloader Einstellungen", + "IgnoreDownloadHint": "Hält {appName} von der weiteren Verarbeitung dieses Downloads ab", + "ClearBlocklist": "Sperrliste leeren", + "CleanLibraryLevel": "Mediathek aufräumen", + "CloneAutoTag": "Automatische Tags kopieren", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Priorität beim Abrufen von Episoden, die vor mehr als 14 Tagen ausgestrahlt wurden", + "DownloadClientSettingsInitialStateHelpText": "Anfangszustand für zu {clientName} hinzugefügte Torrents", + "DownloadClientSettingsOlderPriority": "Ältere Priorität", + "IgnoreDownload": "Download ignorieren", + "CustomFormatsSettingsTriggerInfo": "Ein Eigenes Format wird auf eine Veröffentlichung oder Datei angewandt, wenn sie mindestens einer der verschiedenen ausgewählten Bedingungen entspricht.", + "DatabaseMigration": "DB Migration", + "DownloadClientSettingsDestinationHelpText": "Legt das Ziel für den Download manuell fest; lassen Sie es leer, um die Standardeinstellung zu verwenden", + "DownloadClientSettingsCategorySubFolderHelpText": "Das Hinzufügen einer spezifischen Kategorie für {appName} vermeidet Konflikte mit nicht verwandten Downloads, die nicht {appName} sind. Die Verwendung einer Kategorie ist optional, wird aber dringend empfohlen. Erzeugt ein Unterverzeichnis [category] im Ausgabeverzeichnis.", + "BlocklistReleases": "Release sperren", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent meldet fehlende Dateien", + "ChangeCategoryHint": "Änderung des Downloads in die 'Post-Import-Kategorie' vom Download-Client", + "ChangeCategoryMultipleHint": "Änderung der Downloads in die 'Post-Import-Kategorie' vom Download-Client", + "CustomFormatsSpecificationSource": "Quelle", + "BlocklistFilterHasNoItems": "Ausgewählter Blocklistenfilter enthält keine Elemente", + "CustomFilter": "Benutzerdefinierter Filter", + "CustomFormatsSpecificationFlag": "Markierung", + "CustomFormatScore": "Eigenes Format Bewertungspunkte", + "CustomFormatsSpecificationLanguage": "Sprache", + "CustomFormatsSpecificationMaximumSize": "Maximale Größe", + "AutoTaggingSpecificationTag": "Tag", + "BindAddress": "Adresse binden", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Priorität beim Abrufen von Episoden, die innerhalb der letzten 14 Tage ausgestrahlt wurden", + "DownloadClientSettingsUrlBaseHelpText": "Fügt ein Präfix zur {clientName} Url hinzu, z.B. {url}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads; leer lassen, um den Standardspeicherort für Übertragungen zu verwenden", + "DownloadClientUTorrentTorrentStateError": "uTorrent meldet einen Fehler", + "DoNotBlocklist": "Nicht Sperren", + "ReleaseHash": "Release Hash", + "DownloadClientAriaSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden", + "DownloadClientSeriesTagHelpText": "Verwenden Sie diesen Downloader nur für Serien mit mindestens einem passenden Tag. Lassen Sie ihn leer, um ihn für alle Serien zu verwenden.", + "DownloadClientSettingsCategoryHelpText": "Das Hinzufügen einer spezifischen Kategorie für {appName} vermeidet Konflikte mit nicht verwandten Downloads, die nicht {appName} sind. Die Verwendung einer Kategorie ist optional, wird aber dringend empfohlen.", + "DownloadClientSettingsPostImportCategoryHelpText": "Kategorie für {appName}, die nach dem Importieren des Downloads festgelegt wird. {appName} wird Torrents in dieser Kategorie nicht entfernen, auch wenn das Seeding beendet ist. Leer lassen, um dieselbe Kategorie beizubehalten.", + "DownloadClientValidationCategoryMissingDetail": "Die von Ihnen eingegebene Kategorie existiert nicht in {clientName}. Erstellen Sie sie zuerst in {clientName}.", + "DownloadClientValidationGroupMissingDetail": "Die von Ihnen eingegebene Gruppe existiert nicht in {clientName}. Erstellen Sie sie zuerst in {clientName}.", + "IgnoreDownloads": "Downloads ignorieren", + "IgnoreDownloadsHint": "Hindert {appName}, diese Downloads weiter zu verarbeiten" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 409e01ae3..c5ff71c9e 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -616,7 +616,7 @@ "OnSeriesDelete": "Lors de la suppression de la série", "OnlyTorrent": "Uniquement Torrent", "OpenBrowserOnStart": "Ouvrir le navigateur au démarrage", - "OpenSeries": "Série ouverte", + "OpenSeries": "Ouvrir la série", "Options": "Options", "Organize": "Organiser", "OrganizeLoadError": "Erreur lors du chargement des aperçus", @@ -1344,7 +1344,7 @@ "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 '{path}'?", + "DeleteEpisodeFileMessage": "Êtes-vous sûr de vouloir supprimer « {path} » ?", "DeleteEpisodeFromDisk": "Supprimer l'épisode du disque", "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer la liste « {name} » ?", "DeleteSelectedEpisodeFiles": "Supprimer les fichiers d'épisode sélectionnés", From 11e5c5a11b171138c235224c1aa9a258f0a4ec4d Mon Sep 17 00:00:00 2001 From: yammes08 <111231042+yammes08@users.noreply.github.com> Date: Sat, 1 Jun 2024 01:09:53 +0100 Subject: [PATCH 314/762] Fixed: SDR Files Being Parsed As HLG --- .../MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs | 2 +- src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs | 4 ++-- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index c48f3a63d..49a3b06d9 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo [TestCase(10, "", "", "", null, HdrFormat.None)] [TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)] [TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)] - [TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.Hlg10)] + [TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.None)] [TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)] [TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)] [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)] diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 58674b44d..4aa850af3 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -22,10 +22,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo private readonly List<FFProbePixelFormat> _pixelFormats; public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8; - public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 10; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 11; private static readonly string[] ValidHdrColourPrimaries = { "bt2020" }; - private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" }; + private static readonly string[] HlgTransferFunctions = { "arib-std-b67" }; private static readonly string[] PqTransferFunctions = { "smpte2084" }; private static readonly string[] ValidHdrTransferFunctions = HlgTransferFunctions.Concat(PqTransferFunctions).ToArray(); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 29cab2edb..a04dfa771 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -642,7 +642,7 @@ namespace NzbDrone.Core.Organizer new Dictionary<string, int>(FileNameBuilderTokenEqualityComparer.Instance) { { MediaInfoVideoDynamicRangeToken, 5 }, - { MediaInfoVideoDynamicRangeTypeToken, 10 } + { MediaInfoVideoDynamicRangeTypeToken, 11 } }; private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile) From fd3dd1ab7dc86cd9e231fa432cc8d2772d5a4bad Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 23 May 2024 06:42:16 -0700 Subject: [PATCH 315/762] New: Genres and Images for Webhooks and Notifiarr Closes #6822 --- .../Notifications/Notifiarr/Notifiarr.cs | 5 ++-- .../Notifications/Webhook/Webhook.cs | 5 ++-- .../Notifications/Webhook/WebhookBase.cs | 26 +++++++++++++------ .../Notifications/Webhook/WebhookImage.cs | 18 +++++++++++++ .../Notifications/Webhook/WebhookSeries.cs | 5 ++++ 5 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookImage.cs diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs index 96251eb32..498a4724e 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs @@ -3,6 +3,7 @@ using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications.Webhook; using NzbDrone.Core.Tags; @@ -15,8 +16,8 @@ namespace NzbDrone.Core.Notifications.Notifiarr { private readonly INotifiarrProxy _proxy; - public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) - : base(configFileProvider, configService, localizationService, tagRepository) + public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository, IMapCoversToLocal mediaCoverService) + : base(configFileProvider, configService, localizationService, tagRepository, mediaCoverService) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs index 7d0e84478..1e09e8e13 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -3,6 +3,7 @@ using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tags; using NzbDrone.Core.Tv; @@ -14,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Webhook { private readonly IWebhookProxy _proxy; - public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) - : base(configFileProvider, configService, localizationService, tagRepository) + public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository, IMapCoversToLocal mediaCoverService) + : base(configFileProvider, configService, localizationService, tagRepository, mediaCoverService) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index c02fb5828..d87e92a42 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -4,6 +4,7 @@ using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tags; using NzbDrone.Core.Tv; @@ -17,13 +18,15 @@ namespace NzbDrone.Core.Notifications.Webhook private readonly IConfigService _configService; protected readonly ILocalizationService _localizationService; private readonly ITagRepository _tagRepository; + private readonly IMapCoversToLocal _mediaCoverService; - protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository) + protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository, IMapCoversToLocal mediaCoverService) { _configFileProvider = configFileProvider; _configService = configService; _localizationService = localizationService; _tagRepository = tagRepository; + _mediaCoverService = mediaCoverService; } protected WebhookGrabPayload BuildOnGrabPayload(GrabMessage message) @@ -36,7 +39,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Grab, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), + Series = GetSeries(message.Series), Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)), Release = new WebhookRelease(quality, remoteEpisode), DownloadClient = message.DownloadClientName, @@ -55,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Download, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), + Series = GetSeries(message.Series), Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)), EpisodeFile = new WebhookEpisodeFile(episodeFile), Release = new WebhookGrabbedRelease(message.Release), @@ -85,7 +88,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.EpisodeFileDelete, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)), + Series = GetSeries(deleteMessage.Series), Episodes = deleteMessage.EpisodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)), EpisodeFile = new WebhookEpisodeFile(deleteMessage.EpisodeFile), DeleteReason = deleteMessage.Reason @@ -99,7 +102,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.SeriesAdd, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(addMessage.Series, GetTagLabels(addMessage.Series)), + Series = GetSeries(addMessage.Series), }; } @@ -110,7 +113,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.SeriesDelete, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)), + Series = GetSeries(deleteMessage.Series), DeletedFiles = deleteMessage.DeletedFiles }; } @@ -122,7 +125,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.Rename, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(series, GetTagLabels(series)), + Series = GetSeries(series), RenamedEpisodeFiles = renamedFiles.ConvertAll(x => new WebhookRenamedEpisodeFile(x)) }; } @@ -175,7 +178,7 @@ namespace NzbDrone.Core.Notifications.Webhook EventType = WebhookEventType.ManualInteractionRequired, InstanceName = _configFileProvider.InstanceName, ApplicationUrl = _configService.ApplicationUrl, - Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)), + Series = GetSeries(message.Series), Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)), DownloadInfo = new WebhookDownloadClientItem(quality, message.TrackedDownload.DownloadItem), DownloadClient = message.DownloadClientInfo?.Name, @@ -216,6 +219,13 @@ namespace NzbDrone.Core.Notifications.Webhook }; } + private WebhookSeries GetSeries(Series series) + { + _mediaCoverService.ConvertToLocalUrls(series.Id, series.Images); + + return new WebhookSeries(series, GetTagLabels(series)); + } + private List<string> GetTagLabels(Series series) { return _tagRepository.GetTags(series.Tags) diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookImage.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookImage.cs new file mode 100644 index 000000000..87f511dc1 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookImage.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.MediaCover; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookImage + { + public MediaCoverTypes CoverType { get; set; } + public string Url { get; set; } + public string RemoteUrl { get; set; } + + public WebhookImage(MediaCover.MediaCover image) + { + CoverType = image.CoverType; + RemoteUrl = image.RemoteUrl; + Url = image.Url; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs index 57200fb40..1177f2065 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Webhook @@ -14,6 +15,8 @@ namespace NzbDrone.Core.Notifications.Webhook public string ImdbId { get; set; } public SeriesTypes Type { get; set; } public int Year { get; set; } + public List<string> Genres { get; set; } + public List<WebhookImage> Images { get; set; } public List<string> Tags { get; set; } public WebhookSeries() @@ -31,6 +34,8 @@ namespace NzbDrone.Core.Notifications.Webhook ImdbId = series.ImdbId; Type = series.SeriesType; Year = series.Year; + Genres = series.Genres; + Images = series.Images.Select(i => new WebhookImage(i)).ToList(); Tags = tags; } } From 9c1f48ebc953b06c650a12fc97e5b9b00c17f2cf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 27 May 2024 17:27:57 -0700 Subject: [PATCH 316/762] Fixed: Include full series title in episode search --- .../ReleaseSearchServiceFixture.cs | 42 +++++++++++++++++++ .../IndexerSearch/ReleaseSearchService.cs | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index 522d92f9d..0da5fb02a 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -608,5 +608,47 @@ namespace NzbDrone.Core.Test.IndexerSearchTests allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2); allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3); } + + [Test] + public async Task episode_search_should_include_series_title_when_not_a_direct_title_match() + { + _xemSeries.Title = "Sonarr's Title"; + _xemSeries.CleanTitle = "sonarrstitle"; + + WithEpisode(1, 12, 2, 3); + + Mocker.GetMock<ISceneMappingService>() + .Setup(s => s.FindByTvdbId(It.IsAny<int>())) + .Returns(new List<SceneMapping> + { + new SceneMapping + { + TvdbId = _xemSeries.TvdbId, + SearchTerm = "Sonarrs Title", + ParseTerm = _xemSeries.CleanTitle, + SeasonNumber = 1, + SceneSeasonNumber = 1, + SceneOrigin = "tvdb", + Type = "ServicesProvider" + } + }); + + var allCriteria = WatchForSearchCriteria(); + + await Subject.EpisodeSearch(_xemEpisodes.First(), false, false); + + Mocker.GetMock<ISceneMappingService>() + .Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once()); + + allCriteria.Should().HaveCount(2); + + allCriteria.First().Should().BeOfType<SingleEpisodeSearchCriteria>(); + allCriteria.First().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(1); + allCriteria.First().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(12); + + allCriteria.Last().Should().BeOfType<SingleEpisodeSearchCriteria>(); + allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2); + allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3); + } } } diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 51b0a75cf..ee892b7b9 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -265,7 +265,7 @@ namespace NzbDrone.Core.IndexerSearch } } - if (sceneMapping.ParseTerm == series.CleanTitle && sceneMapping.FilterRegex.IsNullOrWhiteSpace()) + if (sceneMapping.SearchTerm == series.Title && sceneMapping.FilterRegex.IsNullOrWhiteSpace()) { // Disable the implied mapping if we have an explicit mapping by the same name includeGlobal = false; From 6b08e849b80332cfab3ac2f62fc19a4c5a29430a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 27 May 2024 17:29:29 -0700 Subject: [PATCH 317/762] Search for raw and clean titles for Newznab/Torznab indexers that support raw title searching --- .../IndexerSearch/Definitions/SearchCriteriaBase.cs | 1 + src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 49b302df8..7aeddba02 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public virtual bool UserInvokedSearch { get; set; } public virtual bool InteractiveSearch { get; set; } + public List<string> AllSceneTitles => SceneTitles.Concat(CleanSceneTitles).Distinct().ToList(); public List<string> CleanSceneTitles => SceneTitles.Select(GetCleanSceneTitle).Distinct().ToList(); public static string GetCleanSceneTitle(string title) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index ed09945cd..4786900a9 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -410,7 +410,7 @@ namespace NzbDrone.Core.Indexers.Newznab $"&season={NewznabifySeasonNumber(searchCriteria.SeasonNumber)}&ep={searchCriteria.EpisodeNumber}"); } - var queryTitles = TextSearchEngine == "raw" ? searchCriteria.SceneTitles : searchCriteria.CleanSceneTitles; + var queryTitles = TextSearchEngine == "raw" ? searchCriteria.AllSceneTitles : searchCriteria.CleanSceneTitles; foreach (var queryTitle in queryTitles) { From d9b771ab0b705ace6a95ee92743aad3bf6b68dd8 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 1 Jun 2024 03:11:31 +0300 Subject: [PATCH 318/762] Fixed: Error sending Manual Interaction Required when series is unknown --- src/NzbDrone.Core/Notifications/NotificationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 649f69581..fc9c3e865 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -233,7 +233,7 @@ namespace NzbDrone.Core.Notifications public void Handle(ManualInteractionRequiredEvent message) { - var series = message.Episode.Series; + var series = message.Episode?.Series; var mess = ""; if (series != null) @@ -255,7 +255,7 @@ namespace NzbDrone.Core.Notifications { Message = mess, Series = series, - Quality = message.Episode.ParsedEpisodeInfo.Quality, + Quality = message.Episode?.ParsedEpisodeInfo.Quality, Episode = message.Episode, TrackedDownload = message.TrackedDownload, DownloadClientInfo = message.TrackedDownload.DownloadItem?.DownloadClientInfo, From e07eb05e8b4c72dae9b42369b82aad32927f5b82 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 7 Jun 2024 11:25:09 +0000 Subject: [PATCH 319/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: AlbertCoolGuy <Albert.rosenstand@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: xuzhihui <5894940@qq.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 6 +- src/NzbDrone.Core/Localization/Core/es.json | 80 +++++++++---------- .../Localization/Core/pt_BR.json | 2 +- .../Localization/Core/zh_CN.json | 2 +- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 740402a20..b811fa1ce 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -34,5 +34,9 @@ "AddImportListImplementation": "Tilføj importliste - {implementationName}", "AddRootFolderError": "Kunne ikke tilføje rodmappe", "Table": "Tabel", - "AddIndexer": "Tilføj indekser" + "AddIndexer": "Tilføj indekser", + "AddDownloadClient": "Tilføj downloadklient", + "AddImportListExclusion": "Tilføj ekslusion til importeringslisten", + "AddDelayProfileError": "Kan ikke tilføje en ny forsinkelsesprofil. Prøv venligst igen.", + "AddDownloadClientError": "Ikke muligt at tilføje en ny downloadklient. Prøv venligst igen." } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 2fe54a8ee..37ec9c7a7 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1,7 +1,7 @@ { "Added": "Añadido", "ApplyChanges": "Aplicar Cambios", - "AuthBasic": "Básico (ventana emergente del navegador)", + "AuthBasic": "Básica (Ventana emergente del navegador)", "BackupFolderHelpText": "Las rutas relativas estarán en el directorio AppData de {appName}", "BackupsLoadError": "No se pudieron cargar las copias de seguridad", "Enable": "Habilitar", @@ -36,22 +36,22 @@ "AddNewRestriction": "Añadir nueva restricción", "AddRemotePathMapping": "Añadir Asignación de Ruta Remota", "Analytics": "Analíticas", - "ApiKey": "Clave de API", + "ApiKey": "Clave API", "AnimeEpisodeFormat": "Formato de Episodio de Anime", "ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base", "ApplyTagsHelpTextReplace": "Reemplazar: Sustituye las etiquetas por las introducidas (introduce \"no tags\" para borrar todas las etiquetas)", "ApplicationURL": "URL de la aplicación", "Authentication": "Autenticación", "AuthForm": "Formularios (Página de inicio de sesión)", - "AuthenticationMethodHelpText": "Requerir nombre de usuario y contraseña para acceder {appName}", + "AuthenticationMethodHelpText": "Requiere usuario y contraseña para acceder a {appName}", "AuthenticationRequired": "Autenticación requerida", "AutoTaggingLoadError": "No se pudo cargar el etiquetado automático", - "AutoRedownloadFailedHelpText": "Buscar e intentar descargar automáticamente una versión diferente", + "AutoRedownloadFailedHelpText": "Busca e intenta descargar automáticamente una versión diferente", "Backup": "Copia de seguridad", "AutomaticSearch": "Búsqueda Automática", "Automatic": "Automático", "BindAddressHelpText": "Dirección IP4 válida, localhost o '*' para todas las interfaces", - "BindAddress": "Dirección de Ligado", + "BindAddress": "Dirección de enlace", "Branch": "Rama", "BuiltIn": "Integrado", "Condition": "Condición", @@ -65,7 +65,7 @@ "Duplicate": "Duplicar", "Error": "Error", "Episodes": "Episodios", - "External": "Externo", + "External": "Externa", "Extend": "Extender", "Restore": "Restaurar", "Security": "Seguridad", @@ -78,10 +78,10 @@ "Torrents": "Torrents", "Ui": "Interfaz", "Underscore": "Guion bajo", - "UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script", + "UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script", "Warn": "Advertencia", "AutoTagging": "Etiquetado Automático", - "AddAutoTag": "Añadir Etiqueta Automática", + "AddAutoTag": "Añadir etiqueta automática", "AddCondition": "Añadir Condición", "AbsoluteEpisodeNumbers": "Número(s) de Episodio Absoluto(s)", "AirDate": "Fecha de Emisión", @@ -165,7 +165,7 @@ "AddRemotePathMappingError": "No se pudo añadir una nueva asignación de ruta remota, inténtelo de nuevo.", "AgeWhenGrabbed": "Antigüedad (cuando se añadió)", "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.", + "AnalyseVideoFilesHelpText": "Extrae 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": "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", @@ -174,7 +174,7 @@ "Clone": "Clonar", "Connections": "Conexiones", "Dash": "Guion", - "AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", + "AnalyticsEnabledHelpText": "Envía información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre tu navegador, qué páginas de interfaz web de {appName} utilizas, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", "BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas", "BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente", "AddNewSeries": "Añadir Nueva Serie", @@ -182,7 +182,7 @@ "AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empiece escribiendo el nombre de la serie que desea añadir.", "AddNewSeriesRootFolderHelpText": "La subcarpeta '{folder}' será creada automáticamente", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con límites no alcanzados", - "AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes", + "AddNewSeriesSearchForMissingEpisodes": "Empezar la búsqueda de episodios faltantes", "AddQualityProfile": "Añadir Perfil de Calidad", "AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.", "AddReleaseProfile": "Añadir perfil de lanzamiento", @@ -199,7 +199,7 @@ "EditSelectedDownloadClients": "Editar Clientes de Descarga Seleccionados", "DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?", "Implementation": "Implementación", - "ImportUsingScript": "Importar Script de Uso", + "ImportUsingScript": "Importar usando un script", "CloneAutoTag": "Clonar Etiquetado Automático", "ManageIndexers": "Gestionar Indexadores", "DeleteAutoTag": "Eliminar Etiquetado Automático", @@ -219,15 +219,15 @@ "AddConditionImplementation": "Añadir condición - {implementationName}", "AppUpdated": "{appName} Actualizado", "AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script", - "AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.", - "AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación esté habilitada. Opcionalmente puede desactivar la autenticación desde una dirección local.", - "AuthenticationRequiredPasswordHelpTextWarning": "Introduzca una nueva contraseña", - "AuthenticationRequiredUsernameHelpTextWarning": "Introduzca un nuevo nombre de usuario", + "AuthenticationRequiredHelpText": "Cambia para qué solicitudes se requiere autenticación. No cambiar a menos que entiendas los riesgos.", + "AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación sea habilitada. Opcionalmente puedes deshabilitar la autenticación desde direcciones locales.", + "AuthenticationRequiredPasswordHelpTextWarning": "Introduce una nueva contraseña", + "AuthenticationRequiredUsernameHelpTextWarning": "Introduce un nuevo usuario", "AuthenticationMethod": "Método de autenticación", "AddConnectionImplementation": "Añadir Conexión - {implementationName}", "AddDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", "VideoDynamicRange": "Video de Rango Dinámico", - "AuthenticationMethodHelpTextWarning": "Por favor selecciona un método válido de autenticación", + "AuthenticationMethodHelpTextWarning": "Por favor selecciona un método de autenticación válido", "AddCustomFilter": "Añadir Filtro Personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Puntuación mínima de formato personalizado", "CountIndexersSelected": "{count} indexador(es) seleccionado(s)", @@ -266,12 +266,12 @@ "DeleteSelectedImportListsMessageText": "¿Estás seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", "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", + "DisabledForLocalAddresses": "Deshabilitada para direcciones locales", "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", - "MonitorRecentEpisodesDescription": "Monitorizar episodios emitidos en los últimos 90 días y los episodios futuros", + "MonitorRecentEpisodesDescription": "Monitoriza episodios emitidos en los últimos 90 días y los episodios futuros", "MonitorSelected": "Monitorizar seleccionados", "MonitorSeries": "Monitorizar Series", "NoHistory": "Sin historial", @@ -288,7 +288,7 @@ "MonitorNoNewSeasonsDescription": "No monitorizar automáticamente ninguna temporada nueva", "HistoryLoadError": "No se pudo cargar el historial", "LibraryImport": "Importar Librería", - "RescanSeriesFolderAfterRefresh": "Re-escanear la Carpeta de Series tras Actualizar", + "RescanSeriesFolderAfterRefresh": "Volver a escanear la carpeta de series tras actualizar", "Wanted": "Buscado", "MonitorPilotEpisodeDescription": "Sólo monitorizar el primer episodio de la primera temporada", "MonitorRecentEpisodes": "Episodios Recientes", @@ -300,14 +300,14 @@ "MonitorSpecialEpisodes": "Monitorizar Especiales", "Queue": "Cola", "RescanAfterRefreshHelpTextWarning": "{appName} no detectará automáticamente cambios en los archivos si no se elige 'Siempre'", - "RescanAfterRefreshSeriesHelpText": "Re-escanear la carpeta de series tras actualizar las series", + "RescanAfterRefreshSeriesHelpText": "Vuelve a escanear la carpeta de series tras actualizar las series", "MonitorNoNewSeasons": "Sin Nuevas Temporadas", - "MonitorSpecialEpisodesDescription": "Monitorizar todos los episodios especiales sin cambiar el estado de monitorizado de otros episodios", + "MonitorSpecialEpisodesDescription": "Monitoriza todos los episodios especiales sin cambiar el estado de monitorizado de otros episodios", "Calendar": "Calendario", "BlocklistRelease": "Lista de bloqueos de lanzamiento", "CountSeasons": "{count} Temporadas", "BranchUpdate": "Rama a usar para actualizar {appName}", - "ChmodFolder": "Carpeta chmod", + "ChmodFolder": "chmod de la carpeta", "CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles", "ChooseAnotherFolder": "Elige otra Carpeta", "ClientPriority": "Prioridad del Cliente", @@ -317,7 +317,7 @@ "CalendarLoadError": "Incapaz de cargar el calendario", "CertificateValidation": "Validacion de certificado", "BypassProxyForLocalAddresses": "Omitir Proxy para Direcciones Locales", - "ChangeFileDateHelpText": "Cambiar la fecha del archivo al importar/rescan", + "ChangeFileDateHelpText": "Cambia la fecha del archivo al importar/volver a escanear", "ChownGroupHelpText": "Nombre del grupo o gid. Utilice gid para sistemas de archivos remotos.", "CloneProfile": "Clonar Perfil", "CollectionsLoadError": "No se han podido cargar las colecciones", @@ -355,7 +355,7 @@ "Agenda": "Agenda", "Cancel": "Cancelar", "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.", + "CertificateValidationHelpText": "Cambia cómo de estricta es la validación de certificación de HTTPS. No cambiar a menos que entiendas los riesgos.", "AddListExclusion": "Añadir lista de exclusión", "AddedDate": "Agregado: {date}", "AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado", @@ -422,7 +422,7 @@ "FailedToFetchUpdates": "Fallo al buscar las actualizaciones", "FailedToUpdateSettings": "Fallo al actualizar los ajustes", "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver historial de commits de Github para mas detalle", - "CreateEmptySeriesFoldersHelpText": "Cree carpetas de series faltantes durante el análisis del disco", + "CreateEmptySeriesFoldersHelpText": "Crea carpetas de series faltantes durante el análisis del disco", "DefaultCase": "Caso predeterminado", "Daily": "Diario", "CollapseMultipleEpisodesHelpText": "Contraer varios episodios que se emiten el mismo día", @@ -465,7 +465,7 @@ "InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado", "InteractiveSearchModalHeader": "Búsqueda interactiva", "InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración", - "ChownGroup": "chown grupo", + "ChownGroup": "chown del grupo", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo", "ContinuingSeriesDescription": "Se esperan más episodios u otra temporada", @@ -598,7 +598,7 @@ "DownloadClientNzbgetValidationKeepHistoryZero": "La configuración KeepHistory de NzbGet debería ser mayor de 0", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.", "DownloadClientDownloadStationValidationSharedFolderMissing": "La carpeta compartida no existe", - "DownloadPropersAndRepacksHelpText": "Decidir si automáticamente actualizar a Propers/Repacks", + "DownloadPropersAndRepacksHelpText": "Actualiza automáticamente o no a Propers/Repacks", "EditListExclusion": "Editar exclusión de lista", "EnableAutomaticAdd": "Habilitar añadido automático", "EditQualityProfile": "Editar perfil de calidad", @@ -737,7 +737,7 @@ "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", + "EnableCompletedDownloadHandlingHelpText": "Importa automáticamente las descargas completas del gestor de descargas", "EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador", "EnableRss": "Habilitar RSS", "Ended": "Terminado", @@ -783,7 +783,7 @@ "FilterDoesNotEndWith": "no termina en", "Fixed": "Arreglado", "Global": "Global", - "Enabled": "Habilitado", + "Enabled": "Habilitada", "EpisodeHistoryLoadError": "No se puede cargar el historial del episodio", "EpisodeIsDownloading": "El episodio se está descargando", "EpisodeHasNotAired": "El episodio no está en emisión", @@ -801,7 +801,7 @@ "GeneralSettings": "Opciones generales", "GeneralSettingsLoadError": "No se pueden cargar las opciones generales", "Grab": "Capturar", - "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} no pudo determinar para qué serie y episodio era este lanzamiento. {appName} no pudo automáticamente importar este lanzamiento. ¿Te gustaría capturar '{title}'?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} no pudo determinar para qué serie y episodio era este lanzamiento. {appName} no pudo importar automáticamente este lanzamiento. ¿Te gustaría capturar '{title}'?", "HasMissingSeason": "Tiene temporadas faltantes", "ImportListSearchForMissingEpisodesHelpText": "Una vez se añada la serie a {appName}, buscar automáticamente episodios faltantes", "ImportListsSonarrValidationInvalidUrl": "La URL de {appName} es inválida. ¿Te falta la URL base?", @@ -989,7 +989,7 @@ "ImportListsValidationInvalidApiKey": "La clave API es inválida", "ImportListsValidationTestFailed": "El test fue abortado debido a un error: {exceptionMessage}", "ImportScriptPathHelpText": "La ruta al script a usar para importar", - "ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)", + "ImportUsingScriptHelpText": "Copia archivos para importar usando un script (p. ej. para transcodificación)", "Importing": "Importando", "IncludeUnmonitored": "Incluir no monitorizadas", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas", @@ -1040,7 +1040,7 @@ "ImportMechanismHandlingDisabledHealthCheckMessage": "Habilitar Gestión de descargas completadas", "ImportedTo": "Importar a", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado cuando se renombra", - "CleanLibraryLevel": "Limpiar el nivel de la librería", + "CleanLibraryLevel": "Limpiar nivel de biblioteca", "SearchForCutoffUnmetEpisodes": "Buscar todos los episodios con límites no alcanzados", "IconForSpecials": "Icono para Especiales", "ImportListExclusions": "Importar lista de exclusiones", @@ -1067,7 +1067,7 @@ "IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas", "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", + "PasswordConfirmation": "Confirmación de contraseña", "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", @@ -1189,7 +1189,7 @@ "ListSyncTag": "Etiqueta de Sincronización de Lista", "ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)", "MetadataLoadError": "No se puede cargar Metadatos", - "MetadataSourceSettingsSeriesSummary": "Información de dónde {appName} obtiene información de series y episodio", + "MetadataSourceSettingsSeriesSummary": "Fuente de información de donde {appName} obtiene información de series y episodios", "Max": "Máximo", "MaximumSizeHelpText": "Tamaño máximo en MB para que un lanzamiento sea capturado. Establece a cero para establecer a ilimitado", "MatchedToEpisodes": "Ajustado a Episodios", @@ -1486,7 +1486,7 @@ "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", + "RemoveTagsAutomaticallyHelpText": "Elimina 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", @@ -1595,7 +1595,7 @@ "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", + "ThemeHelpText": "Cambia 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", @@ -1621,7 +1621,7 @@ "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.", + "RemoveDownloadsAlert": "Las opciones de eliminación fueron trasladadas a las opciones del cliente de descarga individual en la tabla anterior.", "RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto", "SelectFolder": "Seleccionar carpeta", "TestAllClients": "Probar todos los clientes", @@ -1894,7 +1894,7 @@ "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", + "UpdateAutomaticallyHelpText": "Descarga e instala actualizaciones automáticamente. Podrás seguir instalándolas 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", @@ -1916,7 +1916,7 @@ "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", + "UnmonitorSpecialsEpisodesDescription": "Deja 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", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 1d04b124e..eafa190a4 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -432,7 +432,7 @@ "AuthBasic": "Básico (pop-up do navegador)", "AuthForm": "Formulário (página de login)", "Authentication": "Autenticação", - "AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar o {appName}", + "AuthenticationMethodHelpText": "Exigir Nome de Usuário e Senha para acessar {appName}", "AuthenticationRequired": "Autenticação exigida", "AutoRedownloadFailedHelpText": "Procurar automaticamente e tente baixar uma versão diferente", "AutoTaggingLoadError": "Não foi possível carregar tagging automática", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index e33dcd88c..b1fde6f60 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -550,7 +550,7 @@ "AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的版本", "AutoTaggingLoadError": "无法加载自动标记", "Automatic": "自动化", - "AutoTaggingRequiredHelpText": "这个{implementationName}条件必须匹配自动标记规则才能应用。否则,一个{implementationName}匹配就足够了。", + "AutoTaggingRequiredHelpText": "这个{0}条件必须匹配自动标记规则才能应用。否则,一个{0}匹配就足够了。", "AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。", "BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理", "BackupsLoadError": "无法加载备份", From ea54ade9bfcb51809797edf940b04d23b3c2eeb0 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 30 May 2024 00:25:51 +0300 Subject: [PATCH 320/762] New: Refresh cache for tracked queue on series add --- .../TrackedDownloadService.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 1c06d369c..2f789f23b 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public class TrackedDownloadService : ITrackedDownloadService, IHandle<EpisodeInfoRefreshedEvent>, + IHandle<SeriesAddedEvent>, IHandle<SeriesDeletedEvent> { private readonly IParsingService _parsingService; @@ -278,12 +279,29 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } + public void Handle(SeriesAddedEvent message) + { + var cachedItems = _cache.Values + .Where(t => + t.RemoteEpisode?.Series == null || + message.Series?.TvdbId == t.RemoteEpisode.Series.TvdbId) + .ToList(); + + if (cachedItems.Any()) + { + cachedItems.ForEach(UpdateCachedItem); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); + } + } + public void Handle(SeriesDeletedEvent message) { - var cachedItems = _cache.Values.Where(t => - t.RemoteEpisode?.Series != null && - message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id)) - .ToList(); + var cachedItems = _cache.Values + .Where(t => + t.RemoteEpisode?.Series != null && + message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id || s.TvdbId == t.RemoteEpisode.Series.TvdbId)) + .ToList(); if (cachedItems.Any()) { From 0edc5ba99a15c5f80305b387a053f35fc3f6e51b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:09:39 +0300 Subject: [PATCH 321/762] Fixed: Ignore case for name validation in providers --- src/Sonarr.Api.V3/ProviderControllerBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index 53586a251..e49e16bdc 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -3,6 +3,7 @@ using System.Linq; using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -32,7 +33,7 @@ namespace Sonarr.Api.V3 _bulkResourceMapper = bulkResourceMapper; SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name.EqualsIgnoreCase(c) && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); From a90ab1a8fd50126d7f60eaa684eac1e0cd98e2b7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:10:08 +0300 Subject: [PATCH 322/762] Fixed: Ignore case when resolving indexer by name in release push --- src/Sonarr.Api.V3/Indexers/ReleasePushController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index 96bcaf6c2..8e68c23ea 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -86,7 +86,8 @@ namespace Sonarr.Api.V3.Indexers { if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace()) { - var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer); + var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name.EqualsIgnoreCase(release.Indexer)); + if (indexer != null) { release.IndexerId = indexer.Id; From 378fedcd9dcb0fe07585727dd7d9e5e765c863c0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 10 Jun 2024 20:30:03 -0700 Subject: [PATCH 323/762] Fixed: Skip invalid series paths during validation --- src/NzbDrone.Core/Tv/MoveSeriesService.cs | 6 ++++++ src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs | 5 +++-- src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Tv/MoveSeriesService.cs b/src/NzbDrone.Core/Tv/MoveSeriesService.cs index be9037d96..0a09eaac0 100644 --- a/src/NzbDrone.Core/Tv/MoveSeriesService.cs +++ b/src/NzbDrone.Core/Tv/MoveSeriesService.cs @@ -37,6 +37,12 @@ namespace NzbDrone.Core.Tv private void MoveSingleSeries(Series series, string sourcePath, string destinationPath, int? index = null, int? total = null) { + if (!sourcePath.IsPathValid(PathValidationType.CurrentOs)) + { + _logger.Warn("Folder '{0}' for '{1}' is invalid, unable to move series. Try moving files manually", sourcePath, series.Title); + return; + } + if (!_diskProvider.FolderExists(sourcePath)) { _logger.Debug("Folder '{0}' for '{1}' does not exist, not moving.", sourcePath, series.Title); diff --git a/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs b/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs index 7f3db8ed3..170c73137 100644 --- a/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs @@ -1,4 +1,5 @@ -using FluentValidation.Validators; +using FluentValidation.Validators; +using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.RootFolders; @@ -24,7 +25,7 @@ namespace NzbDrone.Core.Validation.Paths context.MessageFormatter.AppendArgument("path", context.PropertyValue.ToString()); - return !_rootFolderService.All().Exists(r => r.Path.PathEquals(context.PropertyValue.ToString())); + return !_rootFolderService.All().Exists(r => r.Path.IsPathValid(PathValidationType.CurrentOs) && r.Path.PathEquals(context.PropertyValue.ToString())); } } } diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs index 66f2f7689..da8705ff0 100644 --- a/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs @@ -1,5 +1,6 @@ using System.Linq; using FluentValidation.Validators; +using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tv; @@ -28,7 +29,10 @@ namespace NzbDrone.Core.Validation.Paths dynamic instance = context.ParentContext.InstanceToValidate; var instanceId = (int)instance.Id; - return !_seriesService.GetAllSeriesPaths().Any(s => s.Value.PathEquals(context.PropertyValue.ToString()) && s.Key != instanceId); + // Skip the path for this series and any invalid paths + return !_seriesService.GetAllSeriesPaths().Any(s => s.Key != instanceId && + s.Value.IsPathValid(PathValidationType.CurrentOs) && + s.Value.PathEquals(context.PropertyValue.ToString())); } } } From 52b72925f9d42c896144dde3099dc19c397327b0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 7 Jun 2024 15:43:06 -0700 Subject: [PATCH 324/762] Fixed: Improve error messaging if config file isn't formatted correctly Closes #6860 --- .../Configuration/ConfigFileProvider.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 80b4e30fe..3329dee2c 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -419,13 +419,21 @@ namespace NzbDrone.Core.Configuration throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it."); } - return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); + var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile)); + var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList(); + + if (config.Count != 1) + { + throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Sonarr will recreate it."); + } + + return xDoc; } - var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); - xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); + var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); + newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); - return xDoc; + return newXDoc; } } catch (XmlException ex) From c331c8bd119fa9f85a53e96db04f541b2d90bbd3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:23:33 +0300 Subject: [PATCH 325/762] Ignore `Grabbed` from API docs Run application in docs.sh specific to platform --- docs.sh | 12 +- src/NzbDrone.Host/Sonarr.Host.csproj | 2 +- src/Sonarr.Api.V3/Episodes/EpisodeResource.cs | 2 + src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 1 + src/Sonarr.Api.V3/openapi.json | 584 ++++++++---------- 5 files changed, 261 insertions(+), 340 deletions(-) diff --git a/docs.sh b/docs.sh index a0f21c41a..386f5df68 100755 --- a/docs.sh +++ b/docs.sh @@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln platform=Posix +if [ "$PLATFORM" = "Windows" ]; then + application=Sonarr.Console.dll +else + application=Sonarr.dll +fi + dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet new tool-manifest -dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 & +dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 & -sleep 30 +sleep 45 kill %1 diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index 0ccf5c4a2..7eb3a4058 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -6,7 +6,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.21" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> - <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> + <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs index 551d9bb69..9a0ff01fa 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Tv; using Sonarr.Api.V3.EpisodeFiles; using Sonarr.Api.V3.Series; using Sonarr.Http.REST; +using Swashbuckle.AspNetCore.Annotations; namespace Sonarr.Api.V3.Episodes { @@ -40,6 +41,7 @@ namespace Sonarr.Api.V3.Episodes // Hiding this so people don't think its usable (only used to set the initial state) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [SwaggerIgnore] public bool Grabbed { get; set; } } diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 332f902ee..659783773 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -6,6 +6,7 @@ <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Ical.Net" Version="4.2.0" /> <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" /> </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 ff1d2403b..251cdae2f 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -34,7 +34,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -86,7 +86,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -96,7 +96,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -108,7 +108,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -129,7 +129,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -156,7 +156,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -197,7 +197,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -235,7 +235,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -256,7 +256,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -275,7 +275,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -287,7 +287,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -336,7 +336,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -359,7 +359,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -371,7 +371,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -438,7 +438,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -468,7 +468,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -499,7 +499,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -569,7 +569,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -602,7 +602,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -673,7 +673,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -694,7 +694,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -711,7 +711,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -744,7 +744,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -765,7 +765,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -784,7 +784,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -813,7 +813,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -861,7 +861,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -899,7 +899,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -920,7 +920,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -948,7 +948,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -975,7 +975,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1016,7 +1016,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1054,7 +1054,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -1075,7 +1075,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1094,7 +1094,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1172,7 +1172,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1202,7 +1202,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1230,7 +1230,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1257,7 +1257,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1290,7 +1290,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -1319,7 +1319,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1357,7 +1357,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1395,7 +1395,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1433,7 +1433,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1455,7 +1455,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1494,7 +1494,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1540,7 +1540,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1568,7 +1568,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -1589,7 +1589,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1617,7 +1617,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1643,7 +1643,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1655,7 +1655,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1696,7 +1696,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1708,7 +1708,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1739,7 +1739,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1751,7 +1751,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1789,7 +1789,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1827,7 +1827,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1891,7 +1891,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1933,7 +1933,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -1971,7 +1971,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2009,7 +2009,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2042,7 +2042,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2083,7 +2083,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -2121,7 +2121,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -2142,7 +2142,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2170,7 +2170,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2191,7 +2191,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -2213,7 +2213,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2250,7 +2250,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2271,7 +2271,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2292,7 +2292,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2304,7 +2304,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2433,7 +2433,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2485,7 +2485,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2548,7 +2548,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2581,7 +2581,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2593,7 +2593,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -2651,7 +2651,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -2689,7 +2689,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2708,7 +2708,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2747,7 +2747,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2793,7 +2793,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2821,7 +2821,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -2842,7 +2842,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2870,7 +2870,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2896,7 +2896,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2908,7 +2908,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2949,7 +2949,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2961,7 +2961,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2992,7 +2992,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3004,7 +3004,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3042,7 +3042,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3080,7 +3080,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3099,7 +3099,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3129,7 +3129,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3192,7 +3192,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3230,7 +3230,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3268,7 +3268,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -3289,7 +3289,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3308,7 +3308,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3347,7 +3347,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3393,7 +3393,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3421,7 +3421,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -3442,7 +3442,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3470,7 +3470,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3496,7 +3496,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3508,7 +3508,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3549,7 +3549,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3561,7 +3561,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3592,7 +3592,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3604,7 +3604,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3642,7 +3642,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3680,7 +3680,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3699,7 +3699,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3737,7 +3737,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3786,7 +3786,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3814,7 +3814,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3832,7 +3832,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3866,7 +3866,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } }, "deprecated": true @@ -3896,7 +3896,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3925,7 +3925,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3944,7 +3944,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3964,7 +3964,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3983,7 +3983,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4013,7 +4013,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4073,7 +4073,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4092,7 +4092,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4125,7 +4125,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4177,7 +4177,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4209,7 +4209,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4241,7 +4241,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4253,7 +4253,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4291,7 +4291,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -4329,7 +4329,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4348,7 +4348,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4387,7 +4387,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4433,7 +4433,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4461,7 +4461,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -4482,7 +4482,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4501,7 +4501,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4542,7 +4542,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4554,7 +4554,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4585,7 +4585,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4655,7 +4655,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4685,7 +4685,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4704,7 +4704,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -4762,7 +4762,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -4800,7 +4800,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4908,7 +4908,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -4920,7 +4920,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -4959,7 +4959,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5005,7 +5005,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5033,7 +5033,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -5054,7 +5054,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5073,7 +5073,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5114,7 +5114,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5126,7 +5126,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5157,7 +5157,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5185,7 +5185,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5204,7 +5204,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5221,7 +5221,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5269,7 +5269,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -5307,7 +5307,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5326,7 +5326,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -5392,7 +5392,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5413,7 +5413,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -5440,7 +5440,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5473,7 +5473,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -5502,7 +5502,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -5540,7 +5540,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5559,7 +5559,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -5631,7 +5631,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5696,7 +5696,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5803,7 +5803,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5833,7 +5833,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5854,7 +5854,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -5903,7 +5903,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5925,7 +5925,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -5953,7 +5953,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -5989,7 +5989,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6030,7 +6030,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6057,7 +6057,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6106,7 +6106,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -6145,7 +6145,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6183,7 +6183,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6211,7 +6211,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6258,7 +6258,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6285,7 +6285,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6318,7 +6318,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -6357,7 +6357,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6395,7 +6395,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6432,7 +6432,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6463,7 +6463,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6490,7 +6490,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6523,7 +6523,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -6544,7 +6544,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6572,7 +6572,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6602,7 +6602,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6631,7 +6631,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6679,7 +6679,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6723,7 +6723,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -6777,7 +6777,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6808,7 +6808,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -6837,7 +6837,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6877,7 +6877,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6898,7 +6898,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6921,7 +6921,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6943,7 +6943,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6966,7 +6966,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -6978,7 +6978,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -6997,7 +6997,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -7009,7 +7009,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -7021,7 +7021,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -7033,7 +7033,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -7045,7 +7045,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7074,7 +7074,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -7122,7 +7122,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -7160,7 +7160,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -7181,7 +7181,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7200,7 +7200,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7233,7 +7233,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7252,7 +7252,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -7301,7 +7301,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7339,7 +7339,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -7377,7 +7377,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7396,7 +7396,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7415,7 +7415,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7437,7 +7437,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -7470,7 +7470,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -7880,7 +7880,9 @@ "nullable": true }, "duration": { - "$ref": "#/components/schemas/TimeSpan" + "type": "string", + "format": "date-span", + "nullable": true }, "exception": { "type": "string", @@ -8494,9 +8496,6 @@ "$ref": "#/components/schemas/MediaCover" }, "nullable": true - }, - "grabbed": { - "type": "boolean" } }, "additionalProperties": false @@ -9148,7 +9147,8 @@ "format": "int32" }, "minRefreshInterval": { - "$ref": "#/components/schemas/TimeSpan" + "type": "string", + "format": "date-span" } }, "additionalProperties": false @@ -10658,7 +10658,9 @@ "format": "double" }, "timeleft": { - "$ref": "#/components/schemas/TimeSpan" + "type": "string", + "format": "date-span", + "nullable": true }, "estimatedCompletionTime": { "type": "string", @@ -11823,7 +11825,8 @@ "$ref": "#/components/schemas/AuthenticationType" }, "sqliteVersion": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "migrationVersion": { "type": "integer", @@ -11834,7 +11837,8 @@ "nullable": true }, "runtimeVersion": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "runtimeName": { "type": "string", @@ -11860,7 +11864,8 @@ "nullable": true }, "databaseVersion": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "databaseType": { "$ref": "#/components/schemas/DatabaseType" @@ -11992,66 +11997,8 @@ "format": "date-time" }, "lastDuration": { - "$ref": "#/components/schemas/TimeSpan" - } - }, - "additionalProperties": false - }, - "TimeSpan": { - "type": "object", - "properties": { - "ticks": { - "type": "integer", - "format": "int64" - }, - "days": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "hours": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "milliseconds": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minutes": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "seconds": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "totalDays": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalHours": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalMilliseconds": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalMinutes": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalSeconds": { - "type": "number", - "format": "double", + "type": "string", + "format": "date-span", "readOnly": true } }, @@ -12194,7 +12141,8 @@ "format": "int32" }, "version": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "branch": { "type": "string", @@ -12235,42 +12183,6 @@ } }, "additionalProperties": false - }, - "Version": { - "type": "object", - "properties": { - "major": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minor": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "build": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "revision": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "majorRevision": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minorRevision": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false } }, "securitySchemes": { From e1b937e8d5dffeb5211cf3445b95b51a00fb5637 Mon Sep 17 00:00:00 2001 From: Stephan Sundermann <stephansundermann@gmail.com> Date: Tue, 18 Jun 2024 05:38:41 +0200 Subject: [PATCH 326/762] New: Add TMDB ID support Closes #6866 --- .../Page/Header/SeriesSearchInputConnector.js | 2 ++ .../Page/Header/SeriesSearchResult.js | 10 +++++++ .../src/Components/Page/Header/fuse.worker.js | 1 + frontend/src/Series/Details/SeriesDetails.js | 3 ++ .../src/Series/Details/SeriesDetailsLinks.js | 22 +++++++++++++-- frontend/src/Series/Series.ts | 1 + .../MediaManagement/Naming/NamingModal.js | 1 + .../NewznabRequestGeneratorFixture.cs | 21 +++++++++++--- .../FileNameBuilderTests/IdFixture.cs | 9 ++++++ .../TvTests/RefreshSeriesServiceFixture.cs | 14 ++++++++++ .../Datastore/Migration/206_add_tmdbid.cs | 15 ++++++++++ .../Consumers/Xbmc/KodiEpisodeGuide.cs | 1 + .../Newznab/NewznabRequestGenerator.cs | 28 +++++++++++++++++-- .../MediaFiles/ScriptImportDecider.cs | 1 + .../SkyHook/Resource/ShowResource.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 5 ++++ .../CustomScript/CustomScript.cs | 7 +++++ .../MediaBrowser/MediaBrowserItems.cs | 1 + .../MediaBrowser/MediaBrowserProxy.cs | 5 ++++ .../Notifications/Webhook/WebhookSeries.cs | 2 ++ .../Organizer/FileNameBuilder.cs | 1 + .../Organizer/FileNameSampleService.cs | 9 ++++-- src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 1 + src/NzbDrone.Core/Tv/Series.cs | 1 + src/Sonarr.Api.V3/Series/SeriesResource.cs | 3 ++ src/Sonarr.Api.V3/openapi.json | 4 +++ 26 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs diff --git a/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js index afa6f25aa..6dab05693 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js +++ b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js @@ -21,6 +21,7 @@ function createCleanSeriesSelector() { tvdbId, tvMazeId, imdbId, + tmdbId, tags = [] } = series; @@ -33,6 +34,7 @@ function createCleanSeriesSelector() { tvdbId, tvMazeId, imdbId, + tmdbId, firstCharacter: title.charAt(0).toLowerCase(), tags: tags.reduce((acc, id) => { const matchingTag = allTags.find((tag) => tag.id === id); diff --git a/frontend/src/Components/Page/Header/SeriesSearchResult.js b/frontend/src/Components/Page/Header/SeriesSearchResult.js index c43e87605..8a9c35a73 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchResult.js +++ b/frontend/src/Components/Page/Header/SeriesSearchResult.js @@ -14,6 +14,7 @@ function SeriesSearchResult(props) { tvdbId, tvMazeId, imdbId, + tmdbId, tags } = props; @@ -73,6 +74,14 @@ function SeriesSearchResult(props) { null } + { + match.key === 'tmdbId' && tmdbId ? + <div className={styles.alternateTitle}> + TmdbId: {tmdbId} + </div> : + null + } + { tag ? <div className={styles.tagContainer}> @@ -97,6 +106,7 @@ SeriesSearchResult.propTypes = { tvdbId: PropTypes.number, tvMazeId: PropTypes.number, imdbId: PropTypes.string, + tmdbId: PropTypes.number, tags: PropTypes.arrayOf(PropTypes.object).isRequired, match: PropTypes.object.isRequired }; diff --git a/frontend/src/Components/Page/Header/fuse.worker.js b/frontend/src/Components/Page/Header/fuse.worker.js index 53a3fd1fd..23a070617 100644 --- a/frontend/src/Components/Page/Header/fuse.worker.js +++ b/frontend/src/Components/Page/Header/fuse.worker.js @@ -13,6 +13,7 @@ const fuseOptions = { 'tvdbId', 'tvMazeId', 'imdbId', + 'tmdbId', 'tags.label' ] }; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index b4e6303cc..c2fb80b8d 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -175,6 +175,7 @@ class SeriesDetails extends Component { tvdbId, tvMazeId, imdbId, + tmdbId, title, runtime, ratings, @@ -566,6 +567,7 @@ class SeriesDetails extends Component { tvdbId={tvdbId} tvMazeId={tvMazeId} imdbId={imdbId} + tmdbId={tmdbId} /> } kind={kinds.INVERSE} @@ -719,6 +721,7 @@ SeriesDetails.propTypes = { tvdbId: PropTypes.number.isRequired, tvMazeId: PropTypes.number, imdbId: PropTypes.string, + tmdbId: PropTypes.number, title: PropTypes.string.isRequired, runtime: PropTypes.number.isRequired, ratings: PropTypes.object.isRequired, diff --git a/frontend/src/Series/Details/SeriesDetailsLinks.js b/frontend/src/Series/Details/SeriesDetailsLinks.js index f396ecf67..1aa67f297 100644 --- a/frontend/src/Series/Details/SeriesDetailsLinks.js +++ b/frontend/src/Series/Details/SeriesDetailsLinks.js @@ -9,7 +9,8 @@ function SeriesDetailsLinks(props) { const { tvdbId, tvMazeId, - imdbId + imdbId, + tmdbId } = props; return ( @@ -71,6 +72,22 @@ function SeriesDetailsLinks(props) { </Label> </Link> } + + { + !!tmdbId && + <Link + className={styles.link} + to={`https://www.themoviedb.org/tv/${tmdbId}`} + > + <Label + className={styles.linkLabel} + kind={kinds.INFO} + size={sizes.LARGE} + > + TMDB + </Label> + </Link> + } </div> ); } @@ -78,7 +95,8 @@ function SeriesDetailsLinks(props) { SeriesDetailsLinks.propTypes = { tvdbId: PropTypes.number.isRequired, tvMazeId: PropTypes.number, - imdbId: PropTypes.string + imdbId: PropTypes.string, + tmdbId: PropTypes.number }; export default SeriesDetailsLinks; diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index 0e54aff84..321fc7378 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -70,6 +70,7 @@ interface Series extends ModelBase { tvdbId: number; tvMazeId: number; tvRageId: number; + tmdbId: number; useSceneNumbering: boolean; year: number; isSaving?: boolean; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index eec2449cd..3d1099822 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -99,6 +99,7 @@ const seriesTokens = [ const seriesIdTokens = [ { token: '{ImdbId}', example: 'tt12345' }, { token: '{TvdbId}', example: '12345' }, + { token: '{TmdbId}', example: '11223' }, { token: '{TvMazeId}', example: '54321' } ]; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index 47bd40c99..b0fc4998b 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests _singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria { - Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, + Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 }, SceneTitles = new List<string> { "Monkey Island" }, SeasonNumber = 1, EpisodeNumber = 2 @@ -44,14 +44,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests _seasonSearchCriteria = new SeasonSearchCriteria { - Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, + Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 }, SceneTitles = new List<string> { "Monkey Island" }, SeasonNumber = 1, }; _animeSearchCriteria = new AnimeEpisodeSearchCriteria() { - Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, + Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 }, SceneTitles = new List<string>() { "Monkey+Island" }, AbsoluteEpisodeNumber = 100, SeasonNumber = 5, @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests _animeSeasonSearchCriteria = new AnimeSeasonSearchCriteria() { - Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, + Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 }, SceneTitles = new List<string> { "Monkey Island" }, SeasonNumber = 3, }; @@ -268,6 +268,19 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests page.Url.Query.Should().Contain("imdbid=t40"); } + [Test] + public void should_search_by_tmdb_if_supported() + { + _capabilities.SupportedTvSearchParameters = new[] { "q", "tmdbid", "season", "ep" }; + + var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria); + results.GetTier(0).Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.Query.Should().Contain("tmdbid=50"); + } + [Test] public void should_prefer_search_by_tvdbid_if_rid_supported() { diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/IdFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/IdFixture.cs index 456025890..4836abb5a 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/IdFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/IdFixture.cs @@ -56,5 +56,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests Subject.GetSeriesFolder(_series) .Should().Be($"Series Title ({_series.TvMazeId})"); } + + [Test] + public void should_add_tmdb_id() + { + _namingConfig.SeriesFolderFormat = "{Series Title} ({TmdbId})"; + + Subject.GetSeriesFolder(_series) + .Should().Be($"Series Title ({_series.TmdbId})"); + } } } diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs index 429eef2d2..91e6b65f0 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs @@ -137,6 +137,20 @@ namespace NzbDrone.Core.Test.TvTests .Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TvMazeId == newSeriesInfo.TvMazeId), It.IsAny<bool>(), It.IsAny<bool>())); } + [Test] + public void should_update_tmdb_id_if_changed() + { + var newSeriesInfo = _series.JsonClone(); + newSeriesInfo.TmdbId = _series.TmdbId + 1; + + GivenNewSeriesInfo(newSeriesInfo); + + Subject.Execute(new RefreshSeriesCommand(new List<int> { _series.Id })); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TmdbId == newSeriesInfo.TmdbId), It.IsAny<bool>(), It.IsAny<bool>())); + } + [Test] public void should_log_error_if_tvdb_id_not_found() { diff --git a/src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs b/src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs new file mode 100644 index 000000000..0f5151e46 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(206)] + public class add_tmdbid : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series").AddColumn("TmdbId").AsInt32().WithDefaultValue(0); + Create.Index().OnTable("Series").OnColumn("TmdbId"); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/KodiEpisodeGuide.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/KodiEpisodeGuide.cs index 0cb287456..3a85357af 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/KodiEpisodeGuide.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/KodiEpisodeGuide.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc Tvdb = series.TvdbId.ToString(); TvMaze = series.TvMazeId > 0 ? series.TvMazeId.ToString() : null; TvRage = series.TvRageId > 0 ? series.TvMazeId.ToString() : null; + Tmdb = series.TmdbId > 0 ? series.TmdbId.ToString() : null; Imdb = series.ImdbId; } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 4786900a9..fddd5168e 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -119,12 +119,23 @@ namespace NzbDrone.Core.Indexers.Newznab } } + private bool SupportsTmdbSearch + { + get + { + var capabilities = _capabilitiesProvider.GetCapabilities(Settings); + + return capabilities.SupportedTvSearchParameters != null && + capabilities.SupportedTvSearchParameters.Contains("tmdbid"); + } + } + // Combines all ID based searches private bool SupportsTvIdSearches { get { - return SupportsTvdbSearch || SupportsImdbSearch || SupportsTvRageSearch || SupportsTvMazeSearch; + return SupportsTvdbSearch || SupportsImdbSearch || SupportsTvRageSearch || SupportsTvMazeSearch || SupportsTmdbSearch; } } @@ -484,8 +495,9 @@ namespace NzbDrone.Core.Indexers.Newznab var includeImdbSearch = SupportsImdbSearch && searchCriteria.Series.ImdbId.IsNotNullOrWhiteSpace(); var includeTvRageSearch = SupportsTvRageSearch && searchCriteria.Series.TvRageId > 0; var includeTvMazeSearch = SupportsTvMazeSearch && searchCriteria.Series.TvMazeId > 0; + var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.Series.TmdbId > 0; - if (SupportsAggregatedIdSearch && (includeTvdbSearch || includeTvRageSearch || includeTvMazeSearch)) + if (SupportsAggregatedIdSearch && (includeTvdbSearch || includeTvRageSearch || includeTvMazeSearch || includeTmdbSearch)) { var ids = ""; @@ -509,6 +521,11 @@ namespace NzbDrone.Core.Indexers.Newznab ids += "&tvmazeid=" + searchCriteria.Series.TvMazeId; } + if (includeTmdbSearch) + { + ids += "&tmdbid=" + searchCriteria.Series.TmdbId; + } + chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch", ids + parameters)); } else @@ -541,6 +558,13 @@ namespace NzbDrone.Core.Indexers.Newznab "tvsearch", $"&tvmazeid={searchCriteria.Series.TvMazeId}{parameters}")); } + else if (includeTmdbSearch) + { + chain.Add(GetPagedRequests(MaxPages, + categories, + "tvsearch", + $"&tmdbid={searchCriteria.Series.TmdbId}{parameters}")); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs index 6e4b77048..bd1d54080 100644 --- a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs @@ -137,6 +137,7 @@ namespace NzbDrone.Core.MediaFiles environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs index c7acd354a..5cc7fce36 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public string LastAired { get; set; } public int? TvRageId { get; set; } public int? TvMazeId { get; set; } + public int? TmdbId { get; set; } public string Status { get; set; } public int? Runtime { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 9313b0661..c6656816d 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -188,6 +188,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook series.TvMazeId = show.TvMazeId.Value; } + if (show.TmdbId.HasValue) + { + series.TmdbId = show.TmdbId.Value; + } + series.ImdbId = show.ImdbId; series.Title = show.Title; series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title); diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 6b6f3a086..1df4f95a8 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -69,6 +69,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_TitleSlug", series.TitleSlug); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -116,6 +117,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -181,6 +183,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -213,6 +216,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -252,6 +256,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -276,6 +281,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); @@ -345,6 +351,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Path", series.Path); environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs index e47696273..b6e327c86 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Notifications.Emby public string Imdb { get; set; } public int Tvdb { get; set; } public int TvMaze { get; set; } + public int Tmdb { get; set; } public int TvRage { get; set; } } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 428f35a69..3d074034b 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -68,6 +68,11 @@ namespace NzbDrone.Core.Notifications.Emby return MediaBrowserMatchQuality.Id; } + if (item is { ProviderIds.Tmdb: int tmdbid } && tmdbid != 0 && tmdbid == series.TmdbId) + { + return MediaBrowserMatchQuality.Id; + } + if (item is { ProviderIds.TvRage: int tvrageid } && tvrageid != 0 && tvrageid == series.TvRageId) { return MediaBrowserMatchQuality.Id; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs index 1177f2065..de1da85ad 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Notifications.Webhook public string Path { get; set; } public int TvdbId { get; set; } public int TvMazeId { get; set; } + public int TmdbId { get; set; } public string ImdbId { get; set; } public SeriesTypes Type { get; set; } public int Year { get; set; } @@ -31,6 +32,7 @@ namespace NzbDrone.Core.Notifications.Webhook Path = series.Path; TvdbId = series.TvdbId; TvMazeId = series.TvMazeId; + TmdbId = series.TmdbId; ImdbId = series.ImdbId; Type = series.SeriesType; Year = series.Year; diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index a04dfa771..714605a99 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -715,6 +715,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{ImdbId}"] = m => series.ImdbId ?? string.Empty; tokenHandlers["{TvdbId}"] = m => series.TvdbId.ToString(); tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty; + tokenHandlers["{TmdbId}"] = m => series.TmdbId > 0 ? series.TmdbId.ToString() : string.Empty; } private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter) diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index a8d89364a..92bf1e4c1 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -48,7 +48,8 @@ namespace NzbDrone.Core.Organizer Year = 2010, ImdbId = "tt12345", TvdbId = 12345, - TvMazeId = 54321 + TvMazeId = 54321, + TmdbId = 11223 }; _dailySeries = new Series @@ -58,7 +59,8 @@ namespace NzbDrone.Core.Organizer Year = 2010, ImdbId = "tt12345", TvdbId = 12345, - TvMazeId = 54321 + TvMazeId = 54321, + TmdbId = 11223 }; _animeSeries = new Series @@ -68,7 +70,8 @@ namespace NzbDrone.Core.Organizer Year = 2010, ImdbId = "tt12345", TvdbId = 12345, - TvMazeId = 54321 + TvMazeId = 54321, + TmdbId = 11223 }; _episode1 = new Episode diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 41a3b5cae..49aa9ebf6 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -92,6 +92,7 @@ namespace NzbDrone.Core.Tv series.TitleSlug = seriesInfo.TitleSlug; series.TvRageId = seriesInfo.TvRageId; series.TvMazeId = seriesInfo.TvMazeId; + series.TmdbId = seriesInfo.TmdbId; series.ImdbId = seriesInfo.ImdbId; series.AirTime = seriesInfo.AirTime; series.Overview = seriesInfo.Overview; diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 6296d8500..9819773a0 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Tv public int TvRageId { get; set; } public int TvMazeId { get; set; } public string ImdbId { get; set; } + public int TmdbId { get; set; } public string Title { get; set; } public string CleanTitle { get; set; } public string SortTitle { get; set; } diff --git a/src/Sonarr.Api.V3/Series/SeriesResource.cs b/src/Sonarr.Api.V3/Series/SeriesResource.cs index e1c87a434..af1490270 100644 --- a/src/Sonarr.Api.V3/Series/SeriesResource.cs +++ b/src/Sonarr.Api.V3/Series/SeriesResource.cs @@ -51,6 +51,7 @@ namespace Sonarr.Api.V3.Series public int TvdbId { get; set; } public int TvRageId { get; set; } public int TvMazeId { get; set; } + public int TmdbId { get; set; } public DateTime? FirstAired { get; set; } public DateTime? LastAired { get; set; } public SeriesTypes SeriesType { get; set; } @@ -123,6 +124,7 @@ namespace Sonarr.Api.V3.Series TvdbId = model.TvdbId, TvRageId = model.TvRageId, TvMazeId = model.TvMazeId, + TmdbId = model.TmdbId, FirstAired = model.FirstAired, LastAired = model.LastAired, SeriesType = model.SeriesType, @@ -187,6 +189,7 @@ namespace Sonarr.Api.V3.Series TvdbId = resource.TvdbId, TvRageId = resource.TvRageId, TvMazeId = resource.TvMazeId, + TmdbId = resource.TmdbId, FirstAired = resource.FirstAired, SeriesType = resource.SeriesType, CleanTitle = resource.CleanTitle, diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 251cdae2f..1e390e679 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -11585,6 +11585,10 @@ "type": "integer", "format": "int32" }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, "firstAired": { "type": "string", "format": "date-time", From 7fccf590a815179cbb6512c1de33e9cff1962f7d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 17 Jun 2024 20:39:13 -0700 Subject: [PATCH 327/762] Fixed: Adding series with unknown items in queue --- .../Download/Aggregation/RemoteEpisodeAggregationService.cs | 5 +++++ src/NzbDrone.Core/Tv/SeriesScannedHandler.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs b/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs index a2c2ae081..3ad998745 100644 --- a/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs +++ b/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs @@ -25,6 +25,11 @@ namespace NzbDrone.Core.Download.Aggregation public RemoteEpisode Augment(RemoteEpisode remoteEpisode) { + if (remoteEpisode == null) + { + return null; + } + foreach (var augmenter in _augmenters) { try diff --git a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs b/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs index 60efcc6e5..e4d8ea90a 100644 --- a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs +++ b/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs @@ -58,12 +58,12 @@ namespace NzbDrone.Core.Tv } else { - if (series.AddOptions.SearchForMissingEpisodes) + if (addOptions.SearchForMissingEpisodes) { _commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id)); } - if (series.AddOptions.SearchForCutoffUnmetEpisodes) + if (addOptions.SearchForCutoffUnmetEpisodes) { _commandQueueManager.Push(new CutoffUnmetEpisodeSearchCommand(series.Id)); } From f8e81396d409362da359b3fde671ad826e5c68e3 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 12 Jun 2024 16:06:31 -0700 Subject: [PATCH 328/762] Fixed: Importing from IMDb list --- src/NzbDrone.Core/ImportLists/Imdb/ImdbListParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListParser.cs b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListParser.cs index 75e5e6bf6..8c20f268b 100644 --- a/src/NzbDrone.Core/ImportLists/Imdb/ImdbListParser.cs +++ b/src/NzbDrone.Core/ImportLists/Imdb/ImdbListParser.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.ImportLists.Imdb // Parse TSV response from IMDB export var rows = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 1).SelectList(i => new ImportListItemInfo { ImdbId = i[1] }); + series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 5).SelectList(i => new ImportListItemInfo { ImdbId = i[1], Title = i[5] }); return series; } From a30e9da7672a202cb9e9188cf106afc34a5d0361 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 14 Jun 2024 20:22:43 +0300 Subject: [PATCH 329/762] New: Ignore inaccessible folders when getting folders --- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 9c206709f..e0ac2bdc2 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); - return Directory.EnumerateDirectories(path); + return Directory.EnumerateDirectories(path, "*", new EnumerationOptions + { + AttributesToSkip = FileAttributes.System, + IgnoreInaccessible = true + }); } public IEnumerable<string> GetFiles(string path, bool recursive) From 6c39855ebe20ea383e813bdedff57d8f8b5c701d Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 14 Jun 2024 20:42:35 +0300 Subject: [PATCH 330/762] Fix UpdatePackageProviderFixture for v4 ignore-downstream --- .../UpdateTests/UpdatePackageProviderFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs index 661121299..a145c760a 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs @@ -48,11 +48,11 @@ namespace NzbDrone.Core.Test.UpdateTests { const string branch = "main"; UseRealHttp(); - var recent = Subject.GetRecentUpdates(branch, new Version(3, 0), null); + var recent = Subject.GetRecentUpdates(branch, new Version(4, 0), null); recent.Should().NotBeEmpty(); recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace()); - recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.3.")); + recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.4.")); recent.Should().OnlyContain(c => c.ReleaseDate.Year >= 2014); recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.New != null); recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.Fixed != null); From d2509798e937fc2eb8b1b5e672b64413d9bb069c Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:26:05 +0300 Subject: [PATCH 331/762] New: Display stats for delete multiple series modal --- .../Delete/DeleteSeriesModalContent.css | 5 ++ .../Delete/DeleteSeriesModalContent.css.d.ts | 1 + .../Series/Delete/DeleteSeriesModalContent.js | 24 ++++++---- .../Delete/DeleteSeriesModalContent.css | 11 +++++ .../Delete/DeleteSeriesModalContent.css.d.ts | 2 + .../Delete/DeleteSeriesModalContent.tsx | 47 ++++++++++++++++++- 6 files changed, 78 insertions(+), 12 deletions(-) diff --git a/frontend/src/Series/Delete/DeleteSeriesModalContent.css b/frontend/src/Series/Delete/DeleteSeriesModalContent.css index a6ac76133..d19745c2b 100644 --- a/frontend/src/Series/Delete/DeleteSeriesModalContent.css +++ b/frontend/src/Series/Delete/DeleteSeriesModalContent.css @@ -14,4 +14,9 @@ .deleteFilesMessage { margin-top: 20px; color: var(--dangerColor); + + .deleteCount { + margin-top: 20px; + color: var(--warningColor); + } } diff --git a/frontend/src/Series/Delete/DeleteSeriesModalContent.css.d.ts b/frontend/src/Series/Delete/DeleteSeriesModalContent.css.d.ts index f43438341..2b4dea066 100644 --- a/frontend/src/Series/Delete/DeleteSeriesModalContent.css.d.ts +++ b/frontend/src/Series/Delete/DeleteSeriesModalContent.css.d.ts @@ -1,6 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'deleteCount': string; 'deleteFilesMessage': string; 'folderPath': string; 'pathContainer': string; diff --git a/frontend/src/Series/Delete/DeleteSeriesModalContent.js b/frontend/src/Series/Delete/DeleteSeriesModalContent.js index 2af242499..233f355e1 100644 --- a/frontend/src/Series/Delete/DeleteSeriesModalContent.js +++ b/frontend/src/Series/Delete/DeleteSeriesModalContent.js @@ -50,15 +50,15 @@ class DeleteSeriesModalContent extends Component { const { title, path, - statistics, + statistics = {}, deleteOptions, onModalClose, onDeleteOptionChange } = this.props; const { - episodeFileCount, - sizeOnDisk + episodeFileCount = 0, + sizeOnDisk = 0 } = statistics; const deleteFiles = this.state.deleteFiles; @@ -108,16 +108,20 @@ class DeleteSeriesModalContent extends Component { </FormGroup> { - deleteFiles && + deleteFiles ? <div className={styles.deleteFilesMessage}> <div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div> - { - !!episodeFileCount && - <div>{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}</div> - } - </div> - } + { + episodeFileCount ? + <div className={styles.deleteCount}> + {translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })} + </div> : + null + } + </div> : + null + } </ModalBody> <ModalFooter> diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css index 02a0514be..09cf7e58b 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css @@ -10,4 +10,15 @@ .path { margin-left: 5px; color: var(--dangerColor); + font-weight: bold; +} + +.statistics { + margin-left: 5px; + color: var(--warningColor); +} + +.deleteFilesMessage { + margin-top: 20px; + color: var(--warningColor); } diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css.d.ts b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css.d.ts index bcc2e2492..ca4650422 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css.d.ts +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.css.d.ts @@ -1,9 +1,11 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'deleteFilesMessage': string; 'message': string; 'path': string; 'pathContainer': string; + 'statistics': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index 8904a24ce..ea35657e3 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -16,6 +16,7 @@ import Series from 'Series/Series'; import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import { CheckInputChanged } from 'typings/inputs'; +import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import styles from './DeleteSeriesModalContent.css'; @@ -85,6 +86,24 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { onModalClose, ]); + const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { + return series.reduce( + (acc, s) => { + const { statistics = { episodeFileCount: 0, sizeOnDisk: 0 } } = s; + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; + + acc.totalEpisodeFileCount += episodeFileCount; + acc.totalSizeOnDisk += sizeOnDisk; + + return acc; + }, + { + totalEpisodeFileCount: 0, + totalSizeOnDisk: 0, + } + ); + }, [series]); + return ( <ModalContent onModalClose={onModalClose}> <ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader> @@ -137,19 +156,43 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { <ul> {series.map((s) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = s.statistics; + return ( <li key={s.title}> <span>{s.title}</span> {deleteFiles && ( - <span className={styles.pathContainer}> - -<span className={styles.path}>{s.path}</span> + <span> + <span className={styles.pathContainer}> + -<span className={styles.path}>{s.path}</span> + </span> + + {!!episodeFileCount && ( + <span className={styles.statistics}> + ( + {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount, + size: formatBytes(sizeOnDisk), + })} + ) + </span> + )} </span> )} </li> ); })} </ul> + + {deleteFiles && !!totalEpisodeFileCount ? ( + <div className={styles.deleteFilesMessage}> + {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount: totalEpisodeFileCount, + size: formatBytes(totalSizeOnDisk), + })} + </div> + ) : null} </ModalBody> <ModalFooter> From e684c10432fa9107fa745f5d7e3b13a7605b2a60 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 25 Jun 2024 15:25:11 +0000 Subject: [PATCH 332/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com> Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/it.json | 29 ++++++++++++++++++- .../Localization/Core/pt_BR.json | 4 +-- src/NzbDrone.Core/Localization/Core/tr.json | 17 ++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index d8113dd53..9536dc8e4 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -250,5 +250,32 @@ "AutoRedownloadFailed": "Download fallito", "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", "Cutoff": "Taglio", - "AddListExclusion": "Aggiungi elenco esclusioni" + "AddListExclusion": "Aggiungi elenco esclusioni", + "DownloadClientValidationApiKeyRequired": "API Key Richiesta", + "Donate": "Dona", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Nessuna destinazione predefinita", + "ImportListSettings": "Impostazioni delle Liste", + "DownloadClientFreeboxSettingsAppId": "App ID", + "DownloadClientFreeboxSettingsAppToken": "App Token", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Questa cartella dovrà essere raggiungibile da XBMC", + "DownloadClientPneumaticSettingsNzbFolder": "Cartella Nzb", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Disattiva ordinamento per data", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Categoria non è supportata", + "DownloadClientValidationCategoryMissing": "Categoria non esiste", + "DownloadClientRTorrentSettingsUrlPath": "Percorso Url", + "Default": "Predefinito", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientQbittorrentValidationCategoryRecommended": "Categoria è raccomandata", + "Discord": "Discord", + "DownloadClientDownloadStationValidationFolderMissing": "Cartella non esiste", + "DownloadClientValidationAuthenticationFailure": "Autenticazione Fallita", + "DownloadClientDownloadStationValidationFolderMissingDetail": "La cartella '{downloadDir}' non esiste, deve essere creata manualmente all'interno della Cartella Condivisa '{sharedFolder}'.", + "DownloadClientSabnzbdValidationUnknownVersion": "Versione sconosciuta: {rawVersion}", + "DownloadClientValidationVerifySsl": "Verifica impostazioni SSL", + "ChangeCategory": "Cambia Categoria", + "DownloadClientPneumaticSettingsStrmFolder": "Cartella Strm", + "Destination": "Destinazione", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Cartella condivisa non esiste", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} non tenterà di importare i download completati senza una categoria.", + "DownloadClientValidationGroupMissing": "Gruppo non esistente" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index eafa190a4..f6da2c4df 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -169,7 +169,7 @@ "Calendar": "Calendário", "Connect": "Conectar", "CustomFormats": "Formatos personalizados", - "CutoffUnmet": "Limite não alcançado", + "CutoffUnmet": "Corte Não Alcançado", "DownloadClients": "Clientes de download", "Events": "Eventos", "General": "Geral", @@ -494,7 +494,7 @@ "CustomFormatsSettings": "Configurações de Formatos Personalizados", "CustomFormatsSettingsSummary": "Configurações e Formatos Personalizados", "DailyEpisodeFormat": "Formato do episódio diário", - "Cutoff": "Limite", + "Cutoff": "Corte", "Dash": "Traço", "Dates": "Datas", "Debug": "Depuração", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 3a595cec5..b3588e5bc 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -73,7 +73,7 @@ "AppUpdated": "{appName} Güncellendi", "ApplicationURL": "Uygulama URL'si", "ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin", - "ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır?", + "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", @@ -88,8 +88,8 @@ "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?", + "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", @@ -837,5 +837,14 @@ "UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz", "Wanted": "Arananlar", "Cutoff": "Kesinti", - "Required": "Gerekli" + "Required": "Gerekli", + "AirsTbaOn": "Daha sonra duyurulacak {networkLabel}'de", + "AllFiles": "Tüm dosyalar", + "AllSeriesAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir", + "Always": "Her zaman", + "AirsDateAtTimeOn": "{date} saat {time} {networkLabel}'de", + "AllResultsAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir", + "AllSeriesInRootFolderHaveBeenImported": "{path} içerisindeki tüm diziler içeri aktarıldı", + "AlternateTitles": "Alternatif Başlıklar", + "AnEpisodeIsDownloading": "Bir bölüm indiriliyor" } From 63bed3e670edbbc58cb165d1b856e9070ffc98a4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 16 Jun 2024 22:52:13 -0700 Subject: [PATCH 333/762] New: Parse anime seasons with trailing number in title Closes #6883 --- src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 4d81e1e74..8d3a792b3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -33,6 +33,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)] [TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)] [TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)] + [TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)] + [TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)] public void should_parse_full_season_release(string postTitle, string title, int season) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9fbe43b7a..5558fa698 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -126,6 +126,10 @@ namespace NzbDrone.Core.Parser 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 with trailing number S## (Full season) + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))).+?(?:$|\.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)", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 6d5ff9c4d6993d16848980aea499a45b1b51d95c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 25 Jun 2024 15:51:20 -0700 Subject: [PATCH 334/762] New: Improve UI status when downloads cannot be imported automatically Closes #6873 --- frontend/src/Activity/Queue/QueueStatus.js | 5 +++++ .../ImportFixture.cs | 2 +- .../Download/CompletedDownloadService.cs | 16 +++++++++------- .../Download/TrackedDownloads/TrackedDownload.cs | 1 + src/NzbDrone.Core/Localization/Core/en.json | 1 + 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/src/Activity/Queue/QueueStatus.js b/frontend/src/Activity/Queue/QueueStatus.js index c6e8cf5dd..f7cab31ca 100644 --- a/frontend/src/Activity/Queue/QueueStatus.js +++ b/frontend/src/Activity/Queue/QueueStatus.js @@ -70,6 +70,11 @@ function QueueStatus(props) { iconName = icons.DOWNLOADED; title = translate('Downloaded'); + if (trackedDownloadState === 'importBlocked') { + title += ` - ${translate('UnableToImportAutomatically')}`; + iconKind = kinds.WARNING; + } + if (trackedDownloadState === 'importPending') { title += ` - ${translate('WaitingToImport')}`; iconKind = kinds.PURPLE; diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs index 3c6797324..ddb9fd8c6 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs @@ -366,7 +366,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests Mocker.GetMock<IEventAggregator>() .Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never()); - _trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending); + _trackedDownload.State.Should().Be(TrackedDownloadState.ImportBlocked); } private void AssertImported() diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 5f4f94938..009cd7f65 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -64,8 +64,8 @@ namespace NzbDrone.Core.Download SetImportItem(trackedDownload); - // Only process tracked downloads that are still downloading - if (trackedDownload.State != TrackedDownloadState.Downloading) + // Only process tracked downloads that are still downloading or have been blocked for importing due to an issue with matching + if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked) { return; } @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Download if (series == null) { trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes."); - SendManualInteractionRequiredNotification(trackedDownload); + SetStateToImportBlocked(trackedDownload); return; } @@ -108,7 +108,7 @@ namespace NzbDrone.Core.Download if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch) { trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details."); - SendManualInteractionRequiredNotification(trackedDownload); + SetStateToImportBlocked(trackedDownload); return; } @@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download if (trackedDownload.RemoteEpisode == null) { trackedDownload.Warn("Unable to parse download, automatic import is not possible."); - SendManualInteractionRequiredNotification(trackedDownload); + SetStateToImportBlocked(trackedDownload); return; } @@ -187,7 +187,7 @@ namespace NzbDrone.Core.Download if (statusMessages.Any()) { trackedDownload.Warn(statusMessages.ToArray()); - SendManualInteractionRequiredNotification(trackedDownload); + SetStateToImportBlocked(trackedDownload); } } @@ -254,8 +254,10 @@ namespace NzbDrone.Core.Download return false; } - private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload) + private void SetStateToImportBlocked(TrackedDownload trackedDownload) { + trackedDownload.State = TrackedDownloadState.ImportBlocked; + if (!trackedDownload.HasNotifiedManualInteractionRequired) { var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList(); diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 05c25db81..0a982e7ff 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public enum TrackedDownloadState { Downloading, + ImportBlocked, ImportPending, Importing, Imported, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 73a556b40..6ddd95aee 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1992,6 +1992,7 @@ "Umask770Description": "{octal} - Owner & Group write", "Umask775Description": "{octal} - Owner & Group write, Other read", "Umask777Description": "{octal} - Everyone write", + "UnableToImportAutomatically": "Unable to Import Automatically", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", "UnableToUpdateSonarrDirectly": "Unable to update {appName} directly,", From fb060730c7d52cd342484dc68595698a9430df7b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:16:53 +0300 Subject: [PATCH 335/762] Fixed: Exclude invalid releases from Newznab and Torznab parsers --- .../Indexers/Newznab/NewznabRssParser.cs | 9 +++-- src/NzbDrone.Core/Indexers/RssParser.cs | 38 +++++++++---------- .../Indexers/Torznab/TorznabRssParser.cs | 9 +++-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index ea1767569..2444d901c 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -68,16 +68,17 @@ namespace NzbDrone.Core.Indexers.Newznab protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases) { var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any()) { _logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); + + return false; } - else - { - _logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); - } + + _logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); } return true; diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index ff61f2607..4f4f5ce43 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -262,26 +262,26 @@ namespace NzbDrone.Core.Indexers protected virtual RssEnclosure[] GetEnclosures(XElement item) { var enclosures = item.Elements("enclosure") - .Select(v => - { - try - { - return new RssEnclosure - { - Url = v.Attribute("url")?.Value, - Type = v.Attribute("type")?.Value, - Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0 - }; - } - catch (Exception e) - { - _logger.Warn(e, "Failed to get enclosure for: {0}", item.Title()); - } + .Select(v => + { + try + { + return new RssEnclosure + { + Url = v.Attribute("url")?.Value, + Type = v.Attribute("type")?.Value, + Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0 + }; + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to get enclosure for: {0}", item.Title()); + } - return null; - }) - .Where(v => v != null) - .ToArray(); + return null; + }) + .Where(v => v != null) + .ToArray(); return enclosures; } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 94a44828b..3bcd87d76 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -59,16 +59,17 @@ namespace NzbDrone.Core.Indexers.Torznab protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases) { var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any()) { _logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Newznab indexer?", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]); + + return false; } - else - { - _logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]); - } + + _logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]); } return true; From 4c622fd41289cd293a68a6a9f6b8da2a086edecb Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 19 Jun 2024 15:50:36 -0700 Subject: [PATCH 336/762] New: Ability to select Plex Media Server from plex.tv Closes #6887 --- .../Components/Form/EnhancedSelectInput.js | 34 +++++---- .../Form/EnhancedSelectInputConnector.js | 9 ++- .../EditNotificationModalContentConnector.js | 10 +-- .../createSetProviderFieldValuesReducer.js | 25 +++++++ .../Store/Actions/Settings/notifications.js | 10 +++ .../Annotations/FieldDefinitionAttribute.cs | 21 +++++- .../NewznabCategoryFieldOptionsConverter.cs | 8 +- src/NzbDrone.Core/Localization/Core/en.json | 2 + .../Notifications/Plex/PlexTv/PlexTvProxy.cs | 29 ++++++++ .../Plex/PlexTv/PlexTvResource.cs | 32 ++++++++ .../Plex/PlexTv/PlexTvService.cs | 12 +++ .../Notifications/Plex/Server/PlexServer.cs | 74 +++++++++++++++++++ .../Plex/Server/PlexServerSettings.cs | 24 +++--- 13 files changed, 252 insertions(+), 38 deletions(-) create mode 100644 frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer.js create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvResource.cs diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index cc4215025..38b5e6ab5 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component { this.setState({ isOpen: !this.state.isOpen }); }; - onSelect = (value) => { - if (Array.isArray(this.props.value)) { - let newValue = null; - const index = this.props.value.indexOf(value); + onSelect = (newValue) => { + const { name, value, values, onChange } = this.props; + const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties; + + if (Array.isArray(value)) { + let arrayValue = null; + const index = value.indexOf(newValue); + if (index === -1) { - newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); + arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); } else { - newValue = [...this.props.value]; - newValue.splice(index, 1); + arrayValue = [...value]; + arrayValue.splice(index, 1); } - this.props.onChange({ - name: this.props.name, - value: newValue + onChange({ + name, + value: arrayValue, + additionalProperties }); } else { this.setState({ isOpen: false }); - this.props.onChange({ - name: this.props.name, - value + onChange({ + name, + value: newValue, + additionalProperties }); } }; @@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component { values.map((v, index) => { const hasParent = v.parentKey !== undefined; const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && value.includes(v.parentKey); + const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey); return ( <OptionComponent key={v.key} diff --git a/frontend/src/Components/Form/EnhancedSelectInputConnector.js b/frontend/src/Components/Form/EnhancedSelectInputConnector.js index f2af4a585..cfbe9484f 100644 --- a/frontend/src/Components/Form/EnhancedSelectInputConnector.js +++ b/frontend/src/Components/Form/EnhancedSelectInputConnector.js @@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput'; const importantFieldNames = [ 'baseUrl', 'apiPath', - 'apiKey' + 'apiKey', + 'authToken' ]; function getProviderDataKey(providerData) { @@ -34,7 +35,9 @@ function getSelectOptions(items) { key: option.value, value: option.name, hint: option.hint, - parentKey: option.parentValue + parentKey: option.parentValue, + isDisabled: option.isDisabled, + additionalProperties: option.additionalProperties }; }); } @@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = { provider: PropTypes.string.isRequired, providerData: PropTypes.object.isRequired, name: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired, selectOptionsProviderAction: PropTypes.string, onChange: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js index 658d72da8..ce7a0c1ca 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { saveNotification, - setNotificationFieldValue, + setNotificationFieldValues, setNotificationValue, testNotification, toggleAdvancedSettings @@ -27,7 +27,7 @@ function createMapStateToProps() { const mapDispatchToProps = { setNotificationValue, - setNotificationFieldValue, + setNotificationFieldValues, saveNotification, testNotification, toggleAdvancedSettings @@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component { this.props.setNotificationValue({ name, value }); }; - onFieldChange = ({ name, value }) => { - this.props.setNotificationFieldValue({ name, value }); + onFieldChange = ({ name, value, additionalProperties = {} }) => { + this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } }); }; onSavePress = () => { @@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = { saveError: PropTypes.object, item: PropTypes.object.isRequired, setNotificationValue: PropTypes.func.isRequired, - setNotificationFieldValue: PropTypes.func.isRequired, + setNotificationFieldValues: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired, toggleAdvancedSettings: PropTypes.func.isRequired, diff --git a/frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer.js new file mode 100644 index 000000000..ee9afb597 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer.js @@ -0,0 +1,25 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetProviderFieldValuesReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const { properties } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = Object.assign({}, newState.pendingChanges); + const fields = Object.assign({}, newState.pendingChanges.fields || {}); + + Object.keys(properties).forEach((name) => { + fields[name] = properties[name]; + }); + + newState.pendingChanges.fields = fields; + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetProviderFieldValuesReducer; diff --git a/frontend/src/Store/Actions/Settings/notifications.js b/frontend/src/Store/Actions/Settings/notifications.js index 5393ecbd0..c9349df60 100644 --- a/frontend/src/Store/Actions/Settings/notifications.js +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; import selectProviderSchema from 'Utilities/State/selectProviderSchema'; @@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema'; export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue'; export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue'; +export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues'; export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification'; export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification'; export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification'; @@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL }; }); +export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => { + return { + section, + ...payload + }; +}); + // // Details @@ -99,6 +108,7 @@ export default { reducers: { [SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section), [SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section), + [SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section), [SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => { return selectProviderSchema(state, section, payload, (selectedSchema) => { diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 398376117..22088b01f 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace NzbDrone.Core.Annotations @@ -59,13 +60,27 @@ namespace NzbDrone.Core.Annotations public string Value { get; set; } } - public class FieldSelectOption + public class FieldSelectOption<T> + where T : struct { - public int Value { get; set; } + public T Value { get; set; } public string Name { get; set; } public int Order { get; set; } public string Hint { get; set; } - public int? ParentValue { get; set; } + public T? ParentValue { get; set; } + public bool? IsDisabled { get; set; } + public Dictionary<string, object> AdditionalProperties { get; set; } + } + + public class FieldSelectStringOption + { + public string Value { get; set; } + public string Name { get; set; } + public int Order { get; set; } + public string Hint { get; set; } + public string ParentValue { get; set; } + public bool? IsDisabled { get; set; } + public Dictionary<string, object> AdditionalProperties { get; set; } } public enum FieldType diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCategoryFieldOptionsConverter.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCategoryFieldOptionsConverter.cs index 2871266fe..7a278995a 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCategoryFieldOptionsConverter.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCategoryFieldOptionsConverter.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Indexers.Newznab { public static class NewznabCategoryFieldOptionsConverter { - public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories) + public static List<FieldSelectOption<int>> GetFieldSelectOptions(List<NewznabCategory> categories) { // Categories not relevant for Sonarr var ignoreCategories = new[] { 1000, 3000, 4000, 6000, 7000 }; @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Newznab // And maybe relevant for specific users var unimportantCategories = new[] { 0, 2000 }; - var result = new List<FieldSelectOption>(); + var result = new List<FieldSelectOption<int>>(); if (categories == null) { @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Newznab foreach (var category in categories.Where(cat => !ignoreCategories.Contains(cat.Id)).OrderBy(cat => unimportantCategories.Contains(cat.Id)).ThenBy(cat => cat.Id)) { - result.Add(new FieldSelectOption + result.Add(new FieldSelectOption<int> { Value = category.Id, Name = category.Name, @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var subcat in category.Subcategories.OrderBy(cat => cat.Id)) { - result.Add(new FieldSelectOption + result.Add(new FieldSelectOption<int> { Value = subcat.Id, Name = subcat.Name, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 6ddd95aee..b60cecb2e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1361,6 +1361,8 @@ "NotificationsNtfyValidationAuthorizationRequired": "Authorization is required", "NotificationsPlexSettingsAuthToken": "Auth Token", "NotificationsPlexSettingsAuthenticateWithPlexTv": "Authenticate with Plex.tv", + "NotificationsPlexSettingsServer": "Server", + "NotificationsPlexSettingsServerHelpText": "Select server from plex.tv account after authenticating", "NotificationsPlexValidationNoTvLibraryFound": "At least one TV library is required", "NotificationsPushBulletSettingSenderId": "Sender ID", "NotificationsPushBulletSettingSenderIdHelpText": "The device ID to send notifications from, use device_iden in the device's URL on pushbullet.com (leave blank to send from yourself)", diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs index 8073f2485..52b1d9bf3 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using NLog; using NzbDrone.Common.EnvironmentInfo; @@ -12,6 +13,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv { string GetAuthToken(string clientIdentifier, int pinId); bool Ping(string clientIdentifier, string authToken); + List<PlexTvResource> GetResources(string clientIdentifier, string authToken); } public class PlexTvProxy : IPlexTvProxy @@ -62,6 +64,33 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv return false; } + public List<PlexTvResource> GetResources(string clientIdentifier, string authToken) + { + try + { + // Allows us to tell plex.tv that we're still active and tokens should not be expired. + + var request = BuildRequest(clientIdentifier); + + request.ResourceUrl = "/api/v2/resources"; + request.AddQueryParam("includeHttps", 1); + request.AddQueryParam("clientID", clientIdentifier); + request.AddQueryParam("X-Plex-Token", authToken); + + if (Json.TryDeserialize<List<PlexTvResource>>(ProcessRequest(request), out var response)) + { + return response; + } + } + catch (Exception e) + { + // Catch all exceptions and log at trace, this information could be interesting in debugging, but expired tokens will be handled elsewhere. + _logger.Trace(e, "Unable to ping plex.tv"); + } + + return new List<PlexTvResource>(); + } + private HttpRequestBuilder BuildRequest(string clientIdentifier) { var requestBuilder = new HttpRequestBuilder("https://plex.tv") diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvResource.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvResource.cs new file mode 100644 index 000000000..36e8e1314 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvResource.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public class PlexTvResource + { + public string Name { get; set; } + public bool Owned { get; set; } + + public List<PlexTvResourceConnection> Connections { get; set; } + + [JsonProperty("provides")] + public string ProvidesRaw { get; set; } + + [JsonIgnore] + public List<string> Provides => ProvidesRaw.Split(",").ToList(); + } + + public class PlexTvResourceConnection + { + public string Uri { get; set; } + public string Protocol { get; set; } + public string Address { get; set; } + public int Port { get; set; } + public bool Local { get; set; } + public string Host => Uri.IsNullOrWhiteSpace() ? Address : new Uri(Uri).Host; + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs index 85e24ce99..7db58ffda 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using NzbDrone.Common.Cache; @@ -14,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode); string GetAuthToken(int pinId); void Ping(string authToken); + List<PlexTvResource> GetServers(string authToken); HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset); } @@ -93,6 +95,16 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv _cache.Get(authToken, () => _proxy.Ping(_configService.PlexClientIdentifier, authToken), TimeSpan.FromHours(24)); } + public List<PlexTvResource> GetServers(string authToken) + { + Ping(authToken); + + var clientIdentifier = _configService.PlexClientIdentifier; + var resources = _proxy.GetResources(clientIdentifier, authToken); + + return resources.Where(r => r.Owned && r.Provides.Contains("server")).ToList(); + } + public HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset) { Ping(authToken); diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs index 9cfd03b58..46fb118c1 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -5,6 +5,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications.Plex.PlexTv; @@ -193,6 +194,79 @@ namespace NzbDrone.Core.Notifications.Plex.Server }; } + if (action == "servers") + { + Settings.Validate().Filter("AuthToken").ThrowOnError(); + + if (Settings.AuthToken.IsNullOrWhiteSpace()) + { + return new { }; + } + + var servers = _plexTvService.GetServers(Settings.AuthToken); + var options = servers.SelectMany(s => + { + var result = new List<FieldSelectStringOption>(); + + // result.Add(new FieldSelectStringOption + // { + // Value = s.Name, + // Name = s.Name, + // IsDisabled = true + // }); + + s.Connections.ForEach(c => + { + var isSecure = c.Protocol == "https"; + var additionalProperties = new Dictionary<string, object>(); + var hints = new List<string>(); + + additionalProperties.Add("host", c.Host); + additionalProperties.Add("port", c.Port); + additionalProperties.Add("useSsl", isSecure); + hints.Add(c.Local ? "Local" : "Remote"); + + if (isSecure) + { + hints.Add("Secure"); + } + + result.Add(new FieldSelectStringOption + { + Value = c.Uri, + Name = $"{s.Name} ({c.Host})", + Hint = string.Join(", ", hints), + AdditionalProperties = additionalProperties + }); + + if (isSecure) + { + var uri = $"http://{c.Address}:{c.Port}"; + var insecureAdditionalProperties = new Dictionary<string, object>(); + + insecureAdditionalProperties.Add("host", c.Address); + insecureAdditionalProperties.Add("port", c.Port); + insecureAdditionalProperties.Add("useSsl", false); + + result.Add(new FieldSelectStringOption + { + Value = uri, + Name = $"{s.Name} ({c.Address})", + Hint = c.Local ? "Local" : "Remote", + AdditionalProperties = insecureAdditionalProperties + }); + } + }); + + return result; + }); + + return new + { + options + }; + } + return new { }; } } diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index 50ba7f757..721d80dce 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -22,40 +23,45 @@ namespace NzbDrone.Core.Notifications.Plex.Server public PlexServerSettings() { + Host = ""; Port = 32400; UpdateLibrary = true; SignIn = "startOAuth"; } - [FieldDefinition(0, Label = "Host")] + [JsonIgnore] + [FieldDefinition(0, Label = "NotificationsPlexSettingsServer", Type = FieldType.Select, SelectOptionsProviderAction = "servers", HelpText = "NotificationsPlexSettingsServerHelpText")] + public string Server { get; set; } + + [FieldDefinition(1, Label = "Host")] public string Host { get; set; } - [FieldDefinition(1, Label = "Port")] + [FieldDefinition(2, Label = "Port")] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")] + [FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")] [FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Plex")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")] + [FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")] [FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Plex")] [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/plex")] public string UrlBase { get; set; } - [FieldDefinition(4, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)] + [FieldDefinition(5, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)] public string AuthToken { get; set; } - [FieldDefinition(5, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)] + [FieldDefinition(6, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)] public string SignIn { get; set; } - [FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Plex")] public string MapFrom { get; set; } - [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")] + [FieldDefinition(9, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Plex")] public string MapTo { get; set; } From a0d29331341320268552660658b949179c963793 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 25 Jun 2024 15:52:12 -0700 Subject: [PATCH 337/762] New: Ignore Deluge torrents without a title Closes #6885 --- .../Download/Clients/Deluge/Deluge.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 3856e7a70..f2f8ff7d6 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -124,14 +124,23 @@ namespace NzbDrone.Core.Download.Clients.Deluge } var items = new List<DownloadClientItem>(); + var ignoredCount = 0; foreach (var torrent in torrents) { - if (torrent.Hash == null) + // Silently ignore torrents with no hash + if (torrent.Hash.IsNullOrWhiteSpace()) { continue; } + // Ignore torrents without a name, but track to log a single warning for all invalid torrents. + if (torrent.Name.IsNullOrWhiteSpace()) + { + ignoredCount++; + continue; + } + var item = new DownloadClientItem(); item.DownloadId = torrent.Hash.ToUpper(); item.Title = torrent.Name; @@ -189,6 +198,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge items.Add(item); } + if (ignoredCount > 0) + { + _logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents"); + } + return items; } From 45fe5859440584eaa1eee10c209497b719e6a14c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 21 Jun 2024 17:10:10 -0700 Subject: [PATCH 338/762] Fixed: Prevent errors parsing releases in unexpected formats --- .../ParserTests/CrapParserFixture.cs | 7 +++++ src/NzbDrone.Core/Parser/Parser.cs | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index 9d34e2283..7d55563e1 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -39,6 +39,13 @@ namespace NzbDrone.Core.Test.ParserTests ExceptionVerification.IgnoreWarns(); } + [TestCase("علم نف) أ.دعادل الأبيض ٢٠٢٤ ٣ ٣")] + [TestCase("ror-240618_1007-1022-")] + public void should_parse_unknown_formats_without_error(string title) + { + Parser.Parser.ParseTitle(title).Should().NotBeNull(); + } + [Test] public void should_not_parse_md5() { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 5558fa698..89c3d8270 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -745,7 +745,7 @@ namespace NzbDrone.Core.Parser Logger.Trace(regex); try { - var result = ParseMatchCollection(match, releaseTitle); + var result = ParseMatchCollection(match, simpleTitle); if (result != null) { @@ -1209,6 +1209,7 @@ namespace NzbDrone.Core.Parser } } + // TODO: This needs to check the modified title if (lastSeasonEpisodeStringIndex != releaseTitle.Length) { result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex); @@ -1289,7 +1290,7 @@ namespace NzbDrone.Core.Parser private static int ParseNumber(string value) { - var normalized = value.Normalize(NormalizationForm.FormKC); + var normalized = ConvertToNumerals(value.Normalize(NormalizationForm.FormKC)); if (int.TryParse(normalized, out var number)) { @@ -1308,7 +1309,7 @@ namespace NzbDrone.Core.Parser private static decimal ParseDecimal(string value) { - var normalized = value.Normalize(NormalizationForm.FormKC); + var normalized = ConvertToNumerals(value.Normalize(NormalizationForm.FormKC)); if (decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) { @@ -1317,5 +1318,24 @@ namespace NzbDrone.Core.Parser throw new FormatException(string.Format("{0} isn't a number", value)); } + + private static string ConvertToNumerals(string input) + { + var result = new StringBuilder(input.Length); + + foreach (var c in input.ToCharArray()) + { + if (char.IsNumber(c)) + { + result.Append(char.GetNumericValue(c)); + } + else + { + result.Append(c); + } + } + + return result.ToString(); + } } } From ea4fe392a0cc4774bb28c969fb3903db264c8d6c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 21 Jun 2024 17:10:58 -0700 Subject: [PATCH 339/762] New: Remove websites in parentheses before parsing --- src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs index ebc0e70bd..08d9bb822 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[www.test-hyphen.ca] - Series (2011) S01", "Series (2011)")] [TestCase("test123.ca - Series Time S02 720p HDTV x264 CRON", "Series Time")] [TestCase("[www.test-hyphen123.co.za] - Series Title S01E01", "Series Title")] + [TestCase("(seriesawake.com) Series Super - 57 [720p] [English Subbed]", "Series Super")] public void should_not_parse_url_in_name(string postTitle, string title) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 89c3d8270..f164e36ff 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -514,7 +514,7 @@ namespace NzbDrone.Core.Parser // Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt - private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*\]|[ -]{2,})[ -]*", + private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\))|[ -]{2,})[ -]*", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); From bce848facf8aeaeac6a1d59c92941d00589034a4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 26 Jun 2024 08:58:22 -0700 Subject: [PATCH 340/762] Fixed: Reprocessing items that were previously blocked during importing --- src/NzbDrone.Core/Download/FailedDownloadService.cs | 4 ++-- .../Download/TrackedDownloads/DownloadMonitoringService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 3e540a256..d392f0ea4 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -73,8 +73,8 @@ namespace NzbDrone.Core.Download public void Check(TrackedDownload trackedDownload) { - // Only process tracked downloads that are still downloading - if (trackedDownload.State != TrackedDownloadState.Downloading) + // Only process tracked downloads that are still downloading or import is blocked (if they fail after attempting to be processed) + if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked) { return; } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index 69523ab36..e9d9670c5 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem); - if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading) + if (trackedDownload is { State: TrackedDownloadState.Downloading or TrackedDownloadState.ImportBlocked }) { _failedDownloadService.Check(trackedDownload); _completedDownloadService.Check(trackedDownload); From 6de536a7adcb604ec057d37873585fa665567437 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 25 Jun 2024 15:53:17 -0700 Subject: [PATCH 341/762] Fixed: Limit Queue maximum page size to 200 Closes #6899 --- frontend/src/Activity/Queue/Queue.js | 1 + .../src/Components/Table/TableOptions/TableOptionsModal.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 30f5260cb..64bfbc085 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -217,6 +217,7 @@ class Queue extends Component { > <TableOptionsModalWrapper columns={columns} + maxPageSize={200} {...otherProps} optionsComponent={QueueOptionsConnector} > diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js index dfdd0ec88..ab5048717 100644 --- a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js @@ -49,11 +49,12 @@ class TableOptionsModal extends Component { onPageSizeChange = ({ value }) => { let pageSizeError = null; + const maxPageSize = this.props.maxPageSize ?? 250; if (value < 5) { pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' }); - } else if (value > 250) { - pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' }); + } else if (value > maxPageSize) { + pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` }); } else { this.props.onTableOptionChange({ pageSize: value }); } @@ -248,6 +249,7 @@ TableOptionsModal.propTypes = { isOpen: PropTypes.bool.isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, pageSize: PropTypes.number, + maxPageSize: PropTypes.number, canModifyColumns: PropTypes.bool.isRequired, optionsComponent: PropTypes.elementType, onTableOptionChange: PropTypes.func.isRequired, From 29480d9544bcb67f60e5df62c8c525014f8dc0ce Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 27 Jun 2024 16:40:55 -0700 Subject: [PATCH 342/762] Fixed: Don't use cleaned up release title for release title --- src/NzbDrone.Core/Parser/Parser.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index f164e36ff..cd56bbf55 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -745,7 +745,7 @@ namespace NzbDrone.Core.Parser Logger.Trace(regex); try { - var result = ParseMatchCollection(match, simpleTitle); + var result = ParseMatchCollection(match, releaseTitle); if (result != null) { @@ -1209,8 +1209,7 @@ namespace NzbDrone.Core.Parser } } - // TODO: This needs to check the modified title - if (lastSeasonEpisodeStringIndex != releaseTitle.Length) + if (lastSeasonEpisodeStringIndex < releaseTitle.Length) { result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex); } From 143ccb1e2a18c63cf246368a717c8a9e7732ed8f Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 27 Jun 2024 17:08:29 -0700 Subject: [PATCH 343/762] Remove seriesTitle from EpisodeResource Closes #6841 --- src/Sonarr.Api.V3/Episodes/EpisodeResource.cs | 2 -- src/Sonarr.Api.V3/openapi.json | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs index 9a0ff01fa..b073a0670 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs @@ -34,7 +34,6 @@ namespace Sonarr.Api.V3.Episodes public bool UnverifiedSceneNumbering { get; set; } public DateTime? EndTime { get; set; } public DateTime? GrabDate { get; set; } - public string SeriesTitle { get; set; } public SeriesResource Series { get; set; } public List<MediaCover> Images { get; set; } @@ -79,7 +78,6 @@ namespace Sonarr.Api.V3.Episodes SceneEpisodeNumber = model.SceneEpisodeNumber, SceneSeasonNumber = model.SceneSeasonNumber, UnverifiedSceneNumbering = model.UnverifiedSceneNumbering, - SeriesTitle = model.SeriesTitle, // Series = model.Series.MapToResource(), }; diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 1e390e679..a5c4dd475 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8483,10 +8483,6 @@ "format": "date-time", "nullable": true }, - "seriesTitle": { - "type": "string", - "nullable": true - }, "series": { "$ref": "#/components/schemas/SeriesResource" }, From 8099ba10afded446779290de29b1baaf0be932c3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:47:00 +0300 Subject: [PATCH 344/762] Fixed: Already imported downloads appearing in Queue briefly --- src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs | 1 + src/NzbDrone.Core/Queue/QueueService.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs index 6d1fbe328..0d205e2c9 100644 --- a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs +++ b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Test.QueueTests _trackedDownloads = Builder<TrackedDownload>.CreateListOfSize(1) .All() + .With(v => v.IsTrackable = true) .With(v => v.DownloadItem = downloadItem) .With(v => v.RemoteEpisode = remoteEpisode) .Build() diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 3d7078223..8bb11a13c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Queue public class QueueService : IQueueService, IHandle<TrackedDownloadRefreshedEvent> { private readonly IEventAggregator _eventAggregator; - private static List<Queue> _queue = new List<Queue>(); + private static List<Queue> _queue = new (); public QueueService(IEventAggregator eventAggregator) { @@ -96,8 +96,11 @@ namespace NzbDrone.Core.Queue public void Handle(TrackedDownloadRefreshedEvent message) { - _queue = message.TrackedDownloads.OrderBy(c => c.DownloadItem.RemainingTime).SelectMany(MapQueue) - .ToList(); + _queue = message.TrackedDownloads + .Where(t => t.IsTrackable) + .OrderBy(c => c.DownloadItem.RemainingTime) + .SelectMany(MapQueue) + .ToList(); _eventAggregator.PublishEvent(new QueueUpdatedEvent()); } From d5dff8e8d6301b661a713702e1c476705423fc4f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:49:41 +0300 Subject: [PATCH 345/762] Fixed: Trimming disabled logs database Closes #6918 --- .../Housekeeping/Housekeepers/TrimLogDatabase.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs index a719652af..5763a563e 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs @@ -1,18 +1,26 @@ -using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Instrumentation; namespace NzbDrone.Core.Housekeeping.Housekeepers { public class TrimLogDatabase : IHousekeepingTask { private readonly ILogRepository _logRepo; + private readonly IConfigFileProvider _configFileProvider; - public TrimLogDatabase(ILogRepository logRepo) + public TrimLogDatabase(ILogRepository logRepo, IConfigFileProvider configFileProvider) { _logRepo = logRepo; + _configFileProvider = configFileProvider; } public void Clean() { + if (!_configFileProvider.LogDbEnabled) + { + return; + } + _logRepo.Trim(); } } From fd7f0ea9731e3e3739ab8031d974268e6399b410 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 30 Jun 2024 17:47:06 +0000 Subject: [PATCH 346/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Kshitij Burman <kburman6@gmail.com> Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: damienmillet <contact@damien-millet.dev> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 5 +- src/NzbDrone.Core/Localization/Core/fr.json | 12 +- src/NzbDrone.Core/Localization/Core/hi.json | 5 +- src/NzbDrone.Core/Localization/Core/it.json | 154 +++++++++++++++++- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/tr.json | 3 +- 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 37ec9c7a7..c7a8a340a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2078,5 +2078,8 @@ "TomorrowAt": "Mañana a las {time}", "YesterdayAt": "Ayer a las {time}", "TodayAt": "Hoy a las {time}", - "DayOfWeekAt": "{day} a las {time}" + "DayOfWeekAt": "{day} a las {time}", + "UnableToImportAutomatically": "No se pudo importar automáticamente", + "NotificationsPlexSettingsServer": "Servidor", + "NotificationsPlexSettingsServerHelpText": "Selecciona el servidor desde una cuenta de plex.tv después de autenticarse" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c5ff71c9e..19f040494 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1779,7 +1779,7 @@ "NotificationsPushoverSettingsDevicesHelpText": "Liste des noms des appareils (laisser vide pour envoyer à tous les appareils)", "NotificationsPushoverSettingsDevices": "Appareils", "NotificationsPushcutSettingsTimeSensitiveHelpText": "Activer pour marquer la notification comme « Time Sensitive »", - "NotificationsPushcutSettingsTimeSensitive": "Time Sensitive", + "NotificationsPushcutSettingsTimeSensitive": "Sensible au temps", "NotificationsPushcutSettingsNotificationNameHelpText": "Nom de la notification de l'onglet Notifications de l'app Pushcut", "NotificationsPushcutSettingsNotificationName": "Nom de la notification", "NotificationsPushcutSettingsApiKeyHelpText": "Les clés API peuvent être gérées dans la vue Compte de l'app Pushcut", @@ -2071,5 +2071,13 @@ "NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre", "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications", "IndexerSettingsMultiLanguageRelease": "Multilingue", - "IndexerSettingsMultiLanguageReleaseHelpText": "Quelles langues sont normalement présentes dans une version multiple de l'indexeur ?" + "IndexerSettingsMultiLanguageReleaseHelpText": "Quelles langues sont normalement présentes dans une version multiple de l'indexeur ?", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent signale des fichiers manquants", + "BlocklistFilterHasNoItems": "La liste de blocage sélectionnée ne contient aucun élément", + "HasUnmonitoredSeason": "A une saison non surveillée", + "YesterdayAt": "Hier à {time}", + "UnableToImportAutomatically": "Impossible d'importer automatiquement", + "DayOfWeekAt": "{day} à {time}", + "TomorrowAt": "Demain à {time}", + "TodayAt": "Aujourd'hui à {time}" } diff --git a/src/NzbDrone.Core/Localization/Core/hi.json b/src/NzbDrone.Core/Localization/Core/hi.json index 0967ef424..501c14d05 100644 --- a/src/NzbDrone.Core/Localization/Core/hi.json +++ b/src/NzbDrone.Core/Localization/Core/hi.json @@ -1 +1,4 @@ -{} +{ + "About": "के बारे में", + "Absolute": "पूर्ण" +} diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 9536dc8e4..a51ac1461 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -20,7 +20,7 @@ "ApplyChanges": "Applica Cambiamenti", "ApplyTags": "Applica Etichette", "BackupNow": "Esegui backup ora", - "Backups": "Backups", + "Backups": "Backup", "Blocklist": "Lista dei Blocchi", "Activity": "Attività", "About": "Info", @@ -64,7 +64,7 @@ "BeforeUpdate": "Prima dell'aggiornamento", "CalendarFeed": "Feed calendario {appName}", "CalendarOptions": "Opzioni del Calendario", - "ChooseImportMode": "Selezionare Metodo di Importazione", + "ChooseImportMode": "Seleziona Metodo di Importazione", "CollapseMultipleEpisodes": "Collassa Episodi Multipli", "Conditions": "Condizioni", "Continuing": "In Corso", @@ -250,7 +250,7 @@ "AutoRedownloadFailed": "Download fallito", "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", "Cutoff": "Taglio", - "AddListExclusion": "Aggiungi elenco esclusioni", + "AddListExclusion": "Aggiungi Lista esclusioni", "DownloadClientValidationApiKeyRequired": "API Key Richiesta", "Donate": "Dona", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nessuna destinazione predefinita", @@ -277,5 +277,151 @@ "Destination": "Destinazione", "DownloadClientDownloadStationValidationSharedFolderMissing": "Cartella condivisa non esiste", "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} non tenterà di importare i download completati senza una categoria.", - "DownloadClientValidationGroupMissing": "Gruppo non esistente" + "DownloadClientValidationGroupMissing": "Gruppo non esistente", + "DownloadWarning": "Avviso di download: {warningMessage}", + "IndexerSettingsAdditionalParameters": "Parametri Addizionali", + "IndexerSettingsCookie": "Cookie", + "BlackholeWatchFolderHelpText": "Cartella da cui {appName} dovrebbe importare i download completati", + "NotificationsEmailSettingsServer": "Server", + "NotificationsNtfySettingsPasswordHelpText": "Password opzionale", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Autentica con Plex.tv", + "NotificationsPushcutSettingsNotificationName": "Nome Notifica", + "NotificationsTraktSettingsExpires": "Scadenze", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Autentica con Trakt", + "NotificationsValidationUnableToConnectToApi": "Impossibile connettersi alle API di {service}. Connessione al server fallita: ({responseCode}) {exceptionMessage}", + "InteractiveImportNoFilesFound": "Nessun video trovato nella castella selezionata", + "Or": "o", + "ManageLists": "Gestisci Liste", + "OriginalLanguage": "Lingua Originale", + "OverrideGrabNoQuality": "Qualità deve essere selezionata", + "XmlRpcPath": "Percorso XML RPC", + "WouldYouLikeToRestoreBackup": "Vuoi ripristinare il backup '{name}'?", + "PendingDownloadClientUnavailable": "In Attesa - Client di Download in attesa", + "PreviouslyInstalled": "Precedentemente Installato", + "MissingLoadError": "Errore caricando elementi mancanti", + "MonitorSelected": "Monitora Selezionati", + "Period": "Periodo", + "RemoveFailedDownloads": "Rimuovi Download Falliti", + "RemoveMultipleFromDownloadClientHint": "Rimuovi i download e i file dal client di download", + "RemoveSelectedItemQueueMessageText": "Sei sicuro di voler rimuovere 1 elemento dalla coda?", + "TagDetails": "Dettagli Etichetta - {label}", + "BranchUpdate": "Branca da usare per aggiornare {appName}", + "DefaultNotFoundMessage": "Ti devi essere perso, non c'è nulla da vedere qui.", + "DeleteIndexerMessageText": "Sicuro di voler eliminare l'indicizzatore '{name}'?", + "Socks5": "Socks5 (Supporto TOR)", + "DeleteEpisodeFileMessage": "Sei sicuro di volere eliminare '{path}'?", + "NotificationsKodiSettingsCleanLibraryHelpText": "Pulisci libreria dopo l'aggiornamento", + "PreferProtocol": "Preferisci {preferredProtocol}", + "RetryingDownloadOn": "Riprovando il download il {date} alle {time}", + "DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{name}'?", + "RemoveCompletedDownloads": "Rimuovi Download Completati", + "DownloadClientFloodSettingsAdditionalTags": "Tag addizionali", + "DelayingDownloadUntil": "Ritardare il download fino al {date} alle {time}", + "DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{name}'?", + "NoHistoryFound": "Nessun storico trovato", + "OneMinute": "1 Minuto", + "OptionalName": "Nome opzionale", + "DeleteSelectedIndexers": "Elimina Indicizzatore/i", + "Branch": "Branca", + "Debug": "Debug", + "Never": "Mai", + "UsenetDelayTime": "Ritardo Usenet: {usenetDelay}", + "OrganizeModalHeader": "Organizza & Rinomina", + "Parse": "Analizza", + "RemoveFromDownloadClient": "Rimuovi dal client di download", + "RemoveQueueItemConfirmation": "Sei sicuro di voler rimuovere '{sourceTitle}' dalla coda?", + "NoIndexersFound": "Nessun indicizzatore trovato", + "DeleteImportListMessageText": "Sei sicuro di volere eliminare la lista '{name}'?", + "DeleteDelayProfile": "Elimina Profilo di Ritardo", + "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?", + "MinutesSixty": "60 Minuti: {sixty}", + "NotificationsCustomScriptSettingsName": "Script personalizzato", + "NotificationsCustomScriptValidationFileDoesNotExist": "Cartella non esiste", + "Database": "Database", + "NotificationsPushoverSettingsExpire": "Scadenza", + "NotificationsSettingsWebhookMethod": "Metodo", + "NotificationsSynologyValidationInvalidOs": "Deve essere un Synology", + "NotificationsTraktSettingsRefreshToken": "Refresh Token", + "CouldNotFindResults": "Nessun risultato trovato per '{term}'", + "IndexerSettingsApiPath": "Percorso API", + "AutoTaggingSpecificationMaximumYear": "Anno Massimo", + "AutoTaggingSpecificationGenre": "Genere/i", + "AutoTaggingSpecificationMinimumYear": "Anno Minimo", + "AutoTaggingSpecificationOriginalLanguage": "Lingua", + "AutoTaggingSpecificationQualityProfile": "Profilo Qualità", + "AutoTaggingSpecificationRootFolder": "Cartella Radice", + "AutoTaggingSpecificationStatus": "Stato", + "CustomFormatsSpecificationLanguage": "Linguaggio", + "CustomFormatsSpecificationMaximumSize": "Dimensione Massima", + "CustomFormatsSpecificationMinimumSize": "Dimensione Minima", + "DelayProfile": "Profilo di Ritardo", + "DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{name}'?", + "DeleteDelayProfileMessageText": "Sei sicuro di volere eliminare questo profilo di ritardo?", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Invia il messaggio silenziosamente. L'utente riceverà una notifica senza suono", + "NotificationsPushoverSettingsRetry": "Riprova", + "NotificationsSettingsWebhookUrl": "URL Webhook", + "PackageVersionInfo": "{packageVersion} di {packageAuthor}", + "RemoveQueueItem": "Rimuovi - {sourceTitle}", + "ParseModalErrorParsing": "Errore durante l'analisi, per favore prova di nuovo.", + "OverrideGrabNoLanguage": "Almeno una lingua deve essere selezionata", + "PasswordConfirmation": "Conferma Password", + "RemoveSelectedItemsQueueMessageText": "Sei sicuro di voler rimuovere {selectedCount} elementi dalla coda?", + "ConnectionLostToBackend": "{appName} ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.", + "CountIndexersSelected": "{count} indicizzatore(i) selezionato(i)", + "CountDownloadClientsSelected": "{count} client di download selezionato/i", + "PrioritySettings": "Priorità: {priority}", + "OverrideAndAddToDownloadQueue": "Sovrascrivi e aggiungi alla coda di download", + "NotificationsSettingsUseSslHelpText": "Connetti a {serviceName} tramite HTTPS indece di HTTP", + "OrganizeRelativePaths": "Tutti i percorsi sono relativi a: `{path}`", + "CurrentlyInstalled": "Attualmente Installato", + "NotificationsEmailSettingsName": "Email", + "NotificationsNtfySettingsServerUrl": "URL Server", + "NotificationsPushoverSettingsSound": "Suono", + "NotificationsSignalValidationSslRequired": "SSL sembra essere richiesto", + "TorrentDelayTime": "Ritardo torrent: {torrentDelay}", + "NotificationsTwitterSettingsMention": "Menziona", + "NotificationsPushoverSettingsDevices": "Dispositivi", + "NotificationsTelegramSettingsSendSilently": "Invia Silenziosamente", + "DatabaseMigration": "Migrazione Database", + "AutoTaggingSpecificationTag": "Etichetta", + "CustomFormatUnknownConditionOption": "Opzione sconosciuta '{key}' per la condizione '{implementation}'", + "CustomFormatsSpecificationResolution": "Risoluzione", + "CustomFormatsSpecificationSource": "Fonte", + "BlocklistAndSearch": "Lista dei Blocchi e Ricerca", + "NotificationsEmbySettingsSendNotifications": "Invia Notifiche", + "IndexerHDBitsSettingsMediumsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "DeleteQualityProfile": "Elimina Profilo Qualità", + "DeleteSelectedEpisodeFiles": "Elimina i File degli Episodi Selezionati", + "DeleteEpisodesFiles": "Elimina i File di {episodeFileCount} Episodi", + "CustomFilter": "Filtro Personalizzato", + "NotificationsTelegramSettingsIncludeAppName": "Includi {appName} nel Titolo", + "IndexerHDBitsSettingsCodecsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "NotificationsGotifySettingsAppToken": "App Token", + "InfoUrl": "URL Info", + "ConnectionLostReconnect": "{appName} cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.", + "ListWillRefreshEveryInterval": "Le liste verranno aggiornate ogni {refreshInterval}", + "NotificationsNtfySettingsServerUrlHelpText": "Lascia vuoto per usare il server pubblico {url}", + "NotificationsTwitterSettingsMentionHelpText": "Menziona questo utente nei tweet inviati", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Impossibile inviare messaggio di prova. Risposta dalle API: {error}", + "RemoveFromDownloadClientHint": "Rimuovi il download e i file dal client di download", + "DayOfWeekAt": "{day} alle {time}", + "DeleteRootFolder": "Elimina Cartella Radice", + "DeleteRootFolderMessageText": "Sei sicuro di volere eliminare la cartella radice '{path}'?", + "ManageIndexers": "Gestisci Indicizzatori", + "MissingNoItems": "Nessun elemento mancante", + "NotificationsKodiSettingsCleanLibrary": "Pulisci Libreria", + "NotificationsNtfySettingsUsernameHelpText": "Nome utente opzionale", + "NotificationsSettingsUpdateLibrary": "Aggiorna Libreria", + "NotificationsSlackSettingsChannel": "Canale", + "NotificationsSlackSettingsIcon": "Icona", + "NotificationsTelegramSettingsBotToken": "Token Bot", + "NotificationsTwitterSettingsAccessToken": "Access Token", + "NotificationsTwitterSettingsConnectToTwitter": "Connetti a Twitter / X", + "NotificationsTwitterSettingsDirectMessage": "Messaggio Diretto", + "NotificationsValidationInvalidUsernamePassword": "Nome Utente o password non validi", + "NotificationsValidationUnableToConnect": "Impossibile connettersi: {exceptionMessage}", + "NotificationsValidationUnableToConnectToService": "Impossibile connettersi a {serviceName}", + "NotificationsValidationUnableToSendTestMessage": "Impossibile inviare messaggio di prova: {exceptionMessage}", + "ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il suo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da Theme.Park", + "Torrents": "Torrents" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index f6da2c4df..432259958 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2078,5 +2078,6 @@ "TodayAt": "Hoje às {time}", "TomorrowAt": "Amanhã às {time}", "HasUnmonitoredSeason": "Tem Temporada Não Monitorada", - "YesterdayAt": "Ontem às {time}" + "YesterdayAt": "Ontem às {time}", + "UnableToImportAutomatically": "Não foi possível importar automaticamente" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index b3588e5bc..b82f40186 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -846,5 +846,6 @@ "AllResultsAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir", "AllSeriesInRootFolderHaveBeenImported": "{path} içerisindeki tüm diziler içeri aktarıldı", "AlternateTitles": "Alternatif Başlıklar", - "AnEpisodeIsDownloading": "Bir bölüm indiriliyor" + "AnEpisodeIsDownloading": "Bir bölüm indiriliyor", + "UnableToImportAutomatically": "Otomatikman İçe Aktarılamıyor" } From 55c1ce2e3d30f7d986d3ff3e756b3d0579ab8708 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 30 Jun 2024 11:28:48 -0700 Subject: [PATCH 347/762] Bump version to 4.0.6 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa85cf5dc..ba1cff820 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.5 + VERSION: 4.0.6 jobs: backend: From 5c327d5be331a5deac90c6503827a12813738479 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 4 Jul 2024 11:25:16 +0000 Subject: [PATCH 348/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: quek76 <quek@libertysurf.fr> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 4 +- src/NzbDrone.Core/Localization/Core/fr.json | 8 +- src/NzbDrone.Core/Localization/Core/it.json | 452 +++++++++++++++++- .../Localization/Core/pt_BR.json | 4 +- 4 files changed, 454 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index c7a8a340a..94fbb72fb 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -128,7 +128,7 @@ "AudioLanguages": "Idiomas de Audio", "Episode": "Episodio", "Activity": "Actividad", - "AddNew": "Añadir Nuevo", + "AddNew": "Añadir nuevo", "ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente", "ApplyTagsHelpTextRemove": "Eliminar: Elimina las etiquetas introducidas", "Blocklist": "Lista de bloqueos", @@ -287,7 +287,7 @@ "History": "Historial", "MonitorNoNewSeasonsDescription": "No monitorizar automáticamente ninguna temporada nueva", "HistoryLoadError": "No se pudo cargar el historial", - "LibraryImport": "Importar Librería", + "LibraryImport": "Importar biblioteca", "RescanSeriesFolderAfterRefresh": "Volver a escanear la carpeta de series tras actualizar", "Wanted": "Buscado", "MonitorPilotEpisodeDescription": "Sólo monitorizar el primer episodio de la primera temporada", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 19f040494..5399a034f 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -299,7 +299,7 @@ "SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre", "Sunday": "Dimanche", "TorrentDelay": "Retard du torrent", - "DownloadClients": "Clients de téléchargement", + "DownloadClients": "Clients de télécharg.", "CustomFormats": "Formats perso.", "NoIndexersFound": "Aucun indexeur n'a été trouvé", "Profiles": "Profils", @@ -550,7 +550,7 @@ "LastWriteTime": "Heure de la dernière écriture", "LatestSeason": "Dernière saison", "LibraryImportTipsDontUseDownloadsFolder": "Ne l'utilisez pas pour importer des téléchargements à partir de votre client de téléchargement, cela concerne uniquement les bibliothèques organisées existantes, pas les fichiers non triés.", - "ListWillRefreshEveryInterval": "La liste sera actualisée tous les {refreshInterval}", + "ListWillRefreshEveryInterval": "La liste se rafraîchira toutes les {refreshInterval}", "ListsLoadError": "Impossible de charger les listes", "Local": "Locale", "LocalPath": "Chemin local", @@ -849,7 +849,7 @@ "MinimumFreeSpaceHelpText": "Empêcher l'importation si elle laisse moins d'espace disque disponible", "MinimumLimits": "Limites minimales", "MinutesFortyFive": "45 Minutes : {fortyFive}", - "Monitor": "Surveillé", + "Monitor": "Surveiller", "MonitorAllEpisodesDescription": "Surveillez tous les épisodes sauf les spéciaux", "MonitorExistingEpisodes": "Épisodes existants", "MonitorExistingEpisodesDescription": "Surveiller les épisodes contenant des fichiers ou qui n'ont pas encore été diffusés", @@ -1131,7 +1131,7 @@ "NotSeasonPack": "Pas de pack saisonnier", "NotificationTriggersHelpText": "Sélectionnez les événements qui doivent déclencher cette notification", "NotificationsTagsSeriesHelpText": "N'envoyer des notifications que pour les séries avec au moins une balise correspondante", - "OnApplicationUpdate": "Sur la mise à jour de l'application", + "OnApplicationUpdate": "Lors de la mise à jour de l'application", "OnEpisodeFileDelete": "Lors de la suppression du fichier de l'épisode", "OnHealthIssue": "Sur la question de la santé", "OnManualInteractionRequired": "Sur l'interaction manuelle requise", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index a51ac1461..977e0def0 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -59,7 +59,7 @@ "AppUpdated": "{appName} Aggiornato", "AppUpdatedVersion": "{appName} è stato aggiornato alla versione `{version}`, per vedere le modifiche devi ricaricare {appName} ", "ApplicationURL": "URL Applicazione", - "AuthenticationMethodHelpText": "Inserisci Username e Password per accedere a {appName}", + "AuthenticationMethodHelpText": "Utilizza nome utente e password per accedere a {appName}", "BindAddressHelpText": "Indirizzi IP validi, localhost o '*' per tutte le interfacce", "BeforeUpdate": "Prima dell'aggiornamento", "CalendarFeed": "Feed calendario {appName}", @@ -147,7 +147,7 @@ "CreateGroup": "Crea gruppo", "DeleteEmptyFolders": "Cancella le cartelle vuote", "Enabled": "Abilitato", - "UpdateMechanismHelpText": "Usa il sistema di aggiornamento interno di {appName} o uno script", + "UpdateMechanismHelpText": "Usa il sistema di aggiornamento incorporato di {appName} o uno script", "AllResultsAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro applicato", "EditSelectedDownloadClients": "Modifica i Client di Download Selezionati", "EditSelectedImportLists": "Modifica le Liste di Importazione Selezionate", @@ -255,8 +255,8 @@ "Donate": "Dona", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nessuna destinazione predefinita", "ImportListSettings": "Impostazioni delle Liste", - "DownloadClientFreeboxSettingsAppId": "App ID", - "DownloadClientFreeboxSettingsAppToken": "App Token", + "DownloadClientFreeboxSettingsAppId": "ID App", + "DownloadClientFreeboxSettingsAppToken": "Token App", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Questa cartella dovrà essere raggiungibile da XBMC", "DownloadClientPneumaticSettingsNzbFolder": "Cartella Nzb", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Disattiva ordinamento per data", @@ -336,7 +336,7 @@ "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?", "MinutesSixty": "60 Minuti: {sixty}", "NotificationsCustomScriptSettingsName": "Script personalizzato", - "NotificationsCustomScriptValidationFileDoesNotExist": "Cartella non esiste", + "NotificationsCustomScriptValidationFileDoesNotExist": "File non esiste", "Database": "Database", "NotificationsPushoverSettingsExpire": "Scadenza", "NotificationsSettingsWebhookMethod": "Metodo", @@ -423,5 +423,445 @@ "NotificationsValidationUnableToConnectToService": "Impossibile connettersi a {serviceName}", "NotificationsValidationUnableToSendTestMessage": "Impossibile inviare messaggio di prova: {exceptionMessage}", "ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il suo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da Theme.Park", - "Torrents": "Torrents" + "Torrents": "Torrents", + "Upcoming": "In arrivo", + "DownloadClientUTorrentTorrentStateError": "uTorrent sta segnalando un errore", + "DownloadClientValidationSslConnectFailure": "Impossibile connettersi tramite SSL", + "ErrorLoadingContent": "Si è verificato un errore caricando questo contenuto", + "FilterDoesNotStartWith": "non inizia con", + "Filters": "Filtri", + "IgnoreDownloads": "Ignora Download", + "IndexerSettingsApiPathHelpText": "Percorso API, solitamente {url}", + "BlackholeFolderHelpText": "Cartella nella quale {appName} salverà i file di tipo {extension}", + "UseSeasonFolder": "Usa Cartella Stagione", + "Monday": "Lunedì", + "DetailedProgressBarHelpText": "Mostra testo sulla barra di avanzamento", + "DownloadClientValidationUnknownException": "Eccezione sconosciuta: {exception}", + "OnlyTorrent": "Solo Torrent", + "OpenBrowserOnStart": "Apri browser all'avvio", + "OnlyUsenet": "Solo Usenet", + "OpenSeries": "Apri Serie", + "Organize": "Organizza", + "Other": "Altri", + "Yesterday": "Ieri", + "Paused": "In Pausa", + "Priority": "Priorità", + "Metadata": "Metadati", + "Quality": "Qualità", + "MidseasonFinale": "Finale di Metà Stagione", + "QualityProfile": "Profilo Qualità", + "MinimumAge": "Età Minima", + "ShowAdvanced": "Mostra Avanzate", + "MonitorFirstSeason": "Prima Stagione", + "MonitoredOnly": "Solo Monitorati", + "MoreInfo": "Ulteriori Informazioni", + "FormatRuntimeMinutes": "{minutes}m", + "Options": "Opzioni", + "PartialSeason": "Stagione Parziale", + "Port": "Porta", + "PreferTorrent": "Preferisci Torrent", + "Reset": "Reimposta", + "RssIsNotSupportedWithThisIndexer": "RSS non è supportato con questo indicizzatore", + "PublishedDate": "Data Pubblicazione", + "SeriesDetailsGoTo": "Vai a {title}", + "Security": "Sicurezza", + "Settings": "Impostazioni", + "ShowDateAdded": "Mostra Data Aggiunta", + "Reason": "Ragione", + "RecyclingBin": "Cestino", + "SpecialEpisode": "Episodio Speciale", + "Space": "Spazio", + "SslPort": "Porta SSL", + "StartImport": "Inizia Importazione", + "Test": "Prova", + "Titles": "Titoli", + "Result": "Risultato", + "Unavailable": "Non disponibile", + "SearchSelected": "Ricerca Selezionate", + "SeasonCount": "Conteggio Stagioni", + "Season": "Stagione", + "SeriesType": "Tipo Serie", + "ShowNetwork": "Mostra Rete", + "MoveSeriesFoldersMoveFiles": "Sì, Sposta i File", + "Negated": "Negato", + "SizeOnDisk": "Dimensione sul disco", + "Sort": "Ordina", + "FilterSeriesPlaceholder": "Filtra serie", + "FormatShortTimeSpanHours": "{hours} ora/e", + "Monitoring": "Monitorando", + "Month": "Mese", + "MonitoredEpisodesHelpText": "Scarica gli episodi monitorati in questa serie", + "NoChanges": "Nessun Cambiamento", + "NotificationsNtfyValidationAuthorizationRequired": "Autorizzazione richiesta", + "PreferredProtocol": "Protocollo Preferito", + "QualitiesHelpText": "Qualità più alte nella lista sono quelle preferite. Qualità all'interno dello stesso gruppo sono equivalenti. Solo le qualità selezionate saranno ricercate", + "Real": "Reale", + "RefreshAndScanTooltip": "Aggiorna informazioni e scansiona disco", + "SeriesIndexFooterMissingMonitored": "Episodi Mancanti (Serie monitorate)", + "SingleEpisode": "Episodio Singolo", + "Shutdown": "Spegnimento", + "Wiki": "Wiki", + "True": "Vero", + "WhatsNew": "Cosa c'è di nuovo?", + "SelectQuality": "Seleziona Qualità", + "SelectLanguages": "Seleziona Lingue", + "TableOptions": "Opzioni Tabella", + "Tba": "TBA", + "TablePageSizeMinimum": "La dimensione della pagina deve essere almeno {minimumValue}", + "Theme": "Tema", + "Updates": "Aggiornamenti", + "VersionNumber": "Versione {version}", + "EditSelectedIndexers": "Modifica Indicizzatori Selezionati", + "Example": "Esempio", + "FilterContains": "contiene", + "FilterDoesNotContain": "non contiene", + "FilterIsAfter": "è dopo", + "FilterIsBefore": "è prima", + "FilterIs": "è", + "FilterLessThanOrEqual": "meno o uguale di", + "FilterNotEqual": "non uguale", + "DownloadClientFreeboxApiError": "Le API di Freebox hanno segnalato un errore: {errorDescription}", + "DailyEpisodeFormat": "Formato Episodi Giornalieri", + "FileManagement": "Gestione File", + "MissingEpisodes": "Episodi Mancanti", + "Mode": "Modalità", + "MonitorAllEpisodesDescription": "Monitora tutti gli episodi esclusi gli speciali", + "MonitorAllEpisodes": "Tutti gli Episodi", + "MonitorExistingEpisodes": "Episodi Esistenti", + "Preferred": "Preferito", + "Scene": "Scene", + "SizeLimit": "Limite Dimensione", + "Twitter": "Twitter", + "Type": "Tipo", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Questa opzione richiede almeno la versione 16.0 di NzbGet", + "System": "Sistema", + "CollectionsLoadError": "Impossibile caricare le collezioni", + "ConnectSettingsSummary": "Notifiche, connessioni a media servers/players, e script personalizzati", + "Folders": "Cartelle", + "PreferredSize": "Dimensione Preferita", + "Proxy": "Proxy", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una connessione sicura. Vedi Opzioni -> Web UI -> 'Usa HTTPS invece di HTTP' in qBittorrent.", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent sta segnalando un errore", + "TimeLeft": "Tempo Rimasto", + "Unlimited": "Illimitato", + "Title": "Titolo", + "Original": "Originale", + "Path": "Percorso", + "MaximumSize": "Dimensione Massima", + "ProgressBarProgress": "Barra Progressi al {progress}%", + "RootFolderSelectFreeSpace": "{freeSpace} Libero", + "RssSync": "Sincronizza RSS", + "SaveSettings": "Salva Impostazioni", + "Search": "Ricerca", + "SearchForMissing": "Ricerca dei Mancanti", + "SearchForMonitoredEpisodes": "Ricerca degli episodi monitorati", + "SeasonFolder": "Cartella della Stagione", + "SeasonNumber": "Numero Stagione", + "SelectLanguageModalTitle": "{modalTitle} - Seleziona Lingua", + "SeriesDetailsRuntime": "{runtime} Minuti", + "SeriesIsUnmonitored": "Serie non monitorata", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Totale: {totalEpisodeCount}, Scaricando: {downloadingCount})", + "ShowEpisodes": "Mostra Episodi", + "ShowEpisodeInformationHelpText": "Mostra titolo e numero dell'episodio", + "Source": "Fonte", + "Unknown": "Sconosciuto", + "UseSsl": "Usa SSL", + "DownloadClientSettingsDestinationHelpText": "Specifica manualmente la destinazione dei download, lascia vuoti per usare la predefinita", + "DownloadClientSettingsInitialState": "Stato Iniziale", + "Folder": "Cartella", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Solo Lettura", + "Ui": "Interfaccia", + "UiLanguage": "Lingua Interfaccia", + "UiSettingsLoadError": "Impossibile caricare le impostazioni interfaccia", + "MustNotContain": "Non Deve Contenere", + "Network": "Rete", + "ReadTheWikiForMoreInformation": "Leggi la Wiki per più informazioni", + "RecentChanges": "Cambiamenti Recenti", + "Refresh": "Aggiorna", + "DownloadClientSettingsUseSslHelpText": "Usa connessione sicura quando connetti a {clientName}", + "SelectEpisodes": "Seleziona Episodio/i", + "TestAll": "Prova Tutto", + "SelectFolder": "Seleziona Cartella", + "DownloadStationStatusExtracting": "Estrazione: {progress}&", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordine Sequenziale", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} non è stato in grado di aggiungere l'etichetta a qBittorrent.", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Scarica in ordine sequenziale (qBittorrent 4.1.0+)", + "MegabytesPerMinute": "Megabyte Per Minuto", + "MonitorFirstSeasonDescription": "Monitora tutti gli episodi delle prima stagione. Tutte le altre stagioni saranno ignorate", + "MustContain": "Deve Contenere", + "PreferUsenet": "Preferisci Usenet", + "NoChange": "Nessun Cambio", + "RestoreBackup": "Ripristina Backup", + "SelectAll": "Seleziona Tutto", + "SelectSeries": "Seleziona Serie", + "SeriesTitle": "Titolo Serie", + "ShowPath": "Mostra Percorso", + "Table": "Tabella", + "TheTvdb": "TheTVDB", + "Total": "Totale", + "TotalFileSize": "Totale Dimensione File", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent sta scaricando i metadati", + "DownloadClientQbittorrentTorrentStateUnknown": "Stato di download sconosciuto: {state}", + "External": "Esterno", + "Failed": "Fallito", + "FilterLessThan": "meno di", + "FilterDoesNotEndWith": "non termina con", + "FilterEqual": "uguale", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "FormatShortTimeSpanMinutes": "{minutes} minuto/i", + "InvalidUILanguage": "L'interfaccia è impostata in una lingua non valida, correggi e salva le tue impostazioni", + "Max": "Massimo", + "MinutesFortyFive": "45 Minuti: {fortyFive}", + "NoDownloadClientsFound": "Nessun client di download trovato", + "MassSearchCancelWarning": "Questo non può essere cancellato una volta avviato senza riavviare {appName} o disattivando tutti i tuoi indicizzatori.", + "Profiles": "Profili", + "Qualities": "Qualità", + "QualityProfilesLoadError": "Impossibile caricare Profili Qualità", + "QueueLoadError": "Impossibile caricare la Coda", + "Queued": "In Coda", + "SaveChanges": "Salva Cambiamenti", + "Seasons": "Stagioni", + "Series": "Serie", + "MonitorRecentEpisodes": "Episodi Recenti", + "MoreDetails": "Ulteriore dettagli", + "MonitorSpecialEpisodes": "Monitora Speciali", + "TimeFormat": "Formato Orario", + "UsenetDisabled": "Usenet Disabilitato", + "Version": "Versione", + "WithFiles": "Con i File", + "Username": "Nome Utente", + "YesCancel": "Sì, Cancella", + "Yes": "Sì", + "DownloadClientRTorrentSettingsAddStopped": "Aggiungi Fermato", + "Restart": "Riavvia", + "Rss": "RSS", + "ProxyBadRequestHealthCheckMessage": "Test del proxy fallito: Status Code: {statusCode}", + "ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come wildcard per i sottodomini", + "RestartRequiredToApplyChanges": "{appName} richiede un riavvio per applicare i cambiamenti, vuoi riavviare ora?", + "SearchMonitored": "Ricerca Monitorate", + "SeasonDetails": "Dettagli Stagione", + "SelectDownloadClientModalTitle": "{modalTitle} - Seleziona Client di Download", + "SelectDropdown": "Seleziona...", + "SeriesIsMonitored": "Serie monitorata", + "Special": "Speciale", + "TablePageSizeMaximum": "La dimensione della pagina non deve superare {maximumValue}", + "TablePageSize": "Dimensione Pagina", + "UnknownDownloadState": "Stato download sconosciuto: {state}", + "UsenetBlackholeNzbFolder": "Cartella Nzb", + "From": "Da", + "QualityProfiles": "Profili Qualità", + "RecyclingBinCleanupHelpText": "Imposta a 0 per disattivare la pulizia automatica", + "RefreshAndScan": "Aggiorna & Scansiona", + "HourShorthand": "h", + "InteractiveImportNoImportMode": "Una modalità di importazione deve essere selezionata", + "MyComputer": "Mio Computer", + "Posters": "Locandine", + "SeasonNumberToken": "Stagione {seasonNumber}", + "ShowTitle": "Mostra Titolo", + "TorrentBlackholeTorrentFolder": "Cartella Torrent", + "UiLanguageHelpText": "Lingua che {appName} userà per l'interfaccia", + "DownloadIgnored": "Download Ignorato", + "CustomFormatsSpecificationMaximumSizeHelpText": "La release deve essere minore o uguale a questa dimensione", + "CustomFormatsSpecificationMinimumSizeHelpText": "La release deve essere maggiore di questa dimensione", + "CustomFormatsSpecificationRegularExpression": "Espressione Regolare", + "DefaultDelayProfileSeries": "Questo è il profilo di default. Viene Applicato a tutte le serie che non hanno in profilo esplicito.", + "DownloadClientDelugeTorrentStateError": "Deluge sta segnalando un errore", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Aggiungi un prefisso all'url del json di deluge, vedi {url}", + "FilterEpisodesPlaceholder": "Filtra episodi per titolo o numero", + "FilterGreaterThanOrEqual": "più grande o uguale di", + "FilterIsNot": "non è", + "FilterGreaterThan": "più grande di", + "Formats": "Formati", + "FormatRuntimeHours": "{hours}h", + "FirstDayOfWeek": "Primo Giorno della Settimana", + "FreeSpace": "Spazio Libero", + "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatTimeSpanDays": "{days}d {time}", + "MetadataSettingsSeriesImages": "Immagini della Serie", + "MetadataSettingsSeriesMetadata": "Metadati della Serie", + "MetadataSettingsSeasonImages": "Immagini della Stagione", + "NotificationsEmailSettingsBccAddress": "Indirizzo/i BCC", + "OrganizeLoadError": "Errore caricando le anteprime", + "OrganizeSelectedSeriesModalHeader": "Organizza Serie Selezionate", + "Today": "Oggi", + "Specials": "Speciali", + "NotificationsLoadError": "Impossibile caricare Notifiche", + "SeasonPassTruncated": "Solo le ultime 25 stagione sono mostrate, vai ai dettagli per vedere tutte le stagioni", + "SeriesCannotBeFound": "Scusa, quella serie non può essere trovata.", + "Style": "Stile", + "NotificationsEmailSettingsCcAddress": "Indirizzo/i CC", + "NotificationsMailgunSettingsUseEuEndpoint": "Usa Endpoint EU", + "Pending": "In Attesa", + "PendingChangesStayReview": "Rimani e rivedi i cambiamenti", + "FormatAgeDays": "giorni", + "FormatAgeHour": "ora", + "FormatAgeHours": "ore", + "FormatAgeMinute": "minuto", + "FormatAgeMinutes": "minuti", + "FormatAgeDay": "giorno", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Crea un file .plexmatch nella cartella della serie", + "MonitorMissingEpisodes": "Episodi Mancanti", + "New": "Nuovo", + "NoBackupsAreAvailable": "Nessun backup disponibile", + "MetadataSettingsEpisodeImages": "Immagini dell'Episodio", + "PosterSize": "Dimensioni Locandina", + "Restore": "Ripristina", + "RestartReloadNote": "Nota: {appName} si riavvierà automaticamente e ricaricherà l'interfaccia durante il processo di ripristino.", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} episodi scaricati", + "SelectLanguage": "Seleziona Lingua", + "SeriesIndexFooterDownloading": "Scaricando (Uno o più episodi)", + "SeriesTypes": "Tipi Serie", + "SetPermissions": "Imposta Permessi", + "StartupDirectory": "Cartella di Avvio", + "Tomorrow": "Domani", + "UnknownEventTooltip": "Evento sconosciuto", + "UseProxy": "Usa Proxy", + "InteractiveImportNoQuality": "Una qualità deve essere scelta per ogni file selezionato", + "PortNumber": "Numero Porta", + "MonitorAllSeasons": "Tutte le Stagioni", + "DownloadClientValidationUnableToConnect": "Impossibile connettersi a {clientName}", + "DownloadClientValidationVerifySslDetail": "Per favore verifica la tua configurazione SSL su entrambi {clientName} e {appName}", + "IndexerDownloadClientHealthCheckMessage": "Indicizzatori con client di download non validi: {indexerNames}.", + "MonitorAllSeasonsDescription": "Monitora tutte le nuove stagioni automaticamente", + "MonitorFutureEpisodes": "Episodi Futuri", + "No": "No", + "MonitorLastSeason": "Ultima Stagione", + "NoEpisodesInThisSeason": "Nessun episodio in questa stagione", + "OrganizeModalHeaderSeason": "Organizza & Rinomina - {season}", + "Save": "Salva", + "More": "Altro", + "MonitorLastSeasonDescription": "Monitora tutti gli episodi della ultima stagione", + "RestartSonarr": "Riavvia {appName}", + "SeasonFinale": "Finale di Stagione", + "RestartNow": "Riavvia ora", + "TablePageSizeHelpText": "Numero di elementi da mostrare in ogni pagina", + "UiSettings": "Impostazioni Interfaccia", + "TvdbId": "ID TVDB", + "Wanted": "Ricercato", + "CustomFormatsLoadError": "Impossibile a caricare Formati Personalizzati", + "FilterEndsWith": "termina con", + "MetadataSource": "Fonte Metadati", + "Monitored": "Monitorato", + "MoveSeriesFoldersDontMoveFiles": "No, Sposterò i File da Solo", + "RemoveQueueItemRemovalMethod": "Metodo di Rimozione", + "SecretToken": "Secret Token", + "UrlBase": "Base Url", + "NotificationsEmailSettingsFromAddress": "Dall'Indirizzo", + "NotificationsGotifySettingsServer": "Server Gotify", + "NotificationsGotifySettingsPriorityHelpText": "Priorità della notifica", + "Score": "Punteggio", + "SelectSeasonModalTitle": "{modalTitle} - Seleziona Stagione", + "UpdateAvailableHealthCheckMessage": "Nuovo aggiornamento disponibile", + "Permissions": "Permessi", + "Scheduled": "Pianificato", + "SearchAll": "Ricerca tutto", + "VisitTheWikiForMoreDetails": "Visita la wiki per ulteriori dettagli: ", + "Week": "Settimana", + "DownloadClientValidationSslConnectFailureDetail": "{appName} non è in grado di connettersi a {clientName} usando SSL. Questo problema potrebbe essere legato al computer. Prova a configurare entrambi {appName} e {clientName} senza usare SSL.", + "LogFilesLocation": "File di Log localizzati in: {location}", + "PendingChangesMessage": "Hai dei cambiamenti non salvati, sei sicuro di volere lasciare questa pagina?", + "NotificationsEmailSettingsUseEncryption": "Usa Crittografia", + "DailyEpisodeTypeDescription": "Episodi rilasciati giornalmente o meno frequentemente che usano anno-mese-giorno (2023-08-04)", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent sta segnalando dei file mancanti", + "NoEventsFound": "Nessun evento trovato", + "SearchIsNotSupportedWithThisIndexer": "Ricerca non supportata con questo indicizzatore", + "SelectReleaseType": "Seleziona Tipo Release", + "Uppercase": "Maiuscolo", + "False": "Falso", + "Files": "File", + "Filter": "Filtro", + "OverrideGrabNoEpisode": "Almeno un episodio deve essere selezionato", + "FilterStartsWith": "inizia con", + "FinaleTooltip": "Serie o finale di stagione", + "QualitySettings": "Impostazioni Qualità", + "Queue": "Coda", + "QueueIsEmpty": "La coda è vuota", + "QuickSearch": "Ricerca Veloce", + "RefreshSeries": "Aggiorna Serie", + "Year": "Anno", + "LanguagesLoadError": "Impossibile caricare le lingue", + "MetadataLoadError": "Impossibile caricare i Metadati", + "MetadataSettings": "Impostazioni Metadati", + "MetadataSettingsEpisodeMetadata": "Metadati dell'Episodio", + "Status": "Stato", + "DeleteQualityProfileMessageText": "Sicuro di voler cancellare il profilo di qualità '{name}'?", + "NotificationsGotifySettingsServerHelpText": "URL server Gotify, includendo http(s):// e porta se necessario", + "Time": "Orario", + "Password": "Password", + "OrganizeNothingToRename": "Successo! Il mio lavoro è finito, nessun file da rinominare.", + "PosterOptions": "Opzioni Locandina", + "Progress": "Progressi", + "Protocol": "Protocollo", + "ProxyFailedToTestHealthCheckMessage": "Test del proxy fallito: {url}", + "ProxyType": "Tipo Proxy", + "QualitiesLoadError": "Impossibile caricare qualità", + "RecyclingBinCleanup": "Pulizia Cestino", + "SelectFolderModalTitle": "{modalTitle} - Seleziona Cartella", + "ResetTitles": "Reimposta Titoli", + "RestartLater": "Lo riavvierò dopo", + "RssSyncInterval": "Intervallo Sincronizzazione RSS", + "Runtime": "Tempo di esecuzione", + "SearchByTvdbId": "Puoi anche ricercare usando l'ID TVDB di uno show. Es. tvdb:71663", + "SearchForMonitoredEpisodesSeason": "Ricerca degli episodi monitorati in questa stagione", + "SelectEpisodesModalTitle": "{modalTitle} - Seleziona Episodio/i", + "SelectSeason": "Seleziona Stagione", + "SeriesIndexFooterMissingUnmonitored": "Episodi Mancanti (Serie non monitorate)", + "ShowEpisodeInformation": "Mostra Informazioni Episodio", + "Size": "Dimensione", + "YesterdayAt": "Ieri alle {time}", + "UnableToImportAutomatically": "Impossibile Importare Automaticamente", + "Small": "Piccolo", + "Socks4": "Socks4", + "Standard": "Standard", + "Started": "Iniziato", + "TorrentsDisabled": "Torrent Disattivati", + "UnsavedChanges": "Cambiamenti Non Salvati", + "UnselectAll": "Deseleziona Tutto", + "UpdateAll": "Aggiorna Tutto", + "UpdateSonarrDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,", + "UseSeasonFolderHelpText": "Ordina episodi dentro all cartella della stagione", + "VideoCodec": "Codec Video", + "DownloadClientValidationCategoryMissingDetail": "La categoria che ha inserito non esiste in {clientName}. Crealo prima su {clientName}.", + "RestrictionsLoadError": "Impossibile caricare le Restrizioni", + "SearchFailedError": "Ricerca fallita, per favore riprova nuovamente dopo.", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} non è stato in grado di aggiungere l'etichetta a {clientName}.", + "DownloadClientFreeboxAuthenticationError": "Autenticazione alle API di Freebox fallita. Ragione: {errorDescription}", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Categorie non supportate fino alla versione 3.3.0 di qBittorrent. Per favore aggiorna o prova con una Categoria vuota.", + "DownloadClientSettingsInitialStateHelpText": "Stato iniziale per i torrent aggiunti a {clientName}", + "DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare i '{count}' client di download selezionato/i?", + "DownloadClientSettingsAddPaused": "Aggiungi In Pausa", + "DownloadClientValidationUnableToConnectDetail": "Per favore verifica nome host e porta.", + "FormatShortTimeSpanSeconds": "{seconds} secondo/i", + "IgnoreDownload": "Ignora Download", + "Implementation": "Implementazione", + "File": "File", + "LabelIsRequired": "Etichetta richiesta", + "IndexerSettingsSeedRatio": "Rapporto Seed", + "ManageClients": "Gestisci Clients", + "ManageDownloadClients": "Gestisci Clients di Download", + "Message": "Messaggio", + "Min": "Min", + "MinutesThirty": "30 Minuti: {thirty}", + "Missing": "Mancante", + "MonitorNewItems": "Monitora Nuovi Elementi", + "MonitorNewSeasons": "Monitora Nuove Stagioni", + "MonitorNewSeasonsHelpText": "Quali nuove stagioni devono essere monitorati automaticamente", + "MonitorNoEpisodes": "Nessuno", + "MonitorNoEpisodesDescription": "Nessun episodio sarà monitorato", + "NotificationsDiscordSettingsAvatar": "Avatar", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Abilita per usare l'endpoint EU di MailGun", + "MonitorNoNewSeasonsDescription": "Non monitorare nessuna nuova stagione automaticamente", + "MonitorPilotEpisode": "Episodio Pilota", + "MonitorPilotEpisodeDescription": "Monitora solo il primo episodio della prima stagione", + "MonitorSeries": "Monitora Serie", + "MonitoredStatus": "Monitorato/Stato", + "MoveFiles": "Sposta File", + "Name": "Nome", + "Negate": "Nega", + "SubtitleLanguages": "Lingua Sottotitoli", + "Sunday": "Domenica", + "TableColumns": "Colonne", + "TodayAt": "Oggi alle {time}", + "TomorrowAt": "Domani alle {time}", + "TotalSpace": "Totale Spazio" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 432259958..8fa4b47dc 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -205,7 +205,7 @@ "SeasonNumber": "Número da Temporada", "SeriesTitle": "Título da Série", "Special": "Especial", - "TestParsing": "Testar Análise", + "TestParsing": "Análise de teste", "About": "Sobre", "Actions": "Ações", "AppDataDirectory": "Diretório AppData", @@ -891,7 +891,7 @@ "UnmonitorDeletedEpisodes": "Cancelar Monitoramento de Episódios Excluídos", "UnsavedChanges": "Alterações Não Salvas", "UpdateAutomaticallyHelpText": "Baixe e instale atualizações automaticamente. Você ainda poderá instalar a partir do Sistema: Atualizações", - "UpdateMechanismHelpText": "Use o atualizador integrado do {appName} ou um script", + "UpdateMechanismHelpText": "Usar o atualizador integrado do {appName} ou um script", "UpdateSonarrDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,", "UpdateUiNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de IU '{uiFolder}' não pode ser salva pelo usuário '{userName}'.", "UpgradeUntil": "Atualizar Até", From ac1da45ecd33422095a26c75b3a357987f0941e7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:33:33 +0300 Subject: [PATCH 349/762] Fixed: Calculate Custom Formats after user specified options in Manual Import --- .../Manual/ManualImportService.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index f1fcd03cf..91599f843 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -395,14 +395,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); item.DownloadId = downloadId; - if (decision.LocalEpisode.Series != null) - { - item.Series = decision.LocalEpisode.Series; - - item.CustomFormats = _formatCalculator.ParseCustomFormat(decision.LocalEpisode); - item.CustomFormatScore = item.Series.QualityProfile?.Value.CalculateCustomFormatScore(item.CustomFormats) ?? 0; - } - if (decision.LocalEpisode.Episodes.Any() && decision.LocalEpisode.Episodes.Select(c => c.SeasonNumber).Distinct().Count() == 1) { var seasons = decision.LocalEpisode.Episodes.Select(c => c.SeasonNumber).Distinct().ToList(); @@ -430,6 +422,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags; item.ReleaseType = decision.LocalEpisode.ReleaseType; + if (decision.LocalEpisode.Series != null) + { + item.Series = decision.LocalEpisode.Series; + + item.CustomFormats = _formatCalculator.ParseCustomFormat(decision.LocalEpisode); + item.CustomFormatScore = item.Series.QualityProfile?.Value.CalculateCustomFormatScore(item.CustomFormats) ?? 0; + } + return item; } @@ -506,8 +506,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual // Augment episode file so imported files have all additional information an automatic import would localEpisode = _aggregationService.Augment(localEpisode, trackedDownload?.DownloadItem); - localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); - localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; // Apply the user-chosen values. localEpisode.Series = series; @@ -518,6 +516,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags; localEpisode.ReleaseType = file.ReleaseType; + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); + localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + // TODO: Cleanup non-tracked downloads var importDecision = new ImportDecision(localEpisode); From 4ee0ae1418d01538ef52f6440e5f96a36b61bb1d Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 2 Jul 2024 05:31:58 +0300 Subject: [PATCH 350/762] Fixed: History with unknown series --- frontend/src/Activity/History/HistoryRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 2b19e6970..507fdc2d7 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -77,7 +77,7 @@ class HistoryRow extends Component { onMarkAsFailedPress } = this.props; - if (!episode) { + if (!series || !episode) { return null; } From c9ea40b87494d1f269182cd9c36ca057de155abe Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 3 Jul 2024 21:21:36 -0700 Subject: [PATCH 351/762] New: Parse VFI as French Closes #6927 --- src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs | 1 + src/NzbDrone.Core/Parser/LanguageParser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index a7a363c9f..1fe5b84fa 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Title.S01.720p.VFF.WEB-DL.AAC2.0.H.264-BTN")] [TestCase("Title.S01.720p.VFQ.WEB-DL.AAC2.0.H.264-BTN")] [TestCase("Title.S01.720p.TRUEFRENCH.WEB-DL.AAC2.0.H.264-BTN")] + [TestCase("Series In The Middle S01 Multi VFI VO 1080p WEB x265 HEVC AAC 5.1-Papaya")] public void should_parse_language_french(string postTitle) { var result = LanguageParser.ParseLanguages(postTitle); diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 1548e4f82..eea408334 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; - private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b))(?:(?i)(?![\W|_|^]SUB))", From bfe6a740faf8d977a8bf07e96b76f02b4957f97b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 3 Jul 2024 21:16:22 -0700 Subject: [PATCH 352/762] Fixed: Parsing of anime releases using standard numbering Closes #6925 --- .../ParserTests/SingleEpisodeParserFixture.cs | 4 ++++ src/NzbDrone.Core/Parser/Parser.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 5b88912bb..dd146b0f4 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -168,6 +168,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Босх: Спадок / Series: Legacy / S2E1 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] [TestCase("Titles.s06e01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)] [TestCase("Titles.s06.01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)] + [TestCase("[Judas] Series Title (2024) - S01E14", "Series Title (2024)", 1, 14)] + [TestCase("[ReleaseGroup] SeriesTitle S01E1 Webdl 1080p", "SeriesTitle", 1, 1)] + [TestCase("[SubsPlus+] Series no Chill - S02E01 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)] + [TestCase("[SubsPlus+] Series no Chill - S02E01v2 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index cd56bbf55..ebd0205ef 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -83,7 +83,7 @@ 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+)))+)(?:v\d+)?(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01) @@ -127,7 +127,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number S## (Full season) - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))).+?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?![ex]?\d+))).+?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number From a779a5fad2ce3c63ced270ba3dec13201f071553 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Jul 2024 15:59:32 -0700 Subject: [PATCH 353/762] Fixed: Parsing of anime season releases with 3-digit number in title --- src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 8d3a792b3..ac2ad5ce6 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)] [TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)] [TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)] + [TestCase("[Zoombie] Zom 100: Bucket List of the Dead S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Zom 100: Bucket List of the Dead", 1)] public void should_parse_full_season_release(string postTitle, string title, int season) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index ebd0205ef..fabdd8498 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -114,6 +114,10 @@ namespace NzbDrone.Core.Parser 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 S## (Full season) + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?![ex]?\d+))).+?(?:$|\.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)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -126,10 +130,6 @@ namespace NzbDrone.Core.Parser 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 with trailing number S## (Full season) - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?![ex]?\d+))).+?(?:$|\.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)", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 81ac73299a748004b37dc6a3f1bb14e2a53d0b4f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:34:56 +0300 Subject: [PATCH 354/762] Fixed: Bulk series deletion for unmonitored series Closes #6933 --- .../Select/Delete/DeleteSeriesModalContent.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index ea35657e3..eb26675c6 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -88,8 +88,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { return series.reduce( - (acc, s) => { - const { statistics = { episodeFileCount: 0, sizeOnDisk: 0 } } = s; + (acc, { statistics = {} }) => { const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; acc.totalEpisodeFileCount += episodeFileCount; @@ -155,17 +154,17 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { </div> <ul> - {series.map((s) => { - const { episodeFileCount = 0, sizeOnDisk = 0 } = s.statistics; + {series.map(({ title, path, statistics = {} }) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; return ( - <li key={s.title}> - <span>{s.title}</span> + <li key={title}> + <span>{title}</span> {deleteFiles && ( <span> <span className={styles.pathContainer}> - -<span className={styles.path}>{s.path}</span> + -<span className={styles.path}>{path}</span> </span> {!!episodeFileCount && ( From 04f85954989c849618da9d02774ec35735d22f00 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 28 Jun 2024 17:03:06 -0700 Subject: [PATCH 355/762] Custom Import List improvements Fixed: Add placeholder title for Custom Import List title New: Support 'title' property for Custom Import List --- src/NzbDrone.Core/ImportLists/Custom/CustomAPIResource.cs | 1 + src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomAPIResource.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomAPIResource.cs index 3dbe7e184..8db0b0f92 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomAPIResource.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomAPIResource.cs @@ -2,6 +2,7 @@ namespace NzbDrone.Core.ImportLists.Custom { public class CustomSeries { + public string Title { get; set; } public int TvdbId { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs index f82e2f9f9..dbe2938d0 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Core.ImportLists.Custom { series.Add(new ImportListItemInfo { + Title = item.Title.IsNullOrWhiteSpace() ? $"TvdbId: {item.TvdbId}" : item.Title, TvdbId = item.TvdbId }); } From 67943edfbce5cbe73441d5f9bdf7fc1626a8bdc1 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 1 Jul 2024 00:16:19 +0000 Subject: [PATCH 356/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index a5c4dd475..bab330939 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -12007,6 +12007,7 @@ "TrackedDownloadState": { "enum": [ "downloading", + "importBlocked", "importPending", "importing", "imported", From bfcdc89f6a7e659548fe7adae1e9a1ae846dead2 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 8 Jul 2024 13:25:13 +0000 Subject: [PATCH 357/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 2 +- src/NzbDrone.Core/Localization/Core/uk.json | 24 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index b92f93d57..aafc5599f 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -961,7 +961,7 @@ "Global": "Yleiset", "GeneralSettings": "Yleiset asetukset", "ImportListsLoadError": "Tuontilistojen lataus epäonnistui", - "Importing": "Tuonti", + "Importing": "Tuodaan", "IndexerDownloadClientHealthCheckMessage": "Tietolähteet virheellisillä lataustyökaluilla: {indexerNames}.", "Indexer": "Tietolähde", "Location": "Sijainti", diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 00bfc0bbb..0d758d187 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -345,5 +345,27 @@ "DownloadClientDelugeValidationLabelPluginInactive": "Плагін міток не активовано", "DownloadClientAriaSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2", "CustomFormatHelpText": "{appName} оцінює кожен випуск, використовуючи суму балів для відповідності користувацьких форматів. Якщо новий випуск покращить оцінку, з такою ж або кращою якістю, {appName} схопить його.", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station" + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station", + "ClickToChangeIndexerFlags": "Натисніть, щоб змінити прапорці індексатора", + "AutoTaggingLoadError": "Не вдалося завантажити автоматичне маркування", + "CountDownloadClientsSelected": "Вибрано {count} клієнтів завантажувача", + "CountImportListsSelected": "Вибрано {count} списків імпорту", + "AutoTagging": "Автоматичне маркування", + "CloneCondition": "Клонування умови", + "AutomaticAdd": "Автоматичне додавання", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Мінімальна оцінка користувацького формату, необхідна для обходу затримки для обраного протоколу", + "ChownGroup": "chown Група", + "BlocklistMultipleOnlyHint": "Додати до чорного списку без пошуку замін", + "BlocklistOnly": "Тільки чорний список", + "BlocklistOnlyHint": "Додати до чорного списку без пошуку заміни", + "ChangeCategoryHint": "Змінює завантаження на «Категорію після імпорту» з клієнта завантажувача", + "ChangeCategoryMultipleHint": "Змінює завантаження на «Категорію після імпорту» з клієнта завантажувача", + "BlocklistAndSearchHint": "Розпочати пошук заміни після додавання до чорного списку", + "BlocklistAndSearchMultipleHint": "Розпочати пошук замін після додавання до чорного списку", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматично шукати та намагатися завантажити інший реліз, якщо обраний реліз не вдалось завантажити з інтерактивного пошуку.", + "BlackholeWatchFolder": "Папка для спостереження", + "BypassDelayIfAboveCustomFormatScore": "Пропустити, якщо перевищено оцінку користувацького формату", + "Clone": "Клонування", + "BlocklistFilterHasNoItems": "Вибраний фільтр чорного списку не містить елементів", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Увімкнути обхід, якщо реліз має оцінку вищу за встановлений мінімальний бал користувацького формату" } From 1d06e40acb1f887870cfeaba4c53f19aa8907acc Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 6 Jul 2024 23:03:14 +0300 Subject: [PATCH 358/762] New: Queued episode count for seasons in series details --- frontend/src/Components/Label.css | 9 +++ frontend/src/Components/Label.css.d.ts | 1 + .../Series/Details/SeasonProgressLabel.tsx | 69 +++++++++++++++++++ .../src/Series/Details/SeriesDetailsSeason.js | 29 +++----- 4 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 frontend/src/Series/Details/SeasonProgressLabel.tsx diff --git a/frontend/src/Components/Label.css b/frontend/src/Components/Label.css index f3ff83993..c7512987a 100644 --- a/frontend/src/Components/Label.css +++ b/frontend/src/Components/Label.css @@ -88,6 +88,15 @@ } } +.purple { + border-color: var(--purple); + background-color: var(--purple); + + &.outline { + color: var(--purple); + } +} + /** Sizes **/ .small { diff --git a/frontend/src/Components/Label.css.d.ts b/frontend/src/Components/Label.css.d.ts index 1a0b4d9e0..778ba6faf 100644 --- a/frontend/src/Components/Label.css.d.ts +++ b/frontend/src/Components/Label.css.d.ts @@ -11,6 +11,7 @@ interface CssExports { 'medium': string; 'outline': string; 'primary': string; + 'purple': string; 'small': string; 'success': string; 'warning': string; diff --git a/frontend/src/Series/Details/SeasonProgressLabel.tsx b/frontend/src/Series/Details/SeasonProgressLabel.tsx new file mode 100644 index 000000000..466d5c31b --- /dev/null +++ b/frontend/src/Series/Details/SeasonProgressLabel.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import Label from 'Components/Label'; +import { kinds, sizes } from 'Helpers/Props'; +import createSeriesQueueItemsDetailsSelector, { + SeriesQueueDetails, +} from 'Series/Index/createSeriesQueueDetailsSelector'; + +function getEpisodeCountKind( + monitored: boolean, + episodeFileCount: number, + episodeCount: number, + isDownloading: boolean +) { + if (isDownloading) { + return kinds.PURPLE; + } + + if (episodeFileCount === episodeCount && episodeCount > 0) { + return kinds.SUCCESS; + } + + if (!monitored) { + return kinds.WARNING; + } + + return kinds.DANGER; +} + +interface SeasonProgressLabelProps { + seriesId: number; + seasonNumber: number; + monitored: boolean; + episodeCount: number; + episodeFileCount: number; +} + +function SeasonProgressLabel({ + seriesId, + seasonNumber, + monitored, + episodeCount, + episodeFileCount, +}: SeasonProgressLabelProps) { + const queueDetails: SeriesQueueDetails = useSelector( + createSeriesQueueItemsDetailsSelector(seriesId, seasonNumber) + ); + + const newDownloads = queueDetails.count - queueDetails.episodesWithFiles; + const text = newDownloads + ? `${episodeFileCount} + ${newDownloads} / ${episodeCount}` + : `${episodeFileCount} / ${episodeCount}`; + + return ( + <Label + kind={getEpisodeCountKind( + monitored, + episodeFileCount, + episodeCount, + queueDetails.count > 0 + )} + size={sizes.LARGE} + > + <span>{text}</span> + </Label> + ); +} + +export default SeasonProgressLabel; diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.js b/frontend/src/Series/Details/SeriesDetailsSeason.js index 4268dddff..9ec7edcc8 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.js +++ b/frontend/src/Series/Details/SeriesDetailsSeason.js @@ -2,7 +2,6 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Icon from 'Components/Icon'; -import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; @@ -15,7 +14,7 @@ import SpinnerIcon from 'Components/SpinnerIcon'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import Popover from 'Components/Tooltip/Popover'; -import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props'; +import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import SeriesHistoryModal from 'Series/History/SeriesHistoryModal'; @@ -27,6 +26,7 @@ import translate from 'Utilities/String/translate'; import getToggledRange from 'Utilities/Table/getToggledRange'; import EpisodeRowConnector from './EpisodeRowConnector'; import SeasonInfo from './SeasonInfo'; +import SeasonProgressLabel from './SeasonProgressLabel'; import styles from './SeriesDetailsSeason.css'; function getSeasonStatistics(episodes) { @@ -64,18 +64,6 @@ function getSeasonStatistics(episodes) { }; } -function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) { - if (episodeFileCount === episodeCount && episodeCount > 0) { - return kinds.SUCCESS; - } - - if (!monitored) { - return kinds.WARNING; - } - - return kinds.DANGER; -} - class SeriesDetailsSeason extends Component { // @@ -265,12 +253,13 @@ class SeriesDetailsSeason extends Component { className={styles.episodeCountTooltip} canFlip={true} anchor={ - <Label - kind={getEpisodeCountKind(monitored, episodeFileCount, episodeCount)} - size={sizes.LARGE} - > - <span>{episodeFileCount} / {episodeCount}</span> - </Label> + <SeasonProgressLabel + seriesId={seriesId} + seasonNumber={seasonNumber} + monitored={monitored} + episodeCount={episodeCount} + episodeFileCount={episodeFileCount} + /> } title={translate('SeasonInformation')} body={ From a83b5217666bde0b27676071d72c4abfd20c44ab Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 25 Jun 2024 06:25:33 -0700 Subject: [PATCH 359/762] New: 'On Import Complete' notification when all episodes in a release are imported Closes #363 --- .../Notifications/Notification.js | 16 ++- .../Notifications/NotificationEventItems.js | 15 +- .../Store/Actions/Settings/notifications.js | 1 + .../ImportFixture.cs | 4 + ...add_on_import_complete_to_notifications.cs | 14 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 1 + .../Download/CompletedDownloadService.cs | 29 +++- .../Download/DownloadCompletedEvent.cs | 13 +- .../TrackedDownloadAlreadyImported.cs | 2 +- .../UntrackedDownloadCompletedEvent.cs | 26 ++++ src/NzbDrone.Core/Localization/Core/en.json | 3 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../MediaFiles/EpisodeImport/ImportResult.cs | 12 +- .../Manual/ManualImportService.cs | 29 +++- .../Notifications/Apprise/Apprise.cs | 5 + .../CustomScript/CustomScript.cs | 61 ++++++++ .../Notifications/Email/Email.cs | 7 + .../Notifications/Gotify/Gotify.cs | 5 + .../Notifications/INotification.cs | 2 + .../Notifications/ImportCompleteMessage.cs | 29 ++++ src/NzbDrone.Core/Notifications/Join/Join.cs | 5 + .../Notifications/Mailgun/Mailgun.cs | 5 + .../MediaBrowser/MediaBrowser.cs | 13 ++ .../Notifications/Notifiarr/Notifiarr.cs | 5 + .../Notifications/NotificationBase.cs | 7 + .../Notifications/NotificationDefinition.cs | 6 +- .../Notifications/NotificationFactory.cs | 12 ++ .../Notifications/NotificationService.cs | 131 ++++++++++++++++-- src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs | 5 + .../Notifications/Plex/Server/PlexServer.cs | 5 + .../Notifications/Prowl/Prowl.cs | 5 + .../Notifications/PushBullet/PushBullet.cs | 5 + .../Notifications/Pushcut/Pushcut.cs | 5 + .../Notifications/Pushover/Pushover.cs | 5 + .../Notifications/SendGrid/SendGrid.cs | 5 + .../Notifications/Signal/Signal.cs | 5 + .../Notifications/Simplepush/Simplepush.cs | 5 + .../Notifications/Slack/Slack.cs | 17 +++ .../Notifications/Synology/SynologyIndexer.cs | 8 ++ .../Notifications/Telegram/Telegram.cs | 7 + .../Notifications/Trakt/Trakt.cs | 7 + .../Notifications/Twitter/Twitter.cs | 5 + .../Notifications/Webhook/Webhook.cs | 5 + .../Notifications/Webhook/WebhookBase.cs | 23 +++ .../Webhook/WebhookGrabbedRelease.cs | 19 ++- .../Webhook/WebhookImportCompletePayload.cs | 18 +++ src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 8 ++ .../Parser/Model/GrabbedReleaseInfo.cs | 4 + .../Notifications/NotificationResource.cs | 6 + 49 files changed, 597 insertions(+), 35 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/209_add_on_import_complete_to_notifications.cs create mode 100644 src/NzbDrone.Core/Download/UntrackedDownloadCompletedEvent.cs create mode 100644 src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs create mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookImportCompletePayload.cs diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index e7de0b308..22e17c18f 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -59,6 +59,7 @@ class Notification extends Component { onGrab, onDownload, onUpgrade, + onImportComplete, onRename, onSeriesAdd, onSeriesDelete, @@ -71,6 +72,7 @@ class Notification extends Component { supportsOnGrab, supportsOnDownload, supportsOnUpgrade, + supportsOnImportComplete, supportsOnRename, supportsOnSeriesAdd, supportsOnSeriesDelete, @@ -105,7 +107,7 @@ class Notification extends Component { { supportsOnDownload && onDownload ? <Label kind={kinds.SUCCESS}> - {translate('OnImport')} + {translate('OnFileImport')} </Label> : null } @@ -118,6 +120,14 @@ class Notification extends Component { null } + { + supportsOnImportComplete && onImportComplete ? + <Label kind={kinds.SUCCESS}> + {translate('OnImportComplete')} + </Label> : + null + } + { supportsOnRename && onRename ? <Label kind={kinds.SUCCESS}> @@ -191,7 +201,7 @@ class Notification extends Component { } { - !onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ? + !onGrab && !onDownload && !onRename && !onImportComplete && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ? <Label kind={kinds.DISABLED} outline={true} @@ -233,6 +243,7 @@ Notification.propTypes = { onGrab: PropTypes.bool.isRequired, onDownload: PropTypes.bool.isRequired, onUpgrade: PropTypes.bool.isRequired, + onImportComplete: PropTypes.bool.isRequired, onRename: PropTypes.bool.isRequired, onSeriesAdd: PropTypes.bool.isRequired, onSeriesDelete: PropTypes.bool.isRequired, @@ -244,6 +255,7 @@ Notification.propTypes = { onManualInteractionRequired: PropTypes.bool.isRequired, supportsOnGrab: PropTypes.bool.isRequired, supportsOnDownload: PropTypes.bool.isRequired, + supportsOnImportComplete: PropTypes.bool.isRequired, supportsOnSeriesAdd: PropTypes.bool.isRequired, supportsOnSeriesDelete: PropTypes.bool.isRequired, supportsOnEpisodeFileDelete: PropTypes.bool.isRequired, diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js index 4e5958250..ddcdae4f5 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js @@ -18,6 +18,7 @@ function NotificationEventItems(props) { onGrab, onDownload, onUpgrade, + onImportComplete, onRename, onSeriesAdd, onSeriesDelete, @@ -30,6 +31,7 @@ function NotificationEventItems(props) { supportsOnGrab, supportsOnDownload, supportsOnUpgrade, + supportsOnImportComplete, supportsOnRename, supportsOnSeriesAdd, supportsOnSeriesDelete, @@ -66,7 +68,7 @@ function NotificationEventItems(props) { <FormInputGroup type={inputTypes.CHECK} name="onDownload" - helpText={translate('OnImport')} + helpText={translate('OnFileImport')} isDisabled={!supportsOnDownload.value} {...onDownload} onChange={onInputChange} @@ -87,6 +89,17 @@ function NotificationEventItems(props) { </div> } + <div> + <FormInputGroup + type={inputTypes.CHECK} + name="onImportComplete" + helpText={translate('OnImportComplete')} + isDisabled={!supportsOnImportComplete.value} + {...onImportComplete} + onChange={onInputChange} + /> + </div> + <div> <FormInputGroup type={inputTypes.CHECK} diff --git a/frontend/src/Store/Actions/Settings/notifications.js b/frontend/src/Store/Actions/Settings/notifications.js index c9349df60..ad0943f2b 100644 --- a/frontend/src/Store/Actions/Settings/notifications.js +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -116,6 +116,7 @@ export default { selectedSchema.onGrab = selectedSchema.supportsOnGrab; selectedSchema.onDownload = selectedSchema.supportsOnDownload; selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; + selectedSchema.onImportComplete = selectedSchema.supportsOnImportComplete; selectedSchema.onRename = selectedSchema.supportsOnRename; selectedSchema.onSeriesAdd = selectedSchema.supportsOnSeriesAdd; selectedSchema.onSeriesDelete = selectedSchema.supportsOnSeriesDelete; diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs index ddb9fd8c6..56d64eb81 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs @@ -71,6 +71,10 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests Mocker.GetMock<IProvideImportItemService>() .Setup(s => s.ProvideImportItem(It.IsAny<DownloadClientItem>(), It.IsAny<DownloadClientItem>())) .Returns<DownloadClientItem, DownloadClientItem>((i, p) => i); + + Mocker.GetMock<IEpisodeService>() + .Setup(s => s.GetEpisodes(It.IsAny<IEnumerable<int>>())) + .Returns(new List<Episode>()); } private RemoteEpisode BuildRemoteEpisode() diff --git a/src/NzbDrone.Core/Datastore/Migration/209_add_on_import_complete_to_notifications.cs b/src/NzbDrone.Core/Datastore/Migration/209_add_on_import_complete_to_notifications.cs new file mode 100644 index 000000000..dbbc4bbef --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/209_add_on_import_complete_to_notifications.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(209)] + public class add_on_import_complete_to_notifications : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Notifications").AddColumn("OnImportComplete").AsBoolean().WithDefaultValue(false); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 26fa02c4b..b97a66eb8 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -89,6 +89,7 @@ namespace NzbDrone.Core.Datastore .Ignore(x => x.ImplementationName) .Ignore(i => i.SupportsOnGrab) .Ignore(i => i.SupportsOnDownload) + .Ignore(i => i.SupportsOnImportComplete) .Ignore(i => i.SupportsOnUpgrade) .Ignore(i => i.SupportsOnRename) .Ignore(i => i.SupportsOnSeriesAdd) diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 009cd7f65..b157012d3 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -34,6 +34,8 @@ namespace NzbDrone.Core.Download private readonly IParsingService _parsingService; private readonly ISeriesService _seriesService; private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported; + private readonly IEpisodeService _episodeService; + private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; public CompletedDownloadService(IEventAggregator eventAggregator, @@ -43,6 +45,8 @@ namespace NzbDrone.Core.Download IParsingService parsingService, ISeriesService seriesService, ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported, + IEpisodeService episodeService, + IMediaFileService mediaFileService, Logger logger) { _eventAggregator = eventAggregator; @@ -52,6 +56,8 @@ namespace NzbDrone.Core.Download _parsingService = parsingService; _seriesService = seriesService; _trackedDownloadAlreadyImported = trackedDownloadAlreadyImported; + _episodeService = episodeService; + _mediaFileService = mediaFileService; _logger = logger; } @@ -198,11 +204,23 @@ namespace NzbDrone.Core.Download .Count() >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count); + var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId) + .OrderByDescending(h => h.Date) + .ToList(); + + var grabbedHistory = historyItems.Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList(); + var releaseInfo = grabbedHistory.Count > 0 ? new GrabbedReleaseInfo(grabbedHistory) : null; + if (allEpisodesImported) { _logger.Debug("All episodes were imported for {0}", trackedDownload.DownloadItem.Title); trackedDownload.State = TrackedDownloadState.Imported; - _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id)); + + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, + trackedDownload.RemoteEpisode.Series.Id, + importResults.Where(c => c.Result == ImportResultType.Imported).Select(c => c.EpisodeFile).ToList(), + releaseInfo)); + return true; } @@ -216,12 +234,9 @@ namespace NzbDrone.Core.Download // safe, but commenting for future benefit. var atLeastOneEpisodeImported = importResults.Any(c => c.Result == ImportResultType.Imported); - - var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId) - .OrderByDescending(h => h.Date) - .ToList(); - var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); + var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id)); + var files = _mediaFileService.GetFiles(episodes.Select(e => e.EpisodeFileId).Distinct()); if (allEpisodesImportedInHistory) { @@ -245,7 +260,7 @@ namespace NzbDrone.Core.Download } trackedDownload.State = TrackedDownloadState.Imported; - _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id)); + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id, files, releaseInfo)); return true; } diff --git a/src/NzbDrone.Core/Download/DownloadCompletedEvent.cs b/src/NzbDrone.Core/Download/DownloadCompletedEvent.cs index f4debb147..c47f648d8 100644 --- a/src/NzbDrone.Core/Download/DownloadCompletedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadCompletedEvent.cs @@ -1,17 +1,24 @@ -using NzbDrone.Common.Messaging; +using System.Collections.Generic; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download { public class DownloadCompletedEvent : IEvent { public TrackedDownload TrackedDownload { get; private set; } - public int SeriesId { get; set; } + public int SeriesId { get; private set; } + public List<EpisodeFile> EpisodeFiles { get; private set; } + public GrabbedReleaseInfo Release { get; private set; } - public DownloadCompletedEvent(TrackedDownload trackedDownload, int seriesId) + public DownloadCompletedEvent(TrackedDownload trackedDownload, int seriesId, List<EpisodeFile> episodeFiles, GrabbedReleaseInfo release) { TrackedDownload = trackedDownload; SeriesId = seriesId; + EpisodeFiles = episodeFiles; + Release = release; } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs index 37f397f3b..b0f75be0f 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; diff --git a/src/NzbDrone.Core/Download/UntrackedDownloadCompletedEvent.cs b/src/NzbDrone.Core/Download/UntrackedDownloadCompletedEvent.cs new file mode 100644 index 000000000..7c37137f8 --- /dev/null +++ b/src/NzbDrone.Core/Download/UntrackedDownloadCompletedEvent.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Download +{ + public class UntrackedDownloadCompletedEvent : IEvent + { + public Series Series { get; private set; } + public List<Episode> Episodes { get; private set; } + public List<EpisodeFile> EpisodeFiles { get; private set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; private set; } + public string SourcePath { get; private set; } + + public UntrackedDownloadCompletedEvent(Series series, List<Episode> episodes, List<EpisodeFile> episodeFiles, ParsedEpisodeInfo parsedEpisodeInfo, string sourcePath) + { + Series = series; + Episodes = episodes; + EpisodeFiles = episodeFiles; + ParsedEpisodeInfo = parsedEpisodeInfo; + SourcePath = sourcePath; + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b60cecb2e..36e3c35c1 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1456,10 +1456,11 @@ "OnApplicationUpdate": "On Application Update", "OnEpisodeFileDelete": "On Episode File Delete", "OnEpisodeFileDeleteForUpgrade": "On Episode File Delete For Upgrade", + "OnFileImport": "On File Import", "OnGrab": "On Grab", "OnHealthIssue": "On Health Issue", "OnHealthRestored": "On Health Restored", - "OnImport": "On Import", + "OnImportComplete": "On Import Complete", "OnLatestVersion": "The latest version of {appName} is already installed", "OnManualInteractionRequired": "On Manual Interaction Required", "OnRename": "On Rename", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 39c3c849f..739620039 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -171,7 +171,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } episodeFile = _mediaFileService.Add(episodeFile); - importResults.Add(new ImportResult(importDecision)); + importResults.Add(new ImportResult(importDecision, episodeFile)); if (newDownload) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs index a0d989335..6bb715689 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnsureThat; @@ -7,6 +7,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportResult { public ImportDecision ImportDecision { get; private set; } + public EpisodeFile EpisodeFile { get; private set; } public List<string> Errors { get; private set; } public ImportResultType Result @@ -34,5 +35,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport ImportDecision = importDecision; Errors = errors.ToList(); } + + public ImportResult(ImportDecision importDecision, EpisodeFile episodeFile) + { + Ensure.That(importDecision, () => importDecision).IsNotNull(); + + ImportDecision = importDecision; + EpisodeFile = episodeFile; + Errors = new List<string>(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 91599f843..c413172aa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -546,6 +546,24 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _logger.ProgressTrace("Manually imported {0} files", imported.Count); } + var untrackedImports = imported.Where(i => importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList(); + + if (untrackedImports.Any()) + { + foreach (var groupedUntrackedImport in untrackedImports.GroupBy(i => new { i.EpisodeFile.SeriesId, i.EpisodeFile.SeasonNumber })) + { + var localEpisodes = groupedUntrackedImport.Select(u => u.ImportDecision.LocalEpisode).ToList(); + var episodeFiles = groupedUntrackedImport.Select(u => u.EpisodeFile).ToList(); + var localEpisode = localEpisodes.First(); + var series = localEpisode.Series; + var sourcePath = localEpisodes.Select(l => l.Path).ToList().GetLongestCommonPath(); + var episodes = localEpisodes.SelectMany(l => l.Episodes).ToList(); + var parsedEpisodeInfo = localEpisode.FolderEpisodeInfo ?? localEpisode.FileEpisodeInfo; + + _eventAggregator.PublishEvent(new UntrackedDownloadCompletedEvent(series, episodes, episodeFiles, parsedEpisodeInfo, sourcePath)); + } + } + foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) { var trackedDownload = groupedTrackedDownload.First().TrackedDownload; @@ -562,15 +580,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual } } - var allEpisodesImported = groupedTrackedDownload.Select(c => c.ImportResult) - .Where(c => c.Result == ImportResultType.Imported) + var importedResults = groupedTrackedDownload.Select(c => c.ImportResult) + .Where(c => c.Result == ImportResultType.Imported) + .ToList(); + + var allEpisodesImported = importedResults .SelectMany(c => c.ImportDecision.LocalEpisode.Episodes).Count() >= Math.Max(1, trackedDownload.RemoteEpisode?.Episodes?.Count ?? 1); if (allEpisodesImported) { + var episodeFiles = importedResults.Select(i => i.EpisodeFile).ToList(); + trackedDownload.State = TrackedDownloadState.Imported; - _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, importedSeries.Id)); + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, importedSeries.Id, episodeFiles, importedResults.First().ImportDecision.LocalEpisode.Release)); } } } diff --git a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs index db39aadca..04d08901f 100644 --- a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs +++ b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs @@ -27,6 +27,11 @@ namespace NzbDrone.Core.Notifications.Apprise _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 1df4f95a8..f1b344101 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -96,6 +96,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); environmentVariables.Add("Sonarr_Release_CustomFormat", string.Join("|", remoteEpisode.CustomFormats)); environmentVariables.Add("Sonarr_Release_CustomFormatScore", remoteEpisode.CustomFormatScore.ToString()); + environmentVariables.Add("Sonarr_Release_ReleaseType", remoteEpisode.ParsedEpisodeInfo.ReleaseType.ToString()); ExecuteScript(environmentVariables); } @@ -158,6 +159,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Release_Indexer", message.Release?.Indexer); environmentVariables.Add("Sonarr_Release_Size", message.Release?.Size.ToString()); environmentVariables.Add("Sonarr_Release_Title", message.Release?.Title); + environmentVariables.Add("Sonarr_Release_ReleaseType", message.Release?.ReleaseType.ToString() ?? string.Empty); if (message.OldFiles.Any()) { @@ -170,6 +172,65 @@ namespace NzbDrone.Core.Notifications.CustomScript ExecuteScript(environmentVariables); } + public override void OnImportComplete(ImportCompleteMessage message) + { + var series = message.Series; + var episodes = message.Episodes; + var episodeFiles = message.EpisodeFiles; + var sourcePath = message.SourcePath; + var environmentVariables = new StringDictionary(); + + environmentVariables.Add("Sonarr_EventType", "Download"); + environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); + environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); + environmentVariables.Add("Sonarr_Series_Id", series.Id.ToString()); + environmentVariables.Add("Sonarr_Series_Title", series.Title); + environmentVariables.Add("Sonarr_Series_TitleSlug", series.TitleSlug); + environmentVariables.Add("Sonarr_Series_Path", series.Path); + environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); + environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); + environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); + environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); + environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); + environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); + environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); + environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); + environmentVariables.Add("Sonarr_EpisodeFile_Ids", string.Join("|", episodeFiles.Select(f => f.Id))); + environmentVariables.Add("Sonarr_EpisodeFile_Count", message.EpisodeFiles.Count.ToString()); + environmentVariables.Add("Sonarr_EpisodeFile_RelativePaths", string.Join("|", episodeFiles.Select(f => f.RelativePath))); + environmentVariables.Add("Sonarr_EpisodeFile_Paths", string.Join("|", episodeFiles.Select(f => Path.Combine(series.Path, f.RelativePath)))); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeIds", string.Join(",", episodes.Select(e => e.Id))); + environmentVariables.Add("Sonarr_EpisodeFile_SeasonNumber", episodes.First().SeasonNumber.ToString()); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeNumbers", string.Join(",", episodes.Select(e => e.EpisodeNumber))); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDates", string.Join(",", episodes.Select(e => e.AirDate))); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodes.Select(e => e.AirDateUtc))); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeTitles", string.Join("|", episodes.Select(e => e.Title))); + environmentVariables.Add("Sonarr_EpisodeFile_EpisodeOverviews", string.Join("|", episodes.Select(e => e.Overview))); + environmentVariables.Add("Sonarr_EpisodeFile_Qualities", string.Join("|", episodeFiles.Select(f => f.Quality.Quality.Name))); + environmentVariables.Add("Sonarr_EpisodeFile_QualityVersions", string.Join("|", episodeFiles.Select(f => f.Quality.Revision.Version))); + environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroups", string.Join("|", episodeFiles.Select(f => f.ReleaseGroup))); + environmentVariables.Add("Sonarr_EpisodeFile_SceneNames", string.Join("|", episodeFiles.Select(f => f.SceneName))); + environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientInfo?.Name ?? string.Empty); + environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientInfo?.Type ?? string.Empty); + environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); + environmentVariables.Add("Sonarr_Release_Group", message.ReleaseGroup ?? string.Empty); + environmentVariables.Add("Sonarr_Release_Quality", message.ReleaseQuality.Quality.Name); + environmentVariables.Add("Sonarr_Release_QualityVersion", message.ReleaseQuality.Revision.Version.ToString()); + environmentVariables.Add("Sonarr_Release_Indexer", message.Release?.Indexer ?? string.Empty); + environmentVariables.Add("Sonarr_Release_Size", message.Release?.Size.ToString() ?? string.Empty); + environmentVariables.Add("Sonarr_Release_Title", message.Release?.Title ?? string.Empty); + + // Prefer the release type from the release, otherwise use the first imported file (useful for untracked manual imports) + environmentVariables.Add("Sonarr_Release_ReleaseType", message.Release == null ? message.EpisodeFiles.First().ReleaseType.ToString() : message.Release.ReleaseType.ToString()); + environmentVariables.Add("Sonarr_SourcePath", sourcePath); + environmentVariables.Add("Sonarr_SourceFolder", Path.GetDirectoryName(sourcePath)); + environmentVariables.Add("Sonarr_DestinationPath", message.DestinationPath); + environmentVariables.Add("Sonarr_DestinationFolder", Path.GetDirectoryName(message.DestinationPath)); + + ExecuteScript(environmentVariables); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { var environmentVariables = new StringDictionary(); diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index 941754c34..7ade7dd99 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -43,6 +43,13 @@ namespace NzbDrone.Core.Notifications.Email SendEmail(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, body); } + public override void OnImportComplete(ImportCompleteMessage message) + { + var body = $"All expected episode files in {message.Message} downloaded and sorted."; + + SendEmail(Settings, IMPORT_COMPLETE_TITLE, body); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var body = $"{deleteMessage.Message} deleted."; diff --git a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs index b315ec352..47060065c 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs @@ -36,6 +36,11 @@ namespace NzbDrone.Core.Notifications.Gotify SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, message.Series); } + public override void OnImportComplete(ImportCompleteMessage message) + { + SendNotification(IMPORT_COMPLETE_TITLE, message.Message, message.Series); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage message) { SendNotification(EPISODE_DELETED_TITLE, message.Message, message.Series); diff --git a/src/NzbDrone.Core/Notifications/INotification.cs b/src/NzbDrone.Core/Notifications/INotification.cs index 52aafb378..5b03a51f4 100644 --- a/src/NzbDrone.Core/Notifications/INotification.cs +++ b/src/NzbDrone.Core/Notifications/INotification.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Notifications void OnGrab(GrabMessage grabMessage); void OnDownload(DownloadMessage message); void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles); + void OnImportComplete(ImportCompleteMessage message); void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage); void OnSeriesAdd(SeriesAddMessage message); void OnSeriesDelete(SeriesDeleteMessage deleteMessage); @@ -23,6 +24,7 @@ namespace NzbDrone.Core.Notifications bool SupportsOnGrab { get; } bool SupportsOnDownload { get; } bool SupportsOnUpgrade { get; } + bool SupportsOnImportComplete { get; } bool SupportsOnRename { get; } bool SupportsOnSeriesAdd { get; } bool SupportsOnSeriesDelete { get; } diff --git a/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs b/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs new file mode 100644 index 000000000..f68fb40d2 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications +{ + public class ImportCompleteMessage + { + public string Message { get; set; } + public Series Series { get; set; } + public List<Episode> Episodes { get; set; } + public List<EpisodeFile> EpisodeFiles { get; set; } + public string SourcePath { get; set; } + public DownloadClientItemClientInfo DownloadClientInfo { get; set; } + public string DownloadId { get; set; } + public GrabbedReleaseInfo Release { get; set; } + public string DestinationPath { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel ReleaseQuality { get; set; } + + public override string ToString() + { + return Message; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Join/Join.cs b/src/NzbDrone.Core/Notifications/Join/Join.cs index 5b5f3cbb5..f99b6e259 100644 --- a/src/NzbDrone.Core/Notifications/Join/Join.cs +++ b/src/NzbDrone.Core/Notifications/Join/Join.cs @@ -27,6 +27,11 @@ namespace NzbDrone.Core.Notifications.Join _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE_BRANDED, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE_BRANDED, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs b/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs index 9706d8ebb..e2d883c7e 100644 --- a/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs +++ b/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs @@ -32,6 +32,11 @@ namespace NzbDrone.Core.Notifications.Mailgun _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, downloadMessage.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var body = $"{deleteMessage.Message} deleted."; diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index 7ea4362ed..5ab3b0764 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -39,6 +39,19 @@ namespace NzbDrone.Core.Notifications.Emby } } + public override void OnImportComplete(ImportCompleteMessage message) + { + if (Settings.Notify) + { + _mediaBrowserService.Notify(Settings, IMPORT_COMPLETE_TITLE_BRANDED, message.Message); + } + + if (Settings.UpdateLibrary) + { + _mediaBrowserService.Update(Settings, message.Series, "Created"); + } + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { if (Settings.UpdateLibrary) diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs index 498a4724e..279ec5930 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs @@ -35,6 +35,11 @@ namespace NzbDrone.Core.Notifications.Notifiarr _proxy.SendNotification(BuildOnDownloadPayload(message), Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(BuildOnImportCompletePayload(message), Settings); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { _proxy.SendNotification(BuildOnRenamePayload(series, renamedFiles), Settings); diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs index 7c5806833..3d8b65994 100644 --- a/src/NzbDrone.Core/Notifications/NotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Notifications { protected const string EPISODE_GRABBED_TITLE = "Episode Grabbed"; protected const string EPISODE_DOWNLOADED_TITLE = "Episode Downloaded"; + protected const string IMPORT_COMPLETE_TITLE = "Import Complete"; protected const string EPISODE_DELETED_TITLE = "Episode Deleted"; protected const string SERIES_ADDED_TITLE = "Series Added"; protected const string SERIES_DELETED_TITLE = "Series Deleted"; @@ -22,6 +23,7 @@ namespace NzbDrone.Core.Notifications protected const string EPISODE_GRABBED_TITLE_BRANDED = "Sonarr - " + EPISODE_GRABBED_TITLE; protected const string EPISODE_DOWNLOADED_TITLE_BRANDED = "Sonarr - " + EPISODE_DOWNLOADED_TITLE; + protected const string IMPORT_COMPLETE_TITLE_BRANDED = "Sonarr - " + IMPORT_COMPLETE_TITLE; protected const string EPISODE_DELETED_TITLE_BRANDED = "Sonarr - " + EPISODE_DELETED_TITLE; protected const string SERIES_ADDED_TITLE_BRANDED = "Sonarr - " + SERIES_ADDED_TITLE; protected const string SERIES_DELETED_TITLE_BRANDED = "Sonarr - " + SERIES_DELETED_TITLE; @@ -51,6 +53,10 @@ namespace NzbDrone.Core.Notifications { } + public virtual void OnImportComplete(ImportCompleteMessage message) + { + } + public virtual void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { } @@ -91,6 +97,7 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnRename => HasConcreteImplementation("OnRename"); public bool SupportsOnDownload => HasConcreteImplementation("OnDownload"); public bool SupportsOnUpgrade => SupportsOnDownload; + public bool SupportsOnImportComplete => HasConcreteImplementation("OnImportComplete"); public bool SupportsOnSeriesAdd => HasConcreteImplementation("OnSeriesAdd"); public bool SupportsOnSeriesDelete => HasConcreteImplementation("OnSeriesDelete"); public bool SupportsOnEpisodeFileDelete => HasConcreteImplementation("OnEpisodeFileDelete"); diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index d848777e1..6ca3cf6a0 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications public bool OnGrab { get; set; } public bool OnDownload { get; set; } public bool OnUpgrade { get; set; } + public bool OnImportComplete { get; set; } public bool OnRename { get; set; } public bool OnSeriesAdd { get; set; } public bool OnSeriesDelete { get; set; } @@ -34,6 +35,9 @@ namespace NzbDrone.Core.Notifications [MemberwiseEqualityIgnore] public bool SupportsOnRename { get; set; } + [MemberwiseEqualityIgnore] + public bool SupportsOnImportComplete { get; set; } + [MemberwiseEqualityIgnore] public bool SupportsOnSeriesAdd { get; set; } @@ -59,7 +63,7 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnManualInteractionRequired { get; set; } [MemberwiseEqualityIgnore] - public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnRename || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; + public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnImportComplete || OnRename || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired; public bool Equals(NotificationDefinition other) { diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index 35823feca..e4230fefe 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Notifications List<INotification> OnGrabEnabled(bool filterBlockedNotifications = true); List<INotification> OnDownloadEnabled(bool filterBlockedNotifications = true); List<INotification> OnUpgradeEnabled(bool filterBlockedNotifications = true); + List<INotification> OnImportCompleteEnabled(bool filterBlockedNotifications = true); List<INotification> OnRenameEnabled(bool filterBlockedNotifications = true); List<INotification> OnSeriesAddEnabled(bool filterBlockedNotifications = true); List<INotification> OnSeriesDeleteEnabled(bool filterBlockedNotifications = true); @@ -71,6 +72,16 @@ namespace NzbDrone.Core.Notifications return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnUpgrade).ToList(); } + public List<INotification> OnImportCompleteEnabled(bool filterBlockedNotifications = true) + { + if (filterBlockedNotifications) + { + return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnImportComplete)).ToList(); + } + + return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnImportComplete).ToList(); + } + public List<INotification> OnRenameEnabled(bool filterBlockedNotifications = true) { if (filterBlockedNotifications) @@ -184,6 +195,7 @@ namespace NzbDrone.Core.Notifications definition.SupportsOnGrab = provider.SupportsOnGrab; definition.SupportsOnDownload = provider.SupportsOnDownload; definition.SupportsOnUpgrade = provider.SupportsOnUpgrade; + definition.SupportsOnImportComplete = provider.SupportsOnImportComplete; definition.SupportsOnRename = provider.SupportsOnRename; definition.SupportsOnSeriesAdd = provider.SupportsOnSeriesAdd; definition.SupportsOnSeriesDelete = provider.SupportsOnSeriesDelete; diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index fc9c3e865..88e19f423 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -18,6 +19,8 @@ namespace NzbDrone.Core.Notifications public class NotificationService : IHandle<EpisodeGrabbedEvent>, IHandle<EpisodeImportedEvent>, + IHandle<DownloadCompletedEvent>, + IHandle<UntrackedDownloadCompletedEvent>, IHandle<SeriesRenamedEvent>, IHandle<SeriesAddCompletedEvent>, IHandle<SeriesDeletedEvent>, @@ -44,19 +47,7 @@ namespace NzbDrone.Core.Notifications private string GetMessage(Series series, List<Episode> episodes, QualityModel quality) { - var qualityString = quality.Quality.ToString(); - - if (quality.Revision.Version > 1) - { - if (series.SeriesType == SeriesTypes.Anime) - { - qualityString += " v" + quality.Revision.Version; - } - else - { - qualityString += " Proper"; - } - } + var qualityString = GetQualityString(series, quality); if (series.SeriesType == SeriesTypes.Daily) { @@ -82,6 +73,35 @@ namespace NzbDrone.Core.Notifications qualityString); } + private string GetFullSeasonMessage(Series series, int seasonNumber, QualityModel quality) + { + var qualityString = GetQualityString(series, quality); + + return string.Format("{0} - Season {1} [{2}]", + series.Title, + seasonNumber, + qualityString); + } + + private string GetQualityString(Series series, QualityModel quality) + { + var qualityString = quality.Quality.ToString(); + + if (quality.Revision.Version > 1) + { + if (series.SeriesType == SeriesTypes.Anime) + { + qualityString += " v" + quality.Revision.Version; + } + else + { + qualityString += " Proper"; + } + } + + return qualityString; + } + private bool ShouldHandleSeries(ProviderDefinition definition, Series series) { if (definition.Tags.Empty()) @@ -189,6 +209,91 @@ namespace NzbDrone.Core.Notifications } } + public void Handle(DownloadCompletedEvent message) + { + var series = message.TrackedDownload.RemoteEpisode.Series; + var episodes = message.TrackedDownload.RemoteEpisode.Episodes; + var parsedEpisodeInfo = message.TrackedDownload.RemoteEpisode.ParsedEpisodeInfo; + + var downloadMessage = new ImportCompleteMessage + { + Message = parsedEpisodeInfo.FullSeason + ? GetFullSeasonMessage(series, episodes.First().SeasonNumber, parsedEpisodeInfo.Quality) + : GetMessage(series, episodes, parsedEpisodeInfo.Quality), + Series = series, + Episodes = episodes, + EpisodeFiles = message.EpisodeFiles, + DownloadClientInfo = message.TrackedDownload.DownloadItem.DownloadClientInfo, + DownloadId = message.TrackedDownload.DownloadItem.DownloadId, + Release = message.Release, + SourcePath = message.TrackedDownload.DownloadItem.OutputPath.FullPath, + DestinationPath = message.EpisodeFiles.Select(e => Path.Join(series.Path, e.RelativePath)).ToList().GetLongestCommonPath(), + ReleaseGroup = parsedEpisodeInfo.ReleaseGroup, + ReleaseQuality = parsedEpisodeInfo.Quality + }; + + foreach (var notification in _notificationFactory.OnImportCompleteEnabled()) + { + try + { + if (ShouldHandleSeries(notification.Definition, series)) + { + if (((NotificationDefinition)notification.Definition).OnImportComplete) + { + notification.OnImportComplete(downloadMessage); + _notificationStatusService.RecordSuccess(notification.Definition.Id); + } + } + } + catch (Exception ex) + { + _notificationStatusService.RecordFailure(notification.Definition.Id); + _logger.Warn(ex, "Unable to send OnImportComplete notification to: " + notification.Definition.Name); + } + } + } + + public void Handle(UntrackedDownloadCompletedEvent message) + { + var series = message.Series; + var episodes = message.Episodes; + var parsedEpisodeInfo = message.ParsedEpisodeInfo; + + var downloadMessage = new ImportCompleteMessage + { + Message = parsedEpisodeInfo.FullSeason + ? GetFullSeasonMessage(series, episodes.First().SeasonNumber, parsedEpisodeInfo.Quality) + : GetMessage(series, episodes, parsedEpisodeInfo.Quality), + Series = series, + Episodes = episodes, + EpisodeFiles = message.EpisodeFiles, + SourcePath = message.SourcePath, + DestinationPath = message.EpisodeFiles.Select(e => Path.Join(series.Path, e.RelativePath)).ToList().GetLongestCommonPath(), + ReleaseGroup = parsedEpisodeInfo.ReleaseGroup, + ReleaseQuality = parsedEpisodeInfo.Quality + }; + + foreach (var notification in _notificationFactory.OnImportCompleteEnabled()) + { + try + { + if (ShouldHandleSeries(notification.Definition, series)) + { + if (((NotificationDefinition)notification.Definition).OnImportComplete) + { + notification.OnImportComplete(downloadMessage); + _notificationStatusService.RecordSuccess(notification.Definition.Id); + } + } + } + catch (Exception ex) + { + _notificationStatusService.RecordFailure(notification.Definition.Id); + _logger.Warn(ex, "Unable to send OnImportComplete notification to: " + notification.Definition.Name); + } + } + } + public void Handle(SeriesRenamedEvent message) { foreach (var notification in _notificationFactory.OnRenameEnabled()) diff --git a/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs b/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs index 27cd24065..11502d27f 100644 --- a/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs +++ b/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs @@ -28,6 +28,11 @@ namespace NzbDrone.Core.Notifications.Ntfy _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE_BRANDED, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs index 46fb118c1..ab8079f7c 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -45,6 +45,11 @@ namespace NzbDrone.Core.Notifications.Plex.Server UpdateIfEnabled(message.Series); } + public override void OnImportComplete(ImportCompleteMessage message) + { + UpdateIfEnabled(message.Series); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { UpdateIfEnabled(series); diff --git a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs index 5382f93df..538f34f03 100644 --- a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs +++ b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs @@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Prowl _prowlProxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _prowlProxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _prowlProxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs index 6a91070f2..4751e6a67 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs @@ -29,6 +29,11 @@ namespace NzbDrone.Core.Notifications.PushBullet _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE_BRANDED, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs index 07d3269eb..9f9ff55a4 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs @@ -36,6 +36,11 @@ namespace NzbDrone.Core.Notifications.Pushcut _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, downloadMessage.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs index 5603d90a7..467eaf093 100644 --- a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs +++ b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs @@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Pushover _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs b/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs index d05672d24..847435254 100644 --- a/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs +++ b/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs @@ -32,6 +32,11 @@ namespace NzbDrone.Core.Notifications.SendGrid _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Signal/Signal.cs b/src/NzbDrone.Core/Notifications/Signal/Signal.cs index 7b4e170fd..81615f6cd 100644 --- a/src/NzbDrone.Core/Notifications/Signal/Signal.cs +++ b/src/NzbDrone.Core/Notifications/Signal/Signal.cs @@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Signal _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs b/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs index 8b7c38d6c..caa44ff0f 100644 --- a/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs +++ b/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs @@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Simplepush _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendNotification(IMPORT_COMPLETE_TITLE, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 2fe412f2b..eb6bfb636 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -59,6 +59,23 @@ namespace NzbDrone.Core.Notifications.Slack _proxy.SendPayload(payload, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + var attachments = new List<Attachment> + { + new Attachment + { + Fallback = message.Message, + Title = message.Series.Title, + Text = message.Message, + Color = "good" + } + }; + var payload = CreatePayload($"Imported all expected episodes: {message.Message}", attachments); + + _proxy.SendPayload(payload, Settings); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { var attachments = new List<Attachment> diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index 733eaa5e4..bc1cb8efa 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -42,6 +42,14 @@ namespace NzbDrone.Core.Notifications.Synology } } + public override void OnImportComplete(ImportCompleteMessage message) + { + if (Settings.UpdateLibrary) + { + _indexerProxy.UpdateFolder(message.Series.Path); + } + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { if (Settings.UpdateLibrary) diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 1edbfa909..3a8513e27 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -30,6 +30,13 @@ namespace NzbDrone.Core.Notifications.Telegram _proxy.SendNotification(title, message.Message, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE; diff --git a/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs index 9a4362e2f..5dc5123c7 100644 --- a/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs +++ b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs @@ -39,6 +39,13 @@ namespace NzbDrone.Core.Notifications.Trakt AddEpisodeToCollection(Settings, message.Series, message.EpisodeFile); } + public override void OnImportComplete(ImportCompleteMessage message) + { + RefreshTokenIfNecessary(); + + message.EpisodeFiles.ForEach(f => AddEpisodeToCollection(Settings, message.Series, f)); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { RefreshTokenIfNecessary(); diff --git a/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs b/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs index 33ce77f49..15547cf01 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs @@ -28,6 +28,11 @@ namespace NzbDrone.Core.Notifications.Twitter _twitterService.SendNotification($"Imported: {message.Message}", Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _twitterService.SendNotification($"Imported: {message.Message}", Settings); + } + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { _twitterService.SendNotification($"Episode Deleted: {deleteMessage.Message}", Settings); diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs index 1e09e8e13..da30061da 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -33,6 +33,11 @@ namespace NzbDrone.Core.Notifications.Webhook _proxy.SendWebhook(BuildOnDownloadPayload(message), Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + _proxy.SendWebhook(BuildOnImportCompletePayload(message), Settings); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { _proxy.SendWebhook(BuildOnRenamePayload(series, renamedFiles), Settings); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index d87e92a42..558a6114f 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -81,6 +81,29 @@ namespace NzbDrone.Core.Notifications.Webhook return payload; } + protected WebhookImportCompletePayload BuildOnImportCompletePayload(ImportCompleteMessage message) + { + var episodeFiles = message.EpisodeFiles; + + var payload = new WebhookImportCompletePayload + { + EventType = WebhookEventType.Download, + InstanceName = _configFileProvider.InstanceName, + ApplicationUrl = _configService.ApplicationUrl, + Series = GetSeries(message.Series), + Episodes = message.Episodes.ConvertAll(x => new WebhookEpisode(x)), + EpisodeFiles = episodeFiles.ConvertAll(e => new WebhookEpisodeFile(e)), + Release = new WebhookGrabbedRelease(message.Release, episodeFiles.First().ReleaseType), + DownloadClient = message.DownloadClientInfo?.Name, + DownloadClientType = message.DownloadClientInfo?.Type, + DownloadId = message.DownloadId, + SourcePath = message.SourcePath, + DestinationPath = message.DestinationPath + }; + + return payload; + } + protected WebhookEpisodeDeletePayload BuildOnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { return new WebhookEpisodeDeletePayload diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabbedRelease.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabbedRelease.cs index bcce1af60..74f28d6c7 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabbedRelease.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabbedRelease.cs @@ -18,10 +18,27 @@ namespace NzbDrone.Core.Notifications.Webhook ReleaseTitle = release.Title; Indexer = release.Indexer; Size = release.Size; + ReleaseType = release.ReleaseType; + } + + public WebhookGrabbedRelease(GrabbedReleaseInfo release, ReleaseType releaseType) + { + if (release == null) + { + ReleaseType = releaseType; + + return; + } + + ReleaseTitle = release.Title; + Indexer = release.Indexer; + Size = release.Size; + ReleaseType = release.ReleaseType; } public string ReleaseTitle { get; set; } public string Indexer { get; set; } - public long Size { get; set; } + public long? Size { get; set; } + public ReleaseType ReleaseType { get; set; } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookImportCompletePayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportCompletePayload.cs new file mode 100644 index 000000000..a6722e6ac --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportCompletePayload.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookImportCompletePayload : WebhookPayload + { + public WebhookSeries Series { get; set; } + public List<WebhookEpisode> Episodes { get; set; } + public List<WebhookEpisodeFile> EpisodeFiles { get; set; } + public string DownloadClient { get; set; } + public string DownloadClientType { get; set; } + public string DownloadId { get; set; } + public WebhookGrabbedRelease Release { get; set; } + public int FileCount => EpisodeFiles.Count; + public string SourcePath { get; set; } + public string DestinationPath { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index f9fe7c142..1bff9d178 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -37,6 +37,14 @@ namespace NzbDrone.Core.Notifications.Xbmc UpdateAndClean(message.Series, message.OldFiles.Any()); } + public override void OnImportComplete(ImportCompleteMessage message) + { + const string header = "Sonarr - Imported"; + + Notify(Settings, header, message.Message); + UpdateAndClean(message.Series); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { UpdateAndClean(series); diff --git a/src/NzbDrone.Core/Parser/Model/GrabbedReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/GrabbedReleaseInfo.cs index 5ac847bea..4496171de 100644 --- a/src/NzbDrone.Core/Parser/Model/GrabbedReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/GrabbedReleaseInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.History; @@ -9,6 +10,7 @@ namespace NzbDrone.Core.Parser.Model public string Title { get; set; } public string Indexer { get; set; } public long Size { get; set; } + public ReleaseType ReleaseType { get; set; } public List<int> EpisodeIds { get; set; } @@ -19,12 +21,14 @@ namespace NzbDrone.Core.Parser.Model grabbedHistory.Data.TryGetValue("indexer", out var indexer); grabbedHistory.Data.TryGetValue("size", out var sizeString); + Enum.TryParse(grabbedHistory.Data.GetValueOrDefault("releaseType"), out ReleaseType releaseType); long.TryParse(sizeString, out var size); Title = grabbedHistory.SourceTitle; Indexer = indexer; Size = size; EpisodeIds = episodeIds; + ReleaseType = releaseType; } } } diff --git a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs index 6e31390cd..3ada87e92 100644 --- a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs +++ b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs @@ -8,6 +8,7 @@ namespace Sonarr.Api.V3.Notifications public bool OnGrab { get; set; } public bool OnDownload { get; set; } public bool OnUpgrade { get; set; } + public bool OnImportComplete { get; set; } public bool OnRename { get; set; } public bool OnSeriesAdd { get; set; } public bool OnSeriesDelete { get; set; } @@ -21,6 +22,7 @@ namespace Sonarr.Api.V3.Notifications public bool SupportsOnGrab { get; set; } public bool SupportsOnDownload { get; set; } public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnImportComplete { get; set; } public bool SupportsOnRename { get; set; } public bool SupportsOnSeriesAdd { get; set; } public bool SupportsOnSeriesDelete { get; set; } @@ -47,6 +49,7 @@ namespace Sonarr.Api.V3.Notifications resource.OnGrab = definition.OnGrab; resource.OnDownload = definition.OnDownload; resource.OnUpgrade = definition.OnUpgrade; + resource.OnImportComplete = definition.OnImportComplete; resource.OnRename = definition.OnRename; resource.OnSeriesAdd = definition.OnSeriesAdd; resource.OnSeriesDelete = definition.OnSeriesDelete; @@ -60,6 +63,7 @@ namespace Sonarr.Api.V3.Notifications resource.SupportsOnGrab = definition.SupportsOnGrab; resource.SupportsOnDownload = definition.SupportsOnDownload; resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; + resource.SupportsOnImportComplete = definition.SupportsOnImportComplete; resource.SupportsOnRename = definition.SupportsOnRename; resource.SupportsOnSeriesAdd = definition.SupportsOnSeriesAdd; resource.SupportsOnSeriesDelete = definition.SupportsOnSeriesDelete; @@ -85,6 +89,7 @@ namespace Sonarr.Api.V3.Notifications definition.OnGrab = resource.OnGrab; definition.OnDownload = resource.OnDownload; definition.OnUpgrade = resource.OnUpgrade; + definition.OnImportComplete = resource.OnImportComplete; definition.OnRename = resource.OnRename; definition.OnSeriesAdd = resource.OnSeriesAdd; definition.OnSeriesDelete = resource.OnSeriesDelete; @@ -98,6 +103,7 @@ namespace Sonarr.Api.V3.Notifications definition.SupportsOnGrab = resource.SupportsOnGrab; definition.SupportsOnDownload = resource.SupportsOnDownload; definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; + definition.SupportsOnImportComplete = resource.SupportsOnImportComplete; definition.SupportsOnRename = resource.SupportsOnRename; definition.SupportsOnSeriesAdd = resource.SupportsOnSeriesAdd; definition.SupportsOnSeriesDelete = resource.SupportsOnSeriesDelete; From 46c7de379c872f757847a311b21714e905466360 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 6 Jul 2024 15:57:14 -0700 Subject: [PATCH 360/762] New: Group updates for the same series for Kodi and Emby / Jellyfin --- .../Xbmc/OnDownloadFixture.cs | 4 + .../MediaBrowser/MediaBrowser.cs | 71 +++++++----- .../Notifications/MediaServerUpdateQueue.cs | 105 ++++++++++++++++++ .../Notifications/Plex/Server/PlexServer.cs | 77 ++----------- src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 52 ++++++--- 5 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs index 09063bc4d..729c2e326 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc Subject.Definition = new NotificationDefinition(); Subject.Definition.Settings = new XbmcSettings { + Host = "localhost", UpdateLibrary = true }; } @@ -49,6 +50,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc Subject.Definition.Settings = new XbmcSettings { + Host = "localhost", UpdateLibrary = true, CleanLibrary = true }; @@ -58,6 +60,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc public void should_not_clean_if_no_episode_was_replaced() { Subject.OnDownload(_downloadMessage); + Subject.ProcessQueue(); Mocker.GetMock<IXbmcService>().Verify(v => v.Clean(It.IsAny<XbmcSettings>()), Times.Never()); } @@ -67,6 +70,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc { GivenOldFiles(); Subject.OnDownload(_downloadMessage); + Subject.ProcessQueue(); Mocker.GetMock<IXbmcService>().Verify(v => v.Clean(It.IsAny<XbmcSettings>()), Times.Once()); } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index 5ab3b0764..bcdbe237c 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using System.Linq; using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -9,10 +12,18 @@ namespace NzbDrone.Core.Notifications.Emby public class MediaBrowser : NotificationBase<MediaBrowserSettings> { private readonly IMediaBrowserService _mediaBrowserService; + private readonly MediaServerUpdateQueue<MediaBrowser, string> _updateQueue; + private readonly Logger _logger; - public MediaBrowser(IMediaBrowserService mediaBrowserService) + private static string Created = "Created"; + private static string Deleted = "Deleted"; + private static string Modified = "Modified"; + + public MediaBrowser(IMediaBrowserService mediaBrowserService, ICacheManager cacheManager, Logger logger) { _mediaBrowserService = mediaBrowserService; + _updateQueue = new MediaServerUpdateQueue<MediaBrowser, string>(cacheManager); + _logger = logger; } public override string Link => "https://emby.media/"; @@ -33,10 +44,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnImportComplete(ImportCompleteMessage message) @@ -46,18 +54,12 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, IMPORT_COMPLETE_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, series, "Modified"); - } + UpdateIfEnabled(series, Modified); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) @@ -67,10 +69,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, EPISODE_DELETED_TITLE_BRANDED, deleteMessage.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, deleteMessage.Series, "Deleted"); - } + UpdateIfEnabled(deleteMessage.Series, Deleted); } public override void OnSeriesAdd(SeriesAddMessage message) @@ -80,10 +79,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, SERIES_ADDED_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) @@ -93,10 +89,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, SERIES_DELETED_TITLE_BRANDED, deleteMessage.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, deleteMessage.Series, "Deleted"); - } + UpdateIfEnabled(deleteMessage.Series, Deleted); } public override void OnHealthIssue(HealthCheck.HealthCheck message) @@ -123,6 +116,34 @@ namespace NzbDrone.Core.Notifications.Emby } } + public override void ProcessQueue() + { + _updateQueue.ProcessQueue(Settings.Host, (items) => + { + if (Settings.UpdateLibrary) + { + _logger.Debug("Performing library update for {0} series", items.Count); + + items.ForEach(item => + { + // If there is only one update type for the series use that, otherwise send null and let Emby decide + var updateType = item.Info.Count == 1 ? item.Info.First() : null; + + _mediaBrowserService.Update(Settings, item.Series, updateType); + }); + } + }); + } + + private void UpdateIfEnabled(Series series, string updateType) + { + if (Settings.UpdateLibrary) + { + _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); + _updateQueue.Add(Settings.Host, series, updateType); + } + } + public override ValidationResult Test() { var failures = new List<ValidationFailure>(); diff --git a/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs b/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs new file mode 100644 index 000000000..101adf95e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications +{ + public class MediaServerUpdateQueue<TQueueHost, TItemInfo> + where TQueueHost : class + { + private class UpdateQueue + { + public Dictionary<int, UpdateQueueItem<TItemInfo>> Pending { get; } = new Dictionary<int, UpdateQueueItem<TItemInfo>>(); + public bool Refreshing { get; set; } + } + + private readonly ICached<UpdateQueue> _pendingSeriesCache; + + public MediaServerUpdateQueue(ICacheManager cacheManager) + { + _pendingSeriesCache = cacheManager.GetRollingCache<UpdateQueue>(typeof(TQueueHost), "pendingSeries", TimeSpan.FromDays(1)); + } + + public void Add(string identifier, Series series, TItemInfo info) + { + var queue = _pendingSeriesCache.Get(identifier, () => new UpdateQueue()); + + lock (queue) + { + var item = queue.Pending.TryGetValue(series.Id, out var value) + ? value + : new UpdateQueueItem<TItemInfo>(series); + + item.Info.Add(info); + + queue.Pending[series.Id] = item; + } + } + + public void ProcessQueue(string identifier, Action<List<UpdateQueueItem<TItemInfo>>> update) + { + var queue = _pendingSeriesCache.Find(identifier); + + if (queue == null) + { + return; + } + + lock (queue) + { + if (queue.Refreshing) + { + return; + } + + queue.Refreshing = true; + } + + try + { + while (true) + { + List<UpdateQueueItem<TItemInfo>> items; + + lock (queue) + { + if (queue.Pending.Empty()) + { + queue.Refreshing = false; + return; + } + + items = queue.Pending.Values.ToList(); + queue.Pending.Clear(); + } + + update(items); + } + } + catch + { + lock (queue) + { + queue.Refreshing = false; + } + + throw; + } + } + } + + public class UpdateQueueItem<TItemInfo> + { + public Series Series { get; set; } + public HashSet<TItemInfo> Info { get; set; } + + public UpdateQueueItem(Series series) + { + Series = series; + Info = new HashSet<TItemInfo>(); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs index ab8079f7c..ca7cc06b6 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -18,23 +18,15 @@ namespace NzbDrone.Core.Notifications.Plex.Server { private readonly IPlexServerService _plexServerService; private readonly IPlexTvService _plexTvService; + private readonly MediaServerUpdateQueue<PlexServer, bool> _updateQueue; private readonly Logger _logger; - private class PlexUpdateQueue - { - public Dictionary<int, Series> Pending { get; } = new Dictionary<int, Series>(); - public bool Refreshing { get; set; } - } - - private readonly ICached<PlexUpdateQueue> _pendingSeriesCache; - public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService, ICacheManager cacheManager, Logger logger) { _plexServerService = plexServerService; _plexTvService = plexTvService; + _updateQueue = new MediaServerUpdateQueue<PlexServer, bool>(cacheManager); _logger = logger; - - _pendingSeriesCache = cacheManager.GetRollingCache<PlexUpdateQueue>(GetType(), "pendingSeries", TimeSpan.FromDays(1)); } public override string Link => "https://www.plex.tv/"; @@ -80,66 +72,20 @@ namespace NzbDrone.Core.Notifications.Plex.Server if (Settings.UpdateLibrary) { _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); - var queue = _pendingSeriesCache.Get(Settings.Host, () => new PlexUpdateQueue()); - lock (queue) - { - queue.Pending[series.Id] = series; - } + _updateQueue.Add(Settings.Host, series, false); } } public override void ProcessQueue() { - var queue = _pendingSeriesCache.Find(Settings.Host); - - if (queue == null) + _updateQueue.ProcessQueue(Settings.Host, (items) => { - return; - } - - lock (queue) - { - if (queue.Refreshing) + if (Settings.UpdateLibrary) { - return; + _logger.Debug("Performing library update for {0} series", items.Count); + _plexServerService.UpdateLibrary(items.Select(i => i.Series), Settings); } - - queue.Refreshing = true; - } - - try - { - while (true) - { - List<Series> refreshingSeries; - lock (queue) - { - if (queue.Pending.Empty()) - { - queue.Refreshing = false; - return; - } - - refreshingSeries = queue.Pending.Values.ToList(); - queue.Pending.Clear(); - } - - if (Settings.UpdateLibrary) - { - _logger.Debug("Performing library update for {0} series", refreshingSeries.Count); - _plexServerService.UpdateLibrary(refreshingSeries, Settings); - } - } - } - catch - { - lock (queue) - { - queue.Refreshing = false; - } - - throw; - } + }); } public override ValidationResult Test() @@ -213,13 +159,6 @@ namespace NzbDrone.Core.Notifications.Plex.Server { var result = new List<FieldSelectStringOption>(); - // result.Add(new FieldSelectStringOption - // { - // Value = s.Name, - // Name = s.Name, - // IsDisabled = true - // }); - s.Connections.ForEach(c => { var isSecure = c.Protocol == "https"; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 1bff9d178..51a656545 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Sockets; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -12,11 +13,13 @@ namespace NzbDrone.Core.Notifications.Xbmc public class Xbmc : NotificationBase<XbmcSettings> { private readonly IXbmcService _xbmcService; + private readonly MediaServerUpdateQueue<Xbmc, bool> _updateQueue; private readonly Logger _logger; - public Xbmc(IXbmcService xbmcService, Logger logger) + public Xbmc(IXbmcService xbmcService, ICacheManager cacheManager, Logger logger) { _xbmcService = xbmcService; + _updateQueue = new MediaServerUpdateQueue<Xbmc, bool>(cacheManager); _logger = logger; } @@ -99,6 +102,35 @@ namespace NzbDrone.Core.Notifications.Xbmc public override string Name => "Kodi"; + public override void ProcessQueue() + { + _updateQueue.ProcessQueue(Settings.Host, (items) => + { + _logger.Debug("Performing library update for {0} series", items.Count); + + items.ForEach(item => + { + try + { + if (Settings.UpdateLibrary) + { + _xbmcService.Update(Settings, item.Series); + } + + if (item.Info.Contains(true) && Settings.CleanLibrary) + { + _xbmcService.Clean(Settings); + } + } + catch (SocketException ex) + { + var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); + _logger.Debug(ex, logMessage); + } + }); + }); + } + public override ValidationResult Test() { var failures = new List<ValidationFailure>(); @@ -126,22 +158,10 @@ namespace NzbDrone.Core.Notifications.Xbmc private void UpdateAndClean(Series series, bool clean = true) { - try + if (Settings.UpdateLibrary || Settings.CleanLibrary) { - if (Settings.UpdateLibrary) - { - _xbmcService.Update(Settings, series); - } - - if (clean && Settings.CleanLibrary) - { - _xbmcService.Clean(Settings); - } - } - catch (SocketException ex) - { - var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); - _logger.Debug(ex, logMessage); + _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); + _updateQueue.Add(Settings.Host, series, clean); } } } From 0c883f78862f88ff37cd5539da4f569fbe3c93ed Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:25:26 +0300 Subject: [PATCH 361/762] Fixed: Removing pending release without blocklisting --- frontend/src/Activity/Queue/RemoveQueueItemModal.tsx | 3 ++- src/Sonarr.Api.V3/Queue/QueueController.cs | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx index 4348f818c..255c8a562 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx @@ -118,6 +118,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { { key: 'blocklistAndSearch', value: translate('BlocklistAndSearch'), + isDisabled: isPending, hint: multipleSelected ? translate('BlocklistAndSearchMultipleHint') : translate('BlocklistAndSearchHint'), @@ -130,7 +131,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { : translate('BlocklistOnlyHint'), }, ]; - }, [multipleSelected]); + }, [isPending, multipleSelected]); const handleRemovalMethodChange = useCallback( ({ value }: { value: RemovalMethod }) => { diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 8884ef4a6..34622ad18 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -77,7 +77,7 @@ namespace Sonarr.Api.V3.Queue if (pendingRelease != null) { - Remove(pendingRelease); + Remove(pendingRelease, blocklist); return; } @@ -120,7 +120,7 @@ namespace Sonarr.Api.V3.Queue foreach (var pendingRelease in pendingToRemove.DistinctBy(p => p.Id)) { - Remove(pendingRelease); + Remove(pendingRelease, blocklist); } foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId)) @@ -286,9 +286,13 @@ namespace Sonarr.Api.V3.Queue } } - private void Remove(NzbDrone.Core.Queue.Queue pendingRelease) + private void Remove(NzbDrone.Core.Queue.Queue pendingRelease, bool blocklist) { - _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); + if (blocklist) + { + _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); + } + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); } From 293a1bc618ea0b7ddf7d3a5fd4c47b0e6d2d09e9 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Jul 2024 22:02:04 -0700 Subject: [PATCH 362/762] New: Custom colon replacement option Closes #6898 --- .../Settings/MediaManagement/Naming/Naming.js | 19 +++++++++- .../ColonReplacementFixture.cs | 13 +++++++ ...stom_colon_replacement_to_naming_config.cs | 14 ++++++++ src/NzbDrone.Core/Localization/Core/en.json | 3 ++ .../Organizer/FileNameBuilder.cs | 16 +++++---- .../Organizer/FileNameValidation.cs | 36 +++++++++++++++++++ src/NzbDrone.Core/Organizer/NamingConfig.cs | 2 ++ .../Config/NamingConfigController.cs | 1 + .../Config/NamingConfigResource.cs | 1 + .../Config/NamingExampleResource.cs | 2 ++ 10 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index a4a1d4b09..8d188551f 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -138,7 +138,8 @@ class Naming extends Component { { key: 1, value: translate('ReplaceWithDash') }, { key: 2, value: translate('ReplaceWithSpaceDash') }, { key: 3, value: translate('ReplaceWithSpaceDashSpace') }, - { key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') } + { key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') }, + { key: 5, value: translate('Custom'), hint: translate('CustomColonReplacementFormatHint') } ]; const standardEpisodeFormatHelpTexts = []; @@ -262,6 +263,22 @@ class Naming extends Component { null } + { + replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ? + <FormGroup> + <FormLabel>{translate('ColonReplacement')}</FormLabel> + + <FormInputGroup + type={inputTypes.TEXT} + name="customColonReplacementFormat" + helpText={translate('CustomColonReplacementFormatHelpText')} + onChange={onInputChange} + {...settings.customColonReplacementFormat} + /> + </FormGroup> : + null + } + { renameEpisodes && <div> diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs index ae0ae7297..18b4aa9f1 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs @@ -90,5 +90,18 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile) .Should().Be(expected); } + + [TestCase("Series: Title", ColonReplacementFormat.Custom, "\ua789", "Series\ua789 Title")] + [TestCase("Series: Title", ColonReplacementFormat.Custom, "∶", "Series∶ Title")] + public void should_replace_colon_with_custom_format(string seriesName, ColonReplacementFormat replacementFormat, string customFormat, string expected) + { + _series.Title = seriesName; + _namingConfig.StandardEpisodeFormat = "{Series Title}"; + _namingConfig.ColonReplacementFormat = replacementFormat; + _namingConfig.CustomColonReplacementFormat = customFormat; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile) + .Should().Be(expected); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs new file mode 100644 index 000000000..537a70ea9 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(211)] + public class add_custom_colon_replacement_to_naming_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("NamingConfig").AddColumn("CustomColonReplacementFormat").AsString().WithDefaultValue(""); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 36e3c35c1..76d905d4e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -270,6 +270,9 @@ "CreateGroup": "Create Group", "CurrentlyInstalled": "Currently Installed", "Custom": "Custom", + "CustomColonReplacement": "Custom Colon Replacement", + "CustomColonReplacementFormatHelpText": "Characters to be used as a replacement for colons", + "CustomColonReplacementFormatHint": "Valid file system character such as Colon (Letter)", "CustomFilter": "Custom Filter", "CustomFilters": "Custom Filters", "CustomFormat": "Custom Format", diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 714605a99..3755f17f7 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Diacritical; -using DryIoc.ImTools; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; @@ -112,6 +111,9 @@ namespace NzbDrone.Core.Organizer { "wel", "cym" } }.ToImmutableDictionary(); + public static readonly ImmutableArray<string> BadCharacters = ImmutableArray.Create("\\", "/", "<", ">", "?", "*", "|", "\""); + public static readonly ImmutableArray<string> GoodCharacters = ImmutableArray.Create("+", "+", "", "", "!", "-", "", ""); + public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, @@ -1156,8 +1158,6 @@ namespace NzbDrone.Core.Organizer private static string CleanFileName(string name, NamingConfig namingConfig) { var result = name; - string[] badCharacters = { "\\", "/", "<", ">", "?", "*", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "" }; if (namingConfig.ReplaceIllegalCharacters) { @@ -1182,6 +1182,9 @@ namespace NzbDrone.Core.Organizer case ColonReplacementFormat.SpaceDashSpace: replacement = " - "; break; + case ColonReplacementFormat.Custom: + replacement = namingConfig.CustomColonReplacementFormat; + break; } result = result.Replace(":", replacement); @@ -1192,9 +1195,9 @@ namespace NzbDrone.Core.Organizer result = result.Replace(":", string.Empty); } - for (var i = 0; i < badCharacters.Length; i++) + for (var i = 0; i < BadCharacters.Length; i++) { - result = result.Replace(badCharacters[i], namingConfig.ReplaceIllegalCharacters ? goodCharacters[i] : string.Empty); + result = result.Replace(BadCharacters[i], namingConfig.ReplaceIllegalCharacters ? GoodCharacters[i] : string.Empty); } return result.TrimStart(' ', '.').TrimEnd(' '); @@ -1268,6 +1271,7 @@ namespace NzbDrone.Core.Organizer Dash = 1, SpaceDash = 2, SpaceDashSpace = 3, - Smart = 4 + Smart = 4, + Custom = 5 } } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index e8d39469f..b36f9426c 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -61,6 +62,13 @@ namespace NzbDrone.Core.Organizer return ruleBuilder.SetValidator(new IllegalCharactersValidator()); } + + public static IRuleBuilderOptions<T, string> ValidCustomColonReplacement<T>(this IRuleBuilder<T, string> ruleBuilder) + { + ruleBuilder.SetValidator(new IllegalColonCharactersValidator()); + + return ruleBuilder.SetValidator(new IllegalCharactersValidator()); + } } public class ValidStandardEpisodeFormatValidator : PropertyValidator @@ -132,6 +140,34 @@ namespace NzbDrone.Core.Organizer } var invalidCharacters = InvalidPathChars.Where(i => value!.IndexOf(i) >= 0).ToList(); + + if (invalidCharacters.Any()) + { + context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters)); + return false; + } + + return true; + } + } + + public class IllegalColonCharactersValidator : PropertyValidator + { + private static readonly string[] InvalidPathChars = FileNameBuilder.BadCharacters.Concat(new[] { ":" }).ToArray(); + + protected override string GetDefaultMessageTemplate() => "Contains illegal characters: {InvalidCharacters}"; + + protected override bool IsValid(PropertyValidatorContext context) + { + var value = context.PropertyValue as string; + + if (value.IsNullOrWhiteSpace()) + { + return true; + } + + var invalidCharacters = InvalidPathChars.Where(i => value!.IndexOf(i, StringComparison.Ordinal) >= 0).ToList(); + if (invalidCharacters.Any()) { context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters)); diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 57d88d405..eb3f65c84 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer RenameEpisodes = false, ReplaceIllegalCharacters = true, ColonReplacementFormat = ColonReplacementFormat.Smart, + CustomColonReplacementFormat = string.Empty, MultiEpisodeStyle = MultiEpisodeStyle.PrefixedRange, StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", @@ -21,6 +22,7 @@ namespace NzbDrone.Core.Organizer public bool RenameEpisodes { get; set; } public bool ReplaceIllegalCharacters { get; set; } public ColonReplacementFormat ColonReplacementFormat { get; set; } + public string CustomColonReplacementFormat { get; set; } public MultiEpisodeStyle MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } diff --git a/src/Sonarr.Api.V3/Config/NamingConfigController.cs b/src/Sonarr.Api.V3/Config/NamingConfigController.cs index a05894f45..f8870f35c 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigController.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigController.cs @@ -36,6 +36,7 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat(); + SharedValidator.RuleFor(c => c.CustomColonReplacementFormat).ValidCustomColonReplacement().When(c => c.ColonReplacementFormat == (int)ColonReplacementFormat.Custom); } protected override NamingConfigResource GetResourceById(int id) diff --git a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs index f1acc9fe3..e3354fb38 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs @@ -7,6 +7,7 @@ namespace Sonarr.Api.V3.Config public bool RenameEpisodes { get; set; } public bool ReplaceIllegalCharacters { get; set; } public int ColonReplacementFormat { get; set; } + public string CustomColonReplacementFormat { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs index 6784e1a4b..ed1bfbd92 100644 --- a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs +++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs @@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.Config RenameEpisodes = model.RenameEpisodes, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, ColonReplacementFormat = (int)model.ColonReplacementFormat, + CustomColonReplacementFormat = model.CustomColonReplacementFormat, MultiEpisodeStyle = (int)model.MultiEpisodeStyle, StandardEpisodeFormat = model.StandardEpisodeFormat, DailyEpisodeFormat = model.DailyEpisodeFormat, @@ -45,6 +46,7 @@ namespace Sonarr.Api.V3.Config ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle, ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat, + CustomColonReplacementFormat = resource.CustomColonReplacementFormat, StandardEpisodeFormat = resource.StandardEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat, From 10e9735c1cb5f3b0d318c195a37df9e3a0407639 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Jul 2024 22:02:23 -0700 Subject: [PATCH 363/762] New: Update AutoTags on series update Closes #6783 --- .../UpdateMultipleSeriesFixture.cs | 23 ++++++++++ .../SeriesServiceTests/UpdateSeriesFixture.cs | 35 ++++++++++++++- src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 27 +----------- src/NzbDrone.Core/Tv/SeriesService.cs | 44 +++++++++++++++++++ 4 files changed, 103 insertions(+), 26 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs index b48c260fc..b97c3b313 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs @@ -5,6 +5,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Organizer; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -28,6 +29,10 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests .With(s => s.Path = @"C:\Test\name".AsOsAgnostic()) .With(s => s.RootFolderPath = "") .Build().ToList(); + + Mocker.GetMock<IAutoTaggingService>() + .Setup(s => s.GetTagChanges(It.IsAny<Series>())) + .Returns(new AutoTaggingChanges()); } [Test] @@ -79,5 +84,23 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests Subject.UpdateSeries(series, false); } + + [Test] + public void should_add_and_remove_tags() + { + _series[0].Tags = new HashSet<int> { 1, 2 }; + + Mocker.GetMock<IAutoTaggingService>() + .Setup(s => s.GetTagChanges(_series[0])) + .Returns(new AutoTaggingChanges + { + TagsToAdd = new HashSet<int> { 3 }, + TagsToRemove = new HashSet<int> { 1 } + }); + + var result = Subject.UpdateSeries(_series, false); + + result[0].Tags.Should().BeEquivalentTo(new[] { 2, 3 }); + } } } diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs index cc753b716..ba1b4bbf6 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; +using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -31,6 +33,14 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests new Season { SeasonNumber = 1, Monitored = true }, new Season { SeasonNumber = 2, Monitored = true } }; + + Mocker.GetMock<IAutoTaggingService>() + .Setup(s => s.GetTagChanges(It.IsAny<Series>())) + .Returns(new AutoTaggingChanges()); + + Mocker.GetMock<ISeriesRepository>() + .Setup(s => s.Update(It.IsAny<Series>())) + .Returns<Series>(r => r); } private void GivenExistingSeries() @@ -68,5 +78,28 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests Mocker.GetMock<IEpisodeService>() .Verify(v => v.SetEpisodeMonitoredBySeason(_fakeSeries.Id, It.IsAny<int>(), It.IsAny<bool>()), Times.Once()); } + + [Test] + public void should_add_and_remove_tags() + { + GivenExistingSeries(); + var seasonNumber = 1; + var monitored = false; + + _fakeSeries.Tags = new HashSet<int> { 1, 2 }; + _fakeSeries.Seasons.Single(s => s.SeasonNumber == seasonNumber).Monitored = monitored; + + Mocker.GetMock<IAutoTaggingService>() + .Setup(s => s.GetTagChanges(_fakeSeries)) + .Returns(new AutoTaggingChanges + { + TagsToAdd = new HashSet<int> { 3 }, + TagsToRemove = new HashSet<int> { 1 } + }); + + var result = Subject.UpdateSeries(_fakeSeries); + + result.Tags.Should().BeEquivalentTo(new[] { 2, 3 }); + } } } diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 49aa9ebf6..b9dd08482 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -198,34 +198,11 @@ namespace NzbDrone.Core.Tv private void UpdateTags(Series series) { - _logger.Trace("Updating tags for {0}", series); + var tagsUpdated = _seriesService.UpdateTags(series); - var tagsAdded = new HashSet<int>(); - var tagsRemoved = new HashSet<int>(); - var changes = _autoTaggingService.GetTagChanges(series); - - foreach (var tag in changes.TagsToRemove) - { - if (series.Tags.Contains(tag)) - { - series.Tags.Remove(tag); - tagsRemoved.Add(tag); - } - } - - foreach (var tag in changes.TagsToAdd) - { - if (!series.Tags.Contains(tag)) - { - series.Tags.Add(tag); - tagsAdded.Add(tag); - } - } - - if (tagsAdded.Any() || tagsRemoved.Any()) + if (tagsUpdated) { _seriesService.UpdateSeries(series); - _logger.Debug("Updated tags for '{0}'. Added: {1}, Removed: {2}", series.Title, tagsAdded.Count, tagsRemoved.Count); } } diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 6fd4551cf..b582acffa 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv.Events; @@ -30,6 +31,7 @@ namespace NzbDrone.Core.Tv List<Series> UpdateSeries(List<Series> series, bool useExistingRelativeFolder); bool SeriesPathExists(string folder); void RemoveAddOptions(Series series); + bool UpdateTags(Series series); } public class SeriesService : ISeriesService @@ -38,18 +40,21 @@ namespace NzbDrone.Core.Tv private readonly IEventAggregator _eventAggregator; private readonly IEpisodeService _episodeService; private readonly IBuildSeriesPaths _seriesPathBuilder; + private readonly IAutoTaggingService _autoTaggingService; private readonly Logger _logger; public SeriesService(ISeriesRepository seriesRepository, IEventAggregator eventAggregator, IEpisodeService episodeService, IBuildSeriesPaths seriesPathBuilder, + IAutoTaggingService autoTaggingService, Logger logger) { _seriesRepository = seriesRepository; _eventAggregator = eventAggregator; _episodeService = episodeService; _seriesPathBuilder = seriesPathBuilder; + _autoTaggingService = autoTaggingService; _logger = logger; } @@ -205,6 +210,7 @@ namespace NzbDrone.Core.Tv // Never update AddOptions when updating a series, keep it the same as the existing stored series. series.AddOptions = storedSeries.AddOptions; + UpdateTags(series); var updatedSeries = _seriesRepository.Update(series); if (publishUpdatedEvent) @@ -233,6 +239,8 @@ namespace NzbDrone.Core.Tv { _logger.Trace("Not changing path for: {0}", s.Title); } + + UpdateTags(s); } _seriesRepository.UpdateMany(series); @@ -250,5 +258,41 @@ namespace NzbDrone.Core.Tv { _seriesRepository.SetFields(series, s => s.AddOptions); } + + public bool UpdateTags(Series series) + { + _logger.Trace("Updating tags for {0}", series); + + var tagsAdded = new HashSet<int>(); + var tagsRemoved = new HashSet<int>(); + var changes = _autoTaggingService.GetTagChanges(series); + + foreach (var tag in changes.TagsToRemove) + { + if (series.Tags.Contains(tag)) + { + series.Tags.Remove(tag); + tagsRemoved.Add(tag); + } + } + + foreach (var tag in changes.TagsToAdd) + { + if (!series.Tags.Contains(tag)) + { + series.Tags.Add(tag); + tagsAdded.Add(tag); + } + } + + if (tagsAdded.Any() || tagsRemoved.Any()) + { + _logger.Debug("Updated tags for '{0}'. Added: {1}, Removed: {2}", series.Title, tagsAdded.Count, tagsRemoved.Count); + + return true; + } + + return false; + } } } From 678872b879a5f569e3a4f49c2be908e7d4cccef9 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Wed, 10 Jul 2024 07:19:07 +0200 Subject: [PATCH 364/762] Fixed: Parsing of some Web releases --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index a4f9ec8d3..98475053a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -254,6 +254,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title Season 2 (WEB 1080p HEVC Opus) [Netaro]", false)] [TestCase("Series.Title.S01E01.Erste.Begegnungen.German.DD51.Synced.DL.1080p.HBOMaxHD.AVC-TVS", false)] [TestCase("Series.Title.S01E05.Tavora.greift.an.German.DL.1080p.DisneyHD.h264-4SF", false)] + [TestCase("Series.Title.S02E04.German.Dubbed.DL.AAC.1080p.WEB.AVC-GROUP", false)] public void should_parse_webdl1080p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper); @@ -279,6 +280,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][2160p][AAC 2.0][Softsubs (HorribleSubs)]", false)] [TestCase("Series Title S02 2013 WEB-DL 4k H265 AAC 2Audio-HDSWEB", false)] [TestCase("Series.Title.S02E02.This.Year.Will.Be.Different.2160p.WEB.H.265", false)] + [TestCase("Series.Title.S02E04.German.Dubbed.DL.AAC.2160p.DV.HDR.WEB.HEVC-GROUP", false)] public void should_parse_webdl2160p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL2160p, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index d466054e6..575727297 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SourceRegex = new (@"\b(?: (?<bluray>BluRay|Blu-Ray|HD-?DVD|BDMux|BD(?!$))| - (?<webdl>WEB[-_. ]DL(?:mux)?|WEBDL|AmazonHD|AmazonSD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|HBOMaxHD|DisneyHD|[. ]WEB[. ](?:[xh][ .]?26[45]|DDP?5[. ]1)|[. ](?-i:WEB)$|(?:720|1080|2160)p[-. ]WEB[-. ]|[-. ]WEB[-. ](?:720|1080|2160)p|\b\s\/\sWEB\s\/\s\b|(?:AMZN|NF|DP)[. -]WEB[. -](?!Rip))| + (?<webdl>WEB[-_. ]DL(?:mux)?|WEBDL|AmazonHD|AmazonSD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|HBOMaxHD|DisneyHD|[. ]WEB[. ](?:[xh][ .]?26[45]|AVC|HEVC|DDP?5[. ]1)|[. ](?-i:WEB)$|(?:720|1080|2160)p[-. ]WEB[-. ]|[-. ]WEB[-. ](?:720|1080|2160)p|\b\s\/\sWEB\s\/\s\b|(?:AMZN|NF|DP)[. -]WEB[. -](?!Rip))| (?<webrip>WebRip|Web-Rip|WEBMux)| (?<hdtv>HDTV)| (?<bdrip>BDRip|BDLight)| From e97e5bfe8f3a7f01f7188f786eb7b51f1d8ef8b5 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Wed, 10 Jul 2024 05:04:51 +0000 Subject: [PATCH 365/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index bab330939..736c79a9e 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -4840,6 +4840,13 @@ "format": "int32" } }, + { + "name": "customColonReplacementFormat", + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "multiEpisodeStyle", "in": "query", @@ -10006,6 +10013,10 @@ "type": "integer", "format": "int32" }, + "customColonReplacementFormat": { + "type": "string", + "nullable": true + }, "multiEpisodeStyle": { "type": "integer", "format": "int32" @@ -10110,6 +10121,9 @@ "onUpgrade": { "type": "boolean" }, + "onImportComplete": { + "type": "boolean" + }, "onRename": { "type": "boolean" }, @@ -10149,6 +10163,9 @@ "supportsOnUpgrade": { "type": "boolean" }, + "supportsOnImportComplete": { + "type": "boolean" + }, "supportsOnRename": { "type": "boolean" }, From acaf5cd353c84739eb48a4be2ee9d53b3984c14f Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 14 Jul 2024 00:25:14 +0000 Subject: [PATCH 366/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com> Co-authored-by: Rauniik <raunerjakub@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/cs.json | 18 ++++- src/NzbDrone.Core/Localization/Core/es.json | 8 ++- src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/it.json | 66 ++++++++++++++++++- .../Localization/Core/pt_BR.json | 12 +++- .../Localization/Core/zh_CN.json | 1 - 6 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 0f168f6d7..6d8e08d2b 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -209,7 +209,7 @@ "CountSeasons": "{count} Řad", "CustomFormat": "Vlastní formát", "CustomFormats": "Vlastní formáty", - "CutoffUnmet": "Vynechat nevyhovující", + "CutoffUnmet": "Mezní hodnota nesplněna", "Cutoff": "Odříznout", "DailyEpisodeFormat": "Formát denní epizody", "DailyEpisodeTypeDescription": "Epizody vydávané denně nebo méně často, které používají rok-měsíc-den (2023-08-04)", @@ -219,7 +219,7 @@ "CreateGroup": "Vytvořit skupinu", "Custom": "Vlastní", "CustomFormatUnknownCondition": "Neznámá podmínka vlastního formátu „{implementation}“", - "CustomFormatUnknownConditionOption": "Neznámá možnost '{key}' pro podmínku '{implementation}'", + "CustomFormatUnknownConditionOption": "Neznámá možnost „{key}“ pro podmínku „{implementation}“", "CustomFormatsLoadError": "Nelze načíst vlastní formáty", "CustomFormatsSettingsSummary": "Vlastní formáty a nastavení", "CopyUsingHardlinksSeriesHelpText": "Pevné odkazy umožňují aplikaci {appName} importovat odesílané torrenty do složky seriálu, aniž by zabíraly další místo na disku nebo kopírovaly celý obsah souboru. Hardlinky budou fungovat pouze v případě, že zdrojový a cílový soubor jsou na stejném svazku", @@ -319,5 +319,17 @@ "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 {appName}." + "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}.", + "ConnectionSettingsUrlBaseHelpText": "Přidá předponu do {connectionName} url, jako např. {url}", + "CustomFormatsSpecificationRegularExpressionHelpText": "Vlastní formát RegEx nerozlišuje velká a malá písmena", + "CustomFormatsSpecificationFlag": "Vlajka", + "BlackholeFolderHelpText": "Složka do které {appName} uloží {extension} soubor", + "BlackholeWatchFolder": "Složka sledování", + "Category": "Kategorie", + "BlocklistAndSearch": "Seznam blokovaných a vyhledávání", + "BlackholeWatchFolderHelpText": "Složka ze které {appName} má importovat stažené soubory", + "BlocklistReleaseHelpText": "Zabránit {appName} v opětovném sebrání tohoto vydání pomocí RSS nebo automatického vyhledávání", + "BlocklistMultipleOnlyHint": "Blokovat a nehledat náhradu", + "CustomFormatsSettingsTriggerInfo": "Vlastní formát se použije na vydání nebo soubor, pokud odpovídá alespoň jednomu z různých typů zvolených podmínek.", + "ChangeCategory": "Změnit kategorii" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 94fbb72fb..89850b375 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1526,7 +1526,6 @@ "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", @@ -2081,5 +2080,10 @@ "DayOfWeekAt": "{day} a las {time}", "UnableToImportAutomatically": "No se pudo importar automáticamente", "NotificationsPlexSettingsServer": "Servidor", - "NotificationsPlexSettingsServerHelpText": "Selecciona el servidor desde una cuenta de plex.tv después de autenticarse" + "NotificationsPlexSettingsServerHelpText": "Selecciona el servidor desde una cuenta de plex.tv después de autenticarse", + "CustomColonReplacement": "Reemplazo personalizado de dos puntos", + "CustomColonReplacementFormatHelpText": "Caracteres que serán usados como reemplazo para los dos puntos", + "CustomColonReplacementFormatHint": "Caracteres válidos del sistema de archivos como dos puntos (letra)", + "OnFileImport": "Al importar un archivo", + "OnImportComplete": "Al completar la importación" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 5399a034f..4c6ba475d 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -946,7 +946,6 @@ "MatchedToEpisodes": "Adapté aux épisodes", "NoEpisodesFoundForSelectedSeason": "Aucun épisode n'a été trouvé pour la saison sélectionnée", "OnHealthRestored": "Sur la santé restaurée", - "OnImport": "À l'importation", "OnLatestVersion": "La dernière version de {appName} est déjà installée", "PreferAndUpgrade": "Préférer et mettre à niveau", "RejectionCount": "Nombre de rejets", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 977e0def0..bc31e8976 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -863,5 +863,69 @@ "TableColumns": "Colonne", "TodayAt": "Oggi alle {time}", "TomorrowAt": "Domani alle {time}", - "TotalSpace": "Totale Spazio" + "TotalSpace": "Totale Spazio", + "EnableRss": "Abilita RSS", + "DownloadFailedEpisodeTooltip": "Download dell'episodio fallito", + "Downloading": "Scaricando", + "Edit": "Modifica", + "EditCustomFormat": "Modifica Formato Personalizzato", + "Error": "Errore", + "EpisodeDownloaded": "Episodio Scaricato", + "ImportListsSonarrSettingsFullUrl": "URL Completo", + "ImportListsTraktSettingsLimit": "Limite", + "DownloadClient": "Client di Download", + "EditConditionImplementation": "Modifica Condizione - {implementationName}", + "EpisodeFileMissingTooltip": "File dell'episodio mancante", + "EpisodeInfo": "Info Episodio", + "DockerUpdater": "Aggiorna il container di docker per ricevere l'aggiornamento", + "DownloadFailed": "Download Fallito", + "EpisodeFileRenamedTooltip": "Episodio del file rinominato", + "EpisodeTitleRequired": "Titolo Episodio Richiesto", + "Existing": "Esistente", + "Episode": "Episodio", + "ImportSeries": "Importa Serie", + "Donations": "Donazioni", + "EditDelayProfile": "Modifica Profilo di Ritardo", + "EnableInteractiveSearchHelpTextWarning": "Ricerca non supportata con questo indicizzatore", + "EnableProfile": "Abilita Profilo", + "Exception": "Eccezione", + "EditGroups": "Modifica Gruppi", + "EditDownloadClientImplementation": "Modifica Client di Download - {implementationName}", + "EditSelectedSeries": "Modifica Serie Selezionate", + "DotNetVersion": ".NET", + "DiskSpace": "Spazio sul Disco", + "EpisodeFileRenamed": "File dell'Episodio Rinominato", + "Events": "Eventi", + "Episodes": "Episodi", + "NotificationsDiscordSettingsAuthor": "Autore", + "EpisodeTitle": "Titolo Episodio", + "Imported": "Importato", + "EpisodeImported": "Episodio Importato", + "EditConnectionImplementation": "Modifica Connessione - {implementationName}", + "Enable": "Abilita", + "EnableAutomaticAdd": "Abilita Aggiunta Automatica", + "EnableAutomaticAddSeriesHelpText": "Aggiungi serie da questa lista a {appName} quando la sincronizzazione è effettuata tramite UI o da {appName}", + "EditIndexerImplementation": "Modifica Indicizzatore - {implementationName}", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Autentica con Trakt", + "ImportListsTraktSettingsGenres": "Generi", + "ImportListsTraktSettingsListName": "Nome Lista", + "ImportListsTraktSettingsAdditionalParameters": "Parametri Addizionali", + "ImportListsTraktSettingsListType": "Tipo Lista", + "ImportListsTraktSettingsRating": "Valutazione", + "Docker": "Docker", + "IndexerHDBitsSettingsCategories": "Categorie", + "EditSeries": "Modifica Serie", + "Duration": "Durata", + "EnableCompletedDownloadHandlingHelpText": "Importa automaticamente i download completati dal client di download", + "SeriesIndexFooterEnded": "Terminata (Tutti gli episodi scaricati)", + "EpisodeFileDeletedTooltip": "File dell'episodio eliminato", + "Downloaded": "Scaricato", + "EditQualityProfile": "Modifica Profilo Qualità", + "EditSeriesModalHeader": "Modifica - {title}", + "EpisodeRequested": "Episodio Richiesto", + "DownloadIgnoredEpisodeTooltip": "Download dell'Episodio Ignorato", + "EditRestriction": "Modifica Restrizione", + "EnableSsl": "Abilita SSL", + "EpisodeFileDeleted": "File dell'Episodio Eliminato", + "Importing": "Importando" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8fa4b47dc..79e80f627 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -686,7 +686,6 @@ "OnGrab": "Ao obter", "OnHealthIssue": "Ao Problema de Saúde", "OnHealthRestored": "Com a Saúde Restaurada", - "OnImport": "Ao Importar", "OnRename": "Ao Renomear", "OnSeriesAdd": "Ao Adicionar Série", "OnSeriesDelete": "Ao Excluir Série", @@ -1262,7 +1261,7 @@ "OverrideGrabNoQuality": "A qualidade deve ser selecionada", "OverrideGrabNoSeries": "A série deve ser selecionada", "Parse": "Analisar", - "ParseModalHelpTextDetails": "{appName} tentará analisar o título e mostrar detalhes sobre ele", + "ParseModalHelpTextDetails": "O {appName} tentará analisar o título e mostrar detalhes sobre ele", "RecentChanges": "Mudanças Recentes", "ReleaseRejected": "Lançamento Rejeitado", "ReleaseSceneIndicatorAssumingScene": "Assumindo a Numeração da Scene.", @@ -2079,5 +2078,12 @@ "TomorrowAt": "Amanhã às {time}", "HasUnmonitoredSeason": "Tem Temporada Não Monitorada", "YesterdayAt": "Ontem às {time}", - "UnableToImportAutomatically": "Não foi possível importar automaticamente" + "UnableToImportAutomatically": "Não foi possível importar automaticamente", + "CustomColonReplacement": "Substituto de Dois Pontos Personalizado", + "CustomColonReplacementFormatHint": "Caractere válido do sistema de arquivos, como dois pontos (letra)", + "NotificationsPlexSettingsServerHelpText": "Selecione o servidor da conta plex.tv após a autenticação", + "OnFileImport": "Ao Importar o Arquivo", + "OnImportComplete": "Ao Completar Importação", + "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição aos dois pontos", + "NotificationsPlexSettingsServer": "Servidor" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index b1fde6f60..dfea03c0c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1309,7 +1309,6 @@ "OnApplicationUpdate": "程序更新时", "OnHealthIssue": "健康度异常", "OnHealthRestored": "健康度恢复", - "OnImport": "导入中", "Password": "密码", "PendingDownloadClientUnavailable": "挂起 - 下载客户端不可用", "QueueLoadError": "加载队列失败", From 6afd3bd3443ee704dbfc0cdedc5fe1e8d8f7b40c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 14:36:17 -0700 Subject: [PATCH 367/762] Bump version to 4.0.7 --- .github/workflows/build.yml | 242 +++++++++++++++++++----------------- 1 file changed, 125 insertions(+), 117 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba1cff820..3a1bf4d2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,13 +6,13 @@ on: - develop - main paths-ignore: - - 'src/Sonarr.Api.*/openapi.json' + - "src/Sonarr.Api.*/openapi.json" pull_request: branches: - develop paths-ignore: - - 'src/NzbDrone.Core/Localization/Core/**' - - 'src/Sonarr.Api.*/openapi.json' + - "src/NzbDrone.Core/Localization/Core/**" + - "src/Sonarr.Api.*/openapi.json" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.6 + VERSION: 4.0.7 jobs: backend: @@ -32,105 +32,105 @@ jobs: major_version: ${{ steps.variables.outputs.major_version }} version: ${{ steps.variables.outputs.version }} steps: - - name: Check out - uses: actions/checkout@v4 + - name: Check out + uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 - - name: Setup Environment Variables - id: variables - shell: bash - run: | - # Add 800 to the build number because GitHub won't let us pick an arbitrary starting point - SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))" - DOTNET_VERSION=$(jq -r '.sdk.version' global.json) + - name: Setup Environment Variables + id: variables + shell: bash + run: | + # Add 800 to the build number because GitHub won't let us pick an arbitrary starting point + SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))" + DOTNET_VERSION=$(jq -r '.sdk.version' global.json) - echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV" - echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV" - echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV" + echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV" + echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV" + echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV" - echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT" - echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT" - echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT" + echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT" + echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT" + echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT" - - name: Enable Extra Platforms In SDK - shell: bash - run: ./build.sh --enable-extra-platforms-in-sdk + - name: Enable Extra Platforms In SDK + shell: bash + run: ./build.sh --enable-extra-platforms-in-sdk - - name: Build Backend - shell: bash - run: ./build.sh --backend --enable-extra-platforms --packages + - name: Build Backend + shell: bash + run: ./build.sh --backend --enable-extra-platforms --packages - # Test Artifacts + # Test Artifacts - - name: Publish win-x64 Test Artifact - uses: ./.github/actions/publish-test-artifact - with: - framework: ${{ env.FRAMEWORK }} - runtime: win-x64 + - name: Publish win-x64 Test Artifact + uses: ./.github/actions/publish-test-artifact + with: + framework: ${{ env.FRAMEWORK }} + runtime: win-x64 - - name: Publish linux-x64 Test Artifact - uses: ./.github/actions/publish-test-artifact - with: - framework: ${{ env.FRAMEWORK }} - runtime: linux-x64 + - name: Publish linux-x64 Test Artifact + uses: ./.github/actions/publish-test-artifact + with: + framework: ${{ env.FRAMEWORK }} + runtime: linux-x64 - - name: Publish osx-arm64 Test Artifact - uses: ./.github/actions/publish-test-artifact - with: - framework: ${{ env.FRAMEWORK }} - runtime: osx-arm64 + - name: Publish osx-arm64 Test Artifact + uses: ./.github/actions/publish-test-artifact + with: + framework: ${{ env.FRAMEWORK }} + runtime: osx-arm64 - # Build Artifacts (grouped by OS) - - - name: Publish FreeBSD Artifact - uses: actions/upload-artifact@v4 - with: - name: build_freebsd - path: _artifacts/freebsd-*/**/* - - name: Publish Linux Artifact - uses: actions/upload-artifact@v4 - with: - name: build_linux - path: _artifacts/linux-*/**/* - - name: Publish macOS Artifact - uses: actions/upload-artifact@v4 - with: - name: build_macos - path: _artifacts/osx-*/**/* - - name: Publish Windows Artifact - uses: actions/upload-artifact@v4 - with: - name: build_windows - path: _artifacts/win-*/**/* + # Build Artifacts (grouped by OS) + + - name: Publish FreeBSD Artifact + uses: actions/upload-artifact@v4 + with: + name: build_freebsd + path: _artifacts/freebsd-*/**/* + - name: Publish Linux Artifact + uses: actions/upload-artifact@v4 + with: + name: build_linux + path: _artifacts/linux-*/**/* + - name: Publish macOS Artifact + uses: actions/upload-artifact@v4 + with: + name: build_macos + path: _artifacts/osx-*/**/* + - name: Publish Windows Artifact + uses: actions/upload-artifact@v4 + with: + name: build_windows + path: _artifacts/win-*/**/* frontend: runs-on: ubuntu-latest steps: - - name: Check out - uses: actions/checkout@v4 + - name: Check out + uses: actions/checkout@v4 - - name: Volta - uses: volta-cli/action@v4 + - name: Volta + uses: volta-cli/action@v4 - - name: Yarn Install - run: yarn install + - name: Yarn Install + run: yarn install - - name: Lint - run: yarn lint + - name: Lint + run: yarn lint - - name: Stylelint - run: yarn stylelint -f github + - name: Stylelint + run: yarn stylelint -f github - - name: Build - run: yarn build --env production + - name: Build + run: yarn build --env production - - name: Publish UI Artifact - uses: actions/upload-artifact@v4 - with: - name: build_ui - path: _output/UI/**/* + - name: Publish UI Artifact + uses: actions/upload-artifact@v4 + with: + name: build_ui + path: _output/UI/**/* unit_test: needs: backend @@ -150,32 +150,32 @@ jobs: filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest runs-on: ${{ matrix.os }} steps: - - name: Check out - uses: actions/checkout@v4 + - name: Check out + uses: actions/checkout@v4 - - name: Test - uses: ./.github/actions/test - with: - os: ${{ matrix.os }} - artifact: ${{ matrix.artifact }} - pattern: Sonarr.*.Test.dll - filter: ${{ matrix.filter }} + - name: Test + uses: ./.github/actions/test + with: + os: ${{ matrix.os }} + artifact: ${{ matrix.artifact }} + pattern: Sonarr.*.Test.dll + filter: ${{ matrix.filter }} unit_test_postgres: needs: backend runs-on: ubuntu-latest steps: - - name: Check out - uses: actions/checkout@v4 + - name: Check out + uses: actions/checkout@v4 - - name: Test - uses: ./.github/actions/test - with: - os: ubuntu-latest - artifact: tests-linux-x64 - pattern: Sonarr.*.Test.dll - filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest - use_postgres: true + - name: Test + uses: ./.github/actions/test + with: + os: ubuntu-latest + artifact: tests-linux-x64 + pattern: Sonarr.*.Test.dll + filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest + use_postgres: true integration_test: needs: backend @@ -201,19 +201,19 @@ jobs: binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr runs-on: ${{ matrix.os }} steps: - - name: Check out - uses: actions/checkout@v4 + - name: Check out + uses: actions/checkout@v4 - - name: Test - uses: ./.github/actions/test - with: - os: ${{ matrix.os }} - artifact: ${{ matrix.artifact }} - pattern: Sonarr.*.Test.dll - filter: ${{ matrix.filter }} - integration_tests: true - binary_artifact: ${{ matrix.binary_artifact }} - binary_path: ${{ matrix.binary_path }} + - name: Test + uses: ./.github/actions/test + with: + os: ${{ matrix.os }} + artifact: ${{ matrix.artifact }} + pattern: Sonarr.*.Test.dll + filter: ${{ matrix.filter }} + integration_tests: true + binary_artifact: ${{ matrix.binary_artifact }} + binary_path: ${{ matrix.binary_path }} deploy: if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} @@ -228,7 +228,15 @@ jobs: notify: name: Discord Notification - needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy] + 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' }} @@ -239,10 +247,10 @@ jobs: uses: tsickert/discord-webhook@v6.0.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - username: 'GitHub Actions' - avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png' + 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-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" embed-description: | **Branch** ${{ github.ref }} **Build** ${{ needs.backend.outputs.version }} From f5ccf9816257c363a249f7a3b8ad79675ed17851 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 21:57:16 -0700 Subject: [PATCH 368/762] Rename 'On Upgrade' to 'On File Upgrade' --- .../src/Settings/Notifications/Notifications/Notification.js | 2 +- .../Notifications/Notifications/NotificationEventItems.js | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs | 1 + src/NzbDrone.Core/Notifications/NotificationService.cs | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index 22e17c18f..b2d0b29b5 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -115,7 +115,7 @@ class Notification extends Component { { supportsOnUpgrade && onDownload && onUpgrade ? <Label kind={kinds.SUCCESS}> - {translate('OnUpgrade')} + {translate('OnFileUpgrade')} </Label> : null } diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js index ddcdae4f5..cb09783b6 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js @@ -81,7 +81,7 @@ function NotificationEventItems(props) { <FormInputGroup type={inputTypes.CHECK} name="onUpgrade" - helpText={translate('OnUpgrade')} + helpText={translate('OnFileUpgrade')} isDisabled={!supportsOnUpgrade.value} {...onUpgrade} onChange={onInputChange} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 76d905d4e..aeb1d0a09 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1460,6 +1460,7 @@ "OnEpisodeFileDelete": "On Episode File Delete", "OnEpisodeFileDeleteForUpgrade": "On Episode File Delete For Upgrade", "OnFileImport": "On File Import", + "OnFileUpgrade": "On File Upgrade", "OnGrab": "On Grab", "OnHealthIssue": "On Health Issue", "OnHealthRestored": "On Health Restored", @@ -1469,7 +1470,6 @@ "OnRename": "On Rename", "OnSeriesAdd": "On Series Add", "OnSeriesDelete": "On Series Delete", - "OnUpgrade": "On Upgrade", "OneMinute": "1 Minute", "OneSeason": "1 Season", "OnlyForBulkSeasonReleases": "Only for Bulk Season Releases", diff --git a/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs b/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs index f68fb40d2..35fa55469 100644 --- a/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs +++ b/src/NzbDrone.Core/Notifications/ImportCompleteMessage.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Notifications public List<Episode> Episodes { get; set; } public List<EpisodeFile> EpisodeFiles { get; set; } public string SourcePath { get; set; } + public string SourceTitle { get; set; } public DownloadClientItemClientInfo DownloadClientInfo { get; set; } public string DownloadId { get; set; } public GrabbedReleaseInfo Release { get; set; } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 88e19f423..94741adc8 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -268,6 +268,7 @@ namespace NzbDrone.Core.Notifications Episodes = episodes, EpisodeFiles = message.EpisodeFiles, SourcePath = message.SourcePath, + SourceTitle = parsedEpisodeInfo.ReleaseTitle, DestinationPath = message.EpisodeFiles.Select(e => Path.Join(series.Path, e.RelativePath)).ToList().GetLongestCommonPath(), ReleaseGroup = parsedEpisodeInfo.ReleaseGroup, ReleaseQuality = parsedEpisodeInfo.Quality From c01abbf3b524e5befbb065cc074d602ea8a5a86c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 21:57:40 -0700 Subject: [PATCH 369/762] Add 'On Import Complete' for Discord notifications --- .../Notifications/Discord/Discord.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 0f32c1f86..8ef8dbf62 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -236,6 +236,96 @@ namespace NzbDrone.Core.Notifications.Discord _proxy.SendPayload(payload, Settings); } + public override void OnImportComplete(ImportCompleteMessage message) + { + var series = message.Series; + var episodes = message.Episodes; + + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/256.png" + }, + Url = $"http://thetvdb.com/?tab=series&id={series.TvdbId}", + Description = "Import Complete", + Title = GetTitle(series, episodes), + Color = (int)DiscordColors.Success, + Fields = new List<DiscordField>(), + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + + if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) + { + embed.Thumbnail = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl + }; + } + + if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Fanart)) + { + embed.Image = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.RemoteUrl + }; + } + + foreach (var field in Settings.ImportFields) + { + var discordField = new DiscordField(); + + switch ((DiscordImportFieldType)field) + { + case DiscordImportFieldType.Overview: + var overview = episodes.First().Overview ?? ""; + discordField.Name = "Overview"; + discordField.Value = overview.Length <= 300 ? overview : $"{overview.AsSpan(0, 300)}..."; + break; + case DiscordImportFieldType.Rating: + discordField.Name = "Rating"; + discordField.Value = episodes.First().Ratings.Value.ToString(); + break; + case DiscordImportFieldType.Genres: + discordField.Name = "Genres"; + discordField.Value = series.Genres.Take(5).Join(", "); + break; + case DiscordImportFieldType.Quality: + discordField.Name = "Quality"; + discordField.Inline = true; + discordField.Value = message.ReleaseQuality.Quality.Name; + break; + case DiscordImportFieldType.Group: + discordField.Name = "Group"; + discordField.Value = message.ReleaseGroup; + break; + case DiscordImportFieldType.Size: + discordField.Name = "Size"; + discordField.Value = BytesToString(message.Release?.Size ?? message.EpisodeFiles.Sum(f => f.Size)); + discordField.Inline = true; + break; + case DiscordImportFieldType.Release: + discordField.Name = "Release"; + discordField.Value = $"```{message.Release?.Title ?? message.SourceTitle}```"; + break; + case DiscordImportFieldType.Links: + discordField.Name = "Links"; + discordField.Value = GetLinksString(series); + break; + } + + if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace()) + { + embed.Fields.Add(discordField); + } + } + + var payload = CreatePayload(null, new List<Embed> { embed }); + + _proxy.SendPayload(payload, Settings); + } + public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { var attachments = new List<Embed> From 3afae968ebc72ad35abe07cfc4b84d7e243b1840 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Jul 2024 12:03:53 -0700 Subject: [PATCH 370/762] Fixed: Import queue not processing after incomplete import --- src/NzbDrone.Core/Download/CompletedDownloadService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index b157012d3..d22589c37 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -235,8 +235,6 @@ namespace NzbDrone.Core.Download var atLeastOneEpisodeImported = importResults.Any(c => c.Result == ImportResultType.Imported); var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); - var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id)); - var files = _mediaFileService.GetFiles(episodes.Select(e => e.EpisodeFileId).Distinct()); if (allEpisodesImportedInHistory) { @@ -259,6 +257,9 @@ namespace NzbDrone.Core.Download .Write(); } + var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id)); + var files = _mediaFileService.GetFiles(episodes.Select(e => e.EpisodeFileId).Where(i => i > 0).Distinct()); + trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id, files, releaseInfo)); From ae4a97b4aef820a6383e2c0b8a5c59fd93aa09f9 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 15 Jul 2024 19:03:41 +0000 Subject: [PATCH 371/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dream <seth.gecko.rr@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - .../Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 542 +++++++++++++++++- .../Localization/Core/zh_CN.json | 1 - 6 files changed, 540 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 89850b375..38b150e65 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1848,7 +1848,6 @@ "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,", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index aafc5599f..fbe83bee3 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1304,7 +1304,6 @@ "Save": "Tallenna", "Seeders": "Jakajat", "UpgradesAllowed": "Päivitykset sallitaan", - "OnUpgrade": "Päivitettäessä", "UnmappedFilesOnly": "Vain kohdistamattomat tiedostot", "Ignored": "Ohitettu", "PreferAndUpgrade": "Suosi ja päivitä", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 4c6ba475d..14de69518 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -998,7 +998,6 @@ "NoEpisodeInformation": "Aucune information sur l'épisode n'est disponible.", "NoResultsFound": "Aucun résultat trouvé", "NotificationTriggers": "Déclencheurs de notifications", - "OnUpgrade": "Lors de la mise à niveau", "Other": "Autre", "OutputPath": "Chemin de sortie", "OverrideGrabNoEpisode": "Au moins un épisode doit être sélectionné", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 79e80f627..9065661fe 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -689,7 +689,6 @@ "OnRename": "Ao Renomear", "OnSeriesAdd": "Ao Adicionar Série", "OnSeriesDelete": "Ao Excluir Série", - "OnUpgrade": "Ao Atualizar", "OneMinute": "1 Minuto", "OnlyForBulkSeasonReleases": "Apenas para lançamentos de temporada em massa", "OnlyTorrent": "Só Torrent", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index cf5197d5c..e5b563a45 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -197,7 +197,7 @@ "AlreadyInYourLibrary": "Уже в вашей библиотеке", "Always": "Всегда", "Conditions": "Условия", - "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", + "AbsoluteEpisodeNumber": "Абсолютные номера эпизодов", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", "AnalyticsEnabledHelpText": "Отправлять в {appName} анонимную информацию об использовании и ошибках. Анонимная статистика включает в себя информацию о браузере, какие страницы веб-интерфейса {appName} загружены, сообщения об ошибках, а также операционной системе. Мы используем эту информацию для выявления ошибок, а также для разработки нового функционала.", @@ -216,5 +216,543 @@ "AddListExclusionError": "Не удалось добавить новое исключение из списка. Повторите попытку.", "AddImportListExclusionError": "Не удалось добавить новое исключение из списка импорта. Повторите попытку.", "AddListExclusion": "Добавить исключение из списка", - "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку." + "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку.", + "Blocklist": "Черный список", + "Connect": "Подключить", + "Username": "Пользователь", + "View": "Просмотр", + "EpisodeFileMissingTooltip": "Файл эпизода отсутствует", + "DownloadClientAriaSettingsDirectoryHelpText": "Опциональное местоположение для загрузок. Оставьте пустым, чтобы использовать местоположение Aria2 по умолчанию", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Нет пункта назначения по умолчанию", + "DownloadClientFreeboxApiError": "API Freebox вернул ошибку: {errorDescription}", + "DownloadClientFreeboxNotLoggedIn": "Не авторизован", + "DownloadClientFreeboxSettingsAppIdHelpText": "Идентификатор приложения, указанный при создании доступа к Freebox API (т. е. 'app_id')", + "Episode": "Эпизод", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "Для параметра NzbGet KeepHistory должно быть меньше 25000", + "UpgradesAllowedHelpText": "Если отключено, то качества не будут обновляться", + "UseSeasonFolderHelpText": "Сортировать эпизоды внутри папки сезона", + "EpisodeTitleRequired": "Требуется название эпизода", + "DownloadClientValidationTestTorrents": "Не удалось получить список торрентов: {exceptionMessage}", + "Downloaded": "Скачано", + "AddingTag": "Добавить тэг", + "EnableAutomaticAdd": "Включить автоматическое добавление", + "EpisodeTitle": "Название эпизода", + "DeleteSelectedEpisodeFilesHelpText": "Вы уверены, что хотите удалить выбранные файлы эпизода?", + "Calendar": "Календарь", + "CloneProfile": "Клонировать профиль", + "Ended": "Завершен", + "Download": "Скачать", + "DownloadClient": "Загрузочный клиент", + "Donate": "Пожертвовать", + "AnalyseVideoFiles": "Анализировать видео файлы", + "Analytics": "Аналитика", + "Anime": "Аниме", + "Any": "Любой", + "AppUpdated": "{appName} обновлен", + "AppUpdatedVersion": "Приложение {appName} обновлено до версии `{version}`. Чтобы получить последние изменения, вам необходимо перезагрузить приложение {appName} ", + "ApplyTagsHelpTextHowToApplyImportLists": "Как применить теги к выбранным спискам импорта", + "CancelPendingTask": "Вы уверены, что хотите убрать данную задачу из очереди?", + "CancelProcessing": "Отменить обработку", + "CertificateValidationHelpText": "Измените строгую проверку сертификации HTTPS. Не меняйте, если вы не понимаете риски.", + "Certification": "Возрастной рейтинг", + "ChangeFileDate": "Изменить дату файла", + "ClickToChangeQuality": "Нажмите чтобы изменить качество", + "ClickToChangeSeason": "Нажмите, чтобы изменить сезон", + "ClickToChangeSeries": "Нажмите, чтобы изменить сериал", + "CloneIndexer": "Клонировать индексер", + "BackupNow": "Создать резервную копию", + "UpdaterLogFiles": "Фалы журналов обновления", + "Updates": "Обновления", + "UpgradeUntil": "Обновить до качества", + "UrlBaseHelpText": "Для поддержки обратного прокси, по умолчанию пусто", + "AbsoluteEpisodeNumbers": "Абсолютные номера эпизодов", + "AddReleaseProfile": "Добавить профиль выпуска", + "AddRemotePathMapping": "Добавить удаленный путь", + "AirDate": "Дата выхода в эфир", + "AnimeEpisodeFormat": "Формат аниме-эпизода", + "AuthBasic": "Базовый (всплывающее окно браузера)", + "AuthForm": "Формы (Страница авторизации)", + "Authentication": "Аутентификация", + "AuthenticationRequired": "Требуется авторизация", + "BackupIntervalHelpText": "Периодичность автоматического резервного копирования", + "BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены", + "Backups": "Резервные копии", + "BackupsLoadError": "Невозможно загрузить резервные копии", + "BlocklistLoadError": "Не удалось загрузить черный список", + "Branch": "Ветвь", + "BranchUpdate": "Ветвь для обновления {appName}", + "ClientPriority": "Приоритет клиента", + "CopyUsingHardlinksHelpTextWarning": "Блокировка файлов иногда может мешать переименованию файлов во время раздачи. Можно временно приостановить раздачу и использовать функции {appName} для переименования.", + "CreateEmptySeriesFolders": "Создать пустые папки для сериалов", + "CreateGroup": "Создать группу", + "CurrentlyInstalled": "Установлено", + "CustomFormatScore": "Настраиваемый формат оценки", + "DelayMinutes": "{delay} Минуты", + "DailyEpisodeFormat": "Формат ежедневных эпизодов", + "DelayProfilesLoadError": "Невозможно загрузить профили задержки", + "DelayProfile": "Профиль задержки", + "DeleteReleaseProfile": "Удалить профиль релиза", + "DeleteSpecificationHelpText": "Вы уверены, что хотите удалить уведомление '{name}'?", + "ChooseAnotherFolder": "Выбрать другой каталог", + "DownloadClients": "Клиенты для скачивания", + "DownloadPropersAndRepacks": "Проперы и репаки", + "Edit": "Редактирование", + "Duration": "Длительность", + "EnableColorImpairedMode": "Включить режим для слабовидящих", + "EnableProfileHelpText": "Установите флажок, чтобы включить профиль релиза", + "EnableRss": "Включить RSS", + "CollectionsLoadError": "Не удалось загрузить коллекции", + "ColonReplacement": "Замена двоеточий", + "UpgradesAllowed": "Обновления разрешены", + "DoNotPrefer": "Не предпочитать", + "Donations": "Пожертвования", + "ConnectionSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {connectionName}, например {url}", + "CopyToClipboard": "Копировать в буфер обмена", + "Custom": "Настраиваемый", + "CustomFilter": "Настраиваемые фильтры", + "DeleteEpisodeFromDisk": "Удалить эпизод с диска", + "CustomFormatsSettingsSummary": "Пользовательские форматы и настройки", + "DeleteSeriesFolder": "Удалить папку сериала", + "DeleteSpecification": "Удалить уведомление", + "Deleted": "Удалено", + "DeletedReasonUpgrade": "Файл был удален чтобы импортировать обновление", + "DetailedProgressBar": "Подробный индикатор выполнения", + "Directory": "Каталог", + "DownloadClientDelugeValidationLabelPluginFailure": "Не удалось настроить метку", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Вы должны войти в свою Diskstation как {username} и вручную настроить ее в настройках DownloadStation в разделе BT/HTTP/FTP/NZB -> Местоположение", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Добавить теги после импорта загрузки", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Определите базовый URL-адрес Freebox API с версией API, например '{url}', по умолчанию — '{defaultApiUrl}'", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Первый и последний Первый", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Не удалось настроить категорию", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "Пользователю {appName} не удалось добавить метку в qBittorrent", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категории не поддерживаются до версии qBittorrent 3.3.0. Пожалуйста, обновите версию или повторите попытку, указав пустую категорию", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Очередь не включена", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent настроен на удаление торрентов, когда они достигают предельного рейтинга (Ratio)", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Отключите опцию «Проверить перед загрузкой» в Sabnbzd", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Вы должны отключить сортировку фильмов для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог Transmission по умолчанию", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу RPC {clientName}, например {url}, по умолчанию — '{defaultUrl}'", + "EditQualityProfile": "Редактировать профиль качества", + "DownloadClientValidationAuthenticationFailure": "Ошибка аутентификации", + "DownloadClientValidationCategoryMissing": "Категория не существует", + "DownloadClientValidationUnableToConnect": "Невозможно подключиться к {clientName}", + "DownloadClientValidationTestNzbs": "Не удалось получить список NZB: {exceptionMessage}", + "DownloadFailedEpisodeTooltip": "Загрузка эпизода не удалась", + "DownloadFailed": "Неудачное скачивание", + "DownloadStationStatusExtracting": "Извлечение: {progress}%", + "EditCustomFormat": "Редактировать пользовательский формат", + "EditConnectionImplementation": "Добавить соединение - {implementationName}", + "EditMetadata": "Редактировать метаданные {metadataType}", + "EditRemotePathMapping": "Редактировать расположение подключенной папки", + "EnableInteractiveSearchHelpTextWarning": "Поиск не поддерживается с этим индексатором", + "EpisodeFileDeleted": "Файл эпизода удален", + "EpisodeCount": "Количество эпизодов", + "UpgradeUntilThisQualityIsMetOrExceeded": "Обновлять, пока это качество не будет достигнуто или превышено", + "AuthenticationRequiredWarning": "Чтобы предотвратить удаленный доступ без авторизации, {appName} теперь требует, чтобы авторизация была включена. При желании вы можете отключить авторизацию с локальных адресов.", + "AutoTagging": "Автоматическая маркировка", + "AutoTaggingLoadError": "Не удается загрузить автоматическую маркировку", + "AutoTaggingRequiredHelpText": "Это условие {implementationName} должно соответствовать правилу автоматической пометки. В противном случае достаточно одного совпадения {implementationName}", + "Automatic": "Автоматически", + "AutomaticAdd": "Автоматическое добавление", + "AutomaticSearch": "Автоматический поиск", + "Backup": "Резервное копирование", + "BackupFolderHelpText": "Относительные пути будут в каталоге AppData {appName}", + "Connection": "Подключение", + "CountSeasons": "{count} Сезоны", + "DownloadIgnored": "Загрузка игнорируется", + "EditGroups": "Редактировать группы", + "CalendarLoadError": "Не удалось загрузить календарь", + "CertificateValidation": "Проверка сертификата", + "DeleteQualityProfile": "Удалить качественный профиль", + "DotNetVersion": ".NET", + "DoneEditingGroups": "Группы редактирования сделаны", + "CustomColonReplacementFormatHelpText": "Символы, которые будут использоваться вместо двоеточий", + "CustomColonReplacementFormatHint": "Допустимый символ файловой системы, например двоеточие (буква)", + "Debug": "Отладка", + "DeleteEpisodeFileMessage": "Вы уверены, что хотите удалить '{path}'?", + "DeleteEpisodesFiles": "Удалить файлы эпизодов ({episodeFileCount})", + "DeleteImportListExclusion": "Удалить лист исключения для импорта", + "DeleteIndexer": "Удалить индексер", + "DeleteNotification": "Удалить уведомление", + "DeleteSelectedEpisodeFiles": "Удалить выбранные эпизоды сериала", + "DeleteSeriesFolderConfirmation": "Папка с сериалом '{path}' и все ее содержимое будут удалены.", + "DeleteSeriesFolderCountConfirmation": "Вы уверены, что хотите удалить {count} выбранных эпизодов ?", + "DeleteSeriesFolderCountWithFilesConfirmation": "Вы уверены, что хотите удалить {count} выбранных сериалов и все их содержимое?", + "DeleteSeriesFolderEpisodeCount": "Файлы эпизодов: {episodeFileCount}, общим размером {size}", + "DeleteSeriesFoldersHelpText": "Удалить папки сериалов и все их содержимое", + "DeletedSeriesDescription": "Сериал был удален из TheTVDB", + "DestinationRelativePath": "Относительный путь назначения", + "DetailedProgressBarHelpText": "Показать текст на индикаторе выполнения", + "Details": "Подробности", + "DiskSpace": "Дисковое пространство", + "DoNotBlocklist": "Не вносить в черный список", + "DockerUpdater": "Обновить контейнер, чтобы получить обновление", + "DownloadClientDelugeTorrentStateError": "Deluge сообщает об ошибке", + "DownloadClientDelugeValidationLabelPluginInactive": "Плагин меток не активирован", + "DownloadClientDownloadStationProviderMessage": "Приложение {appName} не может подключиться к Download Station, если в вашей учетной записи DSM включена двухфакторная аутентификация", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Опциональная общая папка для размещения загрузок. Оставьте пустым, чтобы использовать каталог Download Station по умолчанию", + "DownloadClientDownloadStationValidationApiVersion": "Версия Download Station API не поддерживается. Она должна быть не ниже {requiredVersion}. Поддерживается от {minVersion} до {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissing": "Папка не существует", + "DownloadClientDownloadStationValidationFolderMissingDetail": "Папка '{downloadDir}' не существует, ее необходимо создать вручную внутри общей папки '{sharedFolder}'.", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Общая папка не существует", + "DownloadClientFloodSettingsAdditionalTags": "Дополнительные теги", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавляет свойства мультимедиа в виде тегов. Подсказки являются примерами", + "DownloadClientFloodSettingsStartOnAdd": "Начать добавление", + "DownloadClientFloodSettingsUrlBaseHelpText": "Добавляет префикс к Flood API, например {url}", + "DownloadClientFreeboxAuthenticationError": "Не удалось выполнить аутентификацию в API Freebox. Причина: {errorDescription}", + "DownloadClientFreeboxSettingsApiUrl": "URL-адрес API", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Токен приложения, полученный при создании доступа к API Freebox (т. е. 'app_token')", + "DownloadClientFreeboxSettingsPortHelpText": "Порт, используемый для доступа к интерфейсу Freebox, по умолчанию — '{port}'", + "DownloadClientFreeboxUnableToReachFreebox": "Невозможно получить доступ к API Freebox. Проверьте настройки «Хост», «Порт» или «Использовать SSL». (Ошибка: {exceptionMessage})", + "DownloadClientNzbVortexMultipleFilesMessage": "Загрузка содержит несколько файлов и находится не в папке задания: {outputPath}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Для этой опции требуется как минимум NzbGet версии 16.0", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Для параметра NzbGet KeepHistory установлено слишком высокое значение", + "DownloadClientNzbgetValidationKeepHistoryZero": "Параметр NzbGet KeepHistory должен быть больше 0", + "DownloadClientOptionsLoadError": "Не удалось загрузить параметры клиента загрузки", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Эта папка должна быть доступна из XBMC", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Файлы .strm в этой папке будут импортированы дроном", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Использовать ли настроенный макет контента qBittorrent, исходный макет из торрента или всегда создавать подпапку (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Последовательный порядок", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Используйте безопасное соединение. См. «Параметры» -> «Веб-интерфейс» -> «Использовать HTTPS вместо HTTP» в qBittorrent", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent не может разрешить магнитную ссылку с отключенным DHT", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent загружает метаданные", + "DownloadClientQbittorrentTorrentStatePathError": "Невозможно импортировать. Путь соответствует базовому каталогу загрузки клиента, возможно, для этого торрента отключен параметр «Сохранить папку верхнего уровня» или для параметра «Макет содержимого торрента» НЕ установлено значение «Исходный» или «Создать подпапку»?", + "DownloadClientQbittorrentTorrentStateUnknown": "Неизвестное состояние загрузки: {state}", + "DownloadClientQbittorrentValidationCategoryRecommended": "Категория рекомендуется", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} не сможет выполнить обработку завершенной загрузки, как настроено. Вы можете исправить это в qBittorrent («Инструменты -> Параметры...» в меню), изменив «Параметры -> BitTorrent -> Ограничение доли ресурсов» с «Удалить их» на «Приостановить их»", + "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd разрабатывает версию, предполагающую версию 3.0.0 или выше.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} может не поддерживать новые функции, добавленные в SABnzbd при запуске разрабатываемых версий.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Включить папки заданий", + "DownloadClientSeriesTagHelpText": "Используйте этот загрузочный клиент только для сериалов, имеющих хотя бы один соответствующий тег. Оставьте поле пустым, чтобы использовать его для всех серий.", + "DownloadClientSettings": "Настройки клиента скачиваний", + "DownloadClientValidationApiKeyIncorrect": "Ключ API неверен", + "DownloadClientValidationCategoryMissingDetail": "Введенная вами категория не существует в {clientName}. Сначала создайте ее в {clientName}.", + "DownloadClientValidationErrorVersion": "Версия {clientName} должна быть не ниже {requiredVersion}. Сообщенная версия: {reportedVersion}", + "DownloadClientValidationUnableToConnectDetail": "Пожалуйста, проверьте имя хоста и порт.", + "DownloadClientVuzeValidationErrorVersion": "Версия протокола не поддерживается, используйте Vuze 5.0.0.0 или выше с плагином Vuze Web Remote.", + "DownloadIgnoredEpisodeTooltip": "Загрузка эпизода проигнорирована", + "DownloadPropersAndRepacksHelpText": "Следует ли автоматически обновляться до Propers / Repacks", + "EditDelayProfile": "Редактировать профиль задержки", + "EditImportListExclusion": "Редактировать лист исключения для импорта", + "EditListExclusion": "Изменить список исключений", + "EnableAutomaticAddSeriesHelpText": "Добавляйте сериалы из этого списка в {appName}, когда синхронизация выполняется через пользовательский интерфейс или с помощью {appName}", + "EnableAutomaticSearchHelpText": "Будет использовано для автоматических поисков через интерфейс или {appName}", + "EndedOnly": "Только завершенные", + "EpisodeFileDeletedTooltip": "Файл эпизода удален", + "EpisodeHistoryLoadError": "Не удалось загрузить историю эпизодов", + "EpisodeImported": "Эпизод импортирован", + "EpisodeImportedTooltip": "Эпизод успешно загружен и получен из загрузочного клиента", + "EpisodeIsDownloading": "Эпизод загружается", + "EpisodeIsNotMonitored": "Эпизод не отслеживается", + "EpisodeMissingAbsoluteNumber": "Эпизод не имеет абсолютного номера эпизода", + "EpisodeNaming": "Именование эпизодов", + "EpisodeNumbers": "Номер(а) эпизодов", + "EpisodeSearchResultsLoadError": "Невозможно загрузить результаты для этого поискового запроса. Повторите попытку позже", + "Episodes": "Эпизоды", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Скачать в последовательном порядке (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsContentLayout": "Макет контента", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Категория не поддерживается", + "DownloadClientValidationGroupMissingDetail": "Введенная вами группа не существует в {clientName}. Сначала создайте ее в {clientName}.", + "DownloadClientValidationGroupMissing": "Группа не существует", + "DownloadClientValidationSslConnectFailure": "Невозможно подключиться через SSL", + "EnableAutomaticSearchHelpTextWarning": "Будет использовано при автоматических поисках", + "EnableCompletedDownloadHandlingHelpText": "Автоматически импортировать завершенные скачивания", + "EnableMetadataHelpText": "Включить создание файла метаданных для этого типа метаданных", + "EnableProfile": "Включить профиль", + "EndedSeriesDescription": "Никаких дополнительных эпизодов и сезонов не ожидается", + "EpisodeAirDate": "Дата выхода эпизода", + "EpisodeGrabbedTooltip": "Эпизод получен из {indexer} и отправлен в {downloadClient}", + "CheckDownloadClientForDetails": "проверьте клиент загрузки для более подробной информации", + "AudioInfo": "Информация о аудио", + "ChmodFolderHelpText": "Восьмеричный, применяется при импорте / переименовании к медиа-папкам и файлам (без битов выполнения)", + "Close": "Закрыть", + "AuthenticationRequiredHelpText": "Отредактируйте, для каких запросов требуется аутентификация. Не меняйте, пока не поймете все риски.", + "Day": "День", + "UpdateStartupNotWritableHealthCheckMessage": "Невозможно установить обновление так как загрузочная папка '{startupFolder}' недоступна для записи для пользователя '{userName}'.", + "UpdateUiNotWritableHealthCheckMessage": "Невозможно установить обновление так как UI папка '{uiFolder}' недоступна для записи для пользователя '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Не удается установить обновление, поскольку папка автозагрузки \"{startupFolder}\" находится в папке перемещения приложений.", + "Uptime": "Время работы", + "Uppercase": "Верхний регистр", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "{appName} перестанет скачивать фильмы после достижения указанного количества очков", + "UpgradeUntilCustomFormatScore": "Обновлять до пользовательской оценки", + "UsenetDelayHelpText": "Задержка в минутах перед получением релиза из Usenet", + "UsenetDelay": "Usenet задержки", + "UseHardlinksInsteadOfCopy": "Используйте жесткие ссылки вместо копирования", + "UrlBase": "Базовый URL", + "VideoCodec": "Видео кодеки", + "VersionNumber": "Версия {version}", + "Version": "Версия", + "UsenetDisabled": "Usenet отключён", + "Wanted": "Разыскиваемый", + "WaitingToProcess": "Ожидает обработки", + "WaitingToImport": "Ожидание импорта", + "VisitTheWikiForMoreDetails": "Перейти в wiki: ", + "Continuing": "В стадии показа (или между сезонами)", + "BlackholeFolderHelpText": "Папка, в которой {appName} будет хранить файл {extension}", + "BlackholeWatchFolder": "Смотреть папку", + "Category": "Категория", + "ChangeFileDateHelpText": "Заменить дату файла при импорте/сканировании", + "ChmodFolder": "chmod Папка", + "ChownGroup": "chown группа", + "ChownGroupHelpText": "Имя группы или id. Используйте id для отдалённой файловой системы.", + "Condition": "Условие", + "CountSelectedFile": "{selectedCount} выбранный файл", + "EpisodeFileRenamed": "Файл эпизода переименован", + "DeleteBackup": "Удалить резервную копию", + "Cutoff": "Прекращение", + "DeleteSelectedSeries": "Удалить выбранный сериал", + "DeleteSeriesModalHeader": "Удалить - {title}", + "DeleteTag": "Удалить тэг", + "CutoffUnmet": "Порог невыполнен", + "Dates": "Даты", + "DefaultCase": "Случай по умолчанию", + "DelayProfiles": "Профиль задержки", + "DeleteEpisodeFile": "Удалить файл эпизода", + "DownloadClientFloodSettingsPostImportTags": "Пост-импортные теги", + "DownloadClientSettingsRecentPriority": "Недавний приоритет", + "DeleteCustomFormat": "Удалить пользовательский формат", + "DeleteDelayProfile": "Удалить профиль задержки", + "DownloadClientSettingsInitialState": "Начальное состояние", + "DownloadClientSettingsOlderPriority": "Более старый приоритет", + "DownloadClientValidationVerifySsl": "Проверьте настройки SSL", + "DownloadClientValidationUnknownException": "Неизвестное исключение: {exception}", + "Agenda": "План", + "Apply": "Применить", + "ApplyTags": "Применить тэги", + "DownloadClientSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {clientName}, например {url}", + "UpgradeUntilEpisodeHelpText": "{appName} перестанет скачивать фильмы после достижения указанного качества", + "UseProxy": "Использовать прокси", + "AddNewSeriesHelpText": "Добавить новый сериал очень просто! Начни печатать название сериала, который хочешь добавить", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Начать поиск отклонённых эпизодов", + "EditSelectedSeries": "Редактировать выбранный сериал", + "CompletedDownloadHandling": "Обработка завершенных скачиваний", + "Component": "Компонент", + "CustomFilters": "Настраиваемые фильтры", + "ApplicationURL": "URL-адрес приложения", + "ApplicationUrlHelpText": "Внешний URL-адрес этого приложения, включая http(s)://, порт и базовый URL-адрес", + "Disabled": "Выключено", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Вы должны отключить сортировку сериалов для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", + "UseSeasonFolder": "Использовать папку сезона", + "AutoTaggingSpecificationTag": "Тэг", + "BlocklistReleaseHelpText": "Блокирует повторную загрузку этого релиза пользователем {appName} через RSS или автоматический поиск", + "BuiltIn": "Встроено", + "BypassProxyForLocalAddresses": "Обход прокси для локальных адресов", + "ChangeCategory": "Изменить категорию", + "Clear": "Очистить", + "ClearBlocklistMessageText": "Вы уверены, что хотите удалить выбранные элементы из черного списка?", + "ClickToChangeEpisode": "Нажмите, чтобы изменить эпизод", + "ClickToChangeLanguage": "Нажмите чтобы сменить язык", + "Clone": "Клонировать", + "CollapseMultipleEpisodes": "Свернуть несколько эпизодов", + "DeleteSeriesFolderHelpText": "Удалить папку с сериалом и её содержимое", + "DestinationPath": "Путь назначения", + "DeleteSeriesFolders": "Удалить папки сериалов", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent сообщает об ошибке", + "DownloadClientQbittorrentTorrentStateStalled": "Загрузка останавливается без подключения", + "DownloadClientFreeboxSettingsAppToken": "Токен приложения", + "DownloadClientValidationApiKeyRequired": "Требуется ключ API", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Приоритет при выборе эпизодов, вышедших в эфир более 14 дней назад", + "DownloadClientsSettingsSummary": "Программы для скачивания, обработка скаченного и отдалённые ссылки", + "Duplicate": "Дублировать", + "EditSeriesModalHeader": "Изменить – {title}", + "EditRestriction": "Редактировать ограничения", + "EpisodeFileRenamedTooltip": "Файл эпизода переименован", + "EnableColorImpairedModeHelpText": "Измененный стиль, позволяющий пользователям с нарушением цвета лучше различать информацию с цветовой кодировкой", + "EpisodeTitleRequiredHelpText": "Запретить импорт на срок до 48 часов, если название эпизода указано в формате имени, а название эпизода будет объявлено позднее", + "AutoTaggingSpecificationGenre": "Жанры", + "AutoTaggingSpecificationMaximumYear": "Максимальный год", + "AutoTaggingSpecificationMinimumYear": "Минимальный год", + "CalendarLegendEpisodeUnmonitoredTooltip": "Эпизод не отслеживается", + "CalendarLegendSeriesFinaleTooltip": "Финал сериала или сезона", + "CalendarLegendSeriesPremiereTooltip": "Премьера сериала или сезона", + "ChmodFolderHelpTextWarning": "Это работает только если пользователь {appName} является владельцем файла. Проверьте, что программа для скачивания установила правильные разрешения.", + "ChooseImportMode": "Выберите режим импорта", + "ClearBlocklist": "Очистить черный список", + "ClickToChangeReleaseGroup": "Нажмите, чтобы изменить релиз-группу", + "CustomFormatJson": "Настраиваемый формат JSON", + "CustomFormats": "Настраиваемое форматирование", + "CustomFormatUnknownConditionOption": "Неизвестный параметр «{key}» для условия '{implementation}'", + "CustomFormatsSpecificationMinimumSizeHelpText": "Релиз должен быть больше этого размера", + "CustomFormatsSpecificationMaximumSizeHelpText": "Релиз должен быть меньше или равен этому размеру", + "CustomFormatsSpecificationRegularExpressionHelpText": "RegEx пользовательского формата не чувствителен к регистру", + "Date": "Дата", + "DelayProfileProtocol": "Протокол: {preferredProtocol}", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Сначала скачайте первую и последнюю части (qBittorrent 4.1.0+)", + "DownloadClientValidationVerifySslDetail": "Проверьте конфигурацию SSL на {clientName} и {appName}", + "EnableSsl": "Включить SSL", + "Enable": "Включить", + "AnimeEpisodeTypeFormat": "Абсолютный номер эпизода ({format})", + "CalendarLegendEpisodeDownloadedTooltip": "Сериал скачан и отсортирован", + "CalendarLegendEpisodeDownloadingTooltip": "Эпизод в настоящее время загружается", + "AutoRedownloadFailed": "Неудачное скачивание", + "AutoRedownloadFailedFromInteractiveSearch": "Не удалось выполнить повторную загрузку из интерактивного поиска", + "AutoRedownloadFailedHelpText": "Автоматически искать и пытаться скачать разные релизы", + "BeforeUpdate": "До обновления", + "CalendarFeed": "Лента календаря {appName}", + "AddRootFolderError": "Невозможно загрузить корневые папки", + "BlocklistReleases": "Релиз из черного списка", + "BypassDelayIfAboveCustomFormatScore": "Пропустить, если значение больше пользовательского формата", + "CalendarLegendEpisodeMissingTooltip": "Эпизод вышел в эфир и отсутствует на диске", + "CalendarLegendEpisodeOnAirTooltip": "Эпизод сейчас в эфире", + "CalendarLegendEpisodeUnairedTooltip": "Эпизод еще не вышел в эфир", + "Cancel": "Отменить", + "Destination": "Место назначения", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. в {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Отключить сортировку фильмов", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Отключить сортировку дат", + "DownloadClientUTorrentTorrentStateError": "uTorrent сообщает об ошибке", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb папка", + "DownloadClientPneumaticSettingsStrmFolder": "Strm папка", + "DownloadClientFreeboxSettingsHostHelpText": "Имя хоста или IP-адрес хоста Freebox, по умолчанию — '{url}' (будет работать только в той же сети)", + "DownloadClientSettingsDestinationHelpText": "Вручную указывает место назначения загрузки. Оставьте поле пустым, чтобы использовать значение по умолчанию", + "Downloading": "Скачивается", + "DownloadClientSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в {clientName}", + "Warn": "Предупреждение", + "CustomFormatsSettingsTriggerInfo": "Пользовательский формат будет применен к релизу или файлу, если он соответствует хотя бы одному из каждого из выбранных типов условий", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не будет пытаться импортировать завершенные загрузки без категории", + "DownloadClientRTorrentSettingsUrlPath": "URL-путь", + "DownloadClientSettingsUseSslHelpText": "Использовать безопасное соединение при подключении к {clientName}", + "DownloadClientSettingsCategorySubFolderHelpText": "Добавление категории, специфичной для {appName}, позволяет избежать конфликтов с несвязанными загрузками, не относящимися к {appName}. Использование категории не является обязательным, но настоятельно рекомендуется. Создает подкаталог [category] в выходном каталоге.", + "DownloadClientSabnzbdValidationUnknownVersion": "Неизвестная версия: {rawVersion}", + "DownloadClientSettingsAddPaused": "Добавить приостановленное", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Отключить сортировку сериалов", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} предпочитает, чтобы каждая загрузка имела отдельную папку. Если к папке/пути добавлен *, Sabnzbd не будет создавать эти папки заданий. Отправляйтесь в Sabnzbd, чтобы исправить это.", + "ConnectionLost": "Соединение прервано", + "CleanLibraryLevel": "Очистить уровень библиотеки", + "ColonReplacementFormatHelpText": "Изменить как Sonarr обрабатывает замену двоеточий", + "ConnectionLostReconnect": "Sonarr попытается соединиться автоматически или нажмите кнопку внизу", + "ConnectSettingsSummary": "Уведомления, подключения к серверам/проигрывателям и настраиваемые скрипты", + "ContinuingOnly": "Только в стадии показа", + "Connections": "Соединения", + "DailyEpisodeTypeDescription": "Эпизоды, выходящие ежедневно или реже, в которых используются год-месяц-день (2023-08-04)", + "DailyEpisodeTypeFormat": "Дата ({format})", + "Database": "База данных", + "DefaultDelayProfileSeries": "Это профиль по умолчанию. Он относится ко всем сериалам, у которых нет явного профиля", + "DefaultNameCopiedProfile": "{name} - Копировать", + "DeleteEmptyFolders": "Удалить пустые папки", + "CutoffUnmetLoadError": "Ошибка при загрузке элементов не выполнивших порог", + "DoNotBlocklistHint": "Удалить без внесения в черный список", + "DoNotUpgradeAutomatically": "Не обновлять автоматически", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу json deluge, см. {url}", + "EpisodeInfo": "Информация об эпизоде", + "DownloadClientDelugeSettingsDirectory": "Каталог загрузки", + "DownloadClientDelugeSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог Deluge по умолчанию", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "На Diskstation нет общей папки с именем '{sharedFolder}', вы уверены, что указали ее правильно?", + "DownloadClientDelugeSettingsDirectoryCompleted": "Переместить каталог по завершении", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Опциональное место для перемещения завершенных загрузок. Оставьте пустым, чтобы использовать местоположение Deluge по умолчанию", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Подтвердите новый пароль", + "BypassDelayIfHighestQuality": "Игнорировать при максимальном качестве", + "BypassDelayIfHighestQualityHelpText": "Игнорирование задержки, когда выпуск имеет максимальное качество в выбранном профиле качества с предпочитаемым протоколом", + "UseSsl": "Использовать SSL", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent сообщает об отсутствующих файлах", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог rTorrent по умолчанию", + "BlocklistAndSearch": "Черный список и поиск", + "EnableSslHelpText": "Требуется перезапуск от администратора", + "EpisodeDownloaded": "Эпизод скачан", + "DeleteCustomFormatMessageText": "Вы уверены, что хотите удалить пользовательский формат '{name}'?", + "AddNewSeries": "Добавить новый эпизод", + "AddNewSeriesSearchForMissingEpisodes": "Начать поиск отсутствующих эпизодов", + "AddRemotePathMappingError": "Не удалось добавить новый удаленный путь, попробуйте еще раз.", + "AddSeriesWithTitle": "Добавить {title}", + "AllSeriesInRootFolderHaveBeenImported": "Все сериалы из {path} были импортированы", + "AllTitles": "Все заголовки", + "AnimeEpisodeTypeDescription": "Эпизоды выпущены с использованием абсолютного номера эпизода", + "ApiKey": "API ключ", + "AptUpdater": "Используйте apt для установки обновления", + "AuthenticationMethodHelpText": "Необходим логин и пароль для доступа в {appName}", + "AutoAdd": "Автоматическое добавление", + "AutoTaggingSpecificationOriginalLanguage": "Язык", + "AutoTaggingSpecificationQualityProfile": "Профиль качества", + "AutoTaggingSpecificationRootFolder": "Корневой каталог", + "AutoTaggingSpecificationSeriesType": "Тип эпизода", + "AutoTaggingSpecificationStatus": "Статус", + "BlocklistAndSearchHint": "Начать поиск для замены после внесения в черный список", + "BlocklistMultipleOnlyHint": "Черный список без поиска замен", + "BlocklistOnly": "Только черный список", + "BlocklistOnlyHint": "Черный список без поиска замен", + "BrowserReloadRequired": "Требуется перезагрузка браузера", + "DatabaseMigration": "Перенос БД", + "CountSelectedFiles": "{selectedCount} выбранные файлы", + "CountSeriesSelected": "{count} сериалов выбрано", + "CustomFormat": "Настраиваемый формат", + "EpisodeFilesLoadError": "Невозможно загрузить файлы эпизодов", + "EpisodeHasNotAired": "Эпизод не вышел в эфир", + "EpisodeMissingFromDisk": "Эпизод отсутствует на диске", + "EpisodeProgress": "Прогресс эпизода", + "EditReleaseProfile": "Редактировать профиль релиза", + "BlocklistFilterHasNoItems": "Выбранный фильтр черного списка не содержит элементов", + "ClickToChangeReleaseType": "Нажмите, чтобы изменить тип релиза", + "ClickToChangeIndexerFlags": "Нажмите, чтобы изменить флаги индексатора", + "CloneCustomFormat": "Клонировать пользовательский формат", + "CollapseAll": "Свернуть Все", + "CollapseMultipleEpisodesHelpText": "Свернуть несколько серий, выходящих в эфир в один и тот же день", + "ConnectSettings": "Настройки соединения", + "ContinuingSeriesDescription": "Ожидается больше эпизодов/еще один сезон", + "CopyUsingHardlinksSeriesHelpText": "Прямые ссылки позволяют {appName} импортировать исходные торренты в папку с сериалами, не занимая дополнительного места на диске и не копируя все содержимое файла. Прямые ссылки будут работать только в том случае, если источник и пункт назначения находятся на одном томе", + "DefaultNotFoundMessage": "Вы, должно быть, заблудились, здесь не на что смотреть.", + "CustomFormatsSpecificationFlag": "Флаг", + "DefaultNameCopiedSpecification": "{name} - Копировать", + "Delete": "Удалить", + "DownloadClientFreeboxSettingsAppId": "Идентификатор приложения", + "AddNewSeriesRootFolderHelpText": "Подпапка \"{0}\" будет создана автоматически", + "AnalyseVideoFilesHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Это может привести к высокой активности диска и сети во время сканирования.", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Как применить теги к выбранным клиентам загрузки", + "ChangeCategoryHint": "Перенести загружаемое в «Категорию после импорта» из клиента загрузки", + "ChownGroupHelpTextWarning": "Это работает только если пользователь {appName} является владельцем файла. Проверьте, что программа для скачивания использует туже самую группу, что и {appName}.", + "ConditionUsingRegularExpressions": "Это условие соответствует использованию регулярных выражений. Обратите внимание, что символы `\\^$.|?*+()[{` имеют особое значение и требуют экранирования с помощью `\\`", + "CreateEmptySeriesFoldersHelpText": "Создать папки для не найденных сериалов при сканировании", + "DownloadClientValidationAuthenticationFailureDetail": "Пожалуйста, подтвердите свое имя пользователя и пароль. Также проверьте, не заблокирован ли хост, на котором работает {appName}, доступ к {clientName} ограничениями белого списка в конфигурации {clientName}.", + "AirsTomorrowOn": "Завтра в {time} на {networkLabel}", + "AlternateTitles": "Альтернативное название", + "ApplyTagsHelpTextHowToApplySeries": "Как применить теги к выбранным сериалам", + "ChangeCategoryMultipleHint": "Перенести загружаемое в «Категорию после импорта» из клиента загрузки", + "DownloadClientFloodSettingsRemovalInfo": "{appName} будет автоматически удалять торренты на основе текущих критериев раздачи в Настройки -> Индексаторы", + "DownloadClientValidationSslConnectFailureDetail": "{appName} не может подключиться к {clientName} с помощью SSL. Эта проблема может быть связана с компьютером. Попробуйте настроить {appName} и {clientName} так, чтобы они не использовали SSL.", + "AnEpisodeIsDownloading": "Эпизод загружается", + "ConnectionLostToBackend": "Sonarr потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", + "CustomFormatHelpText": "Sonarr оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", + "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите действительный метод аутентификации", + "CustomFormatUnknownCondition": "Неизвестное условие пользовательского формата '{implementation}'", + "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это позволяет избежать конфликтов с несвязанными загрузками", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Используйте 'Не предпочитать' для сортировки по рейтингу пользовательского формата по сравнению с Propers / Repacks", + "DownloadPropersAndRepacksHelpTextWarning": "Используйте настраиваемое форматирование для автоматических обновлений до Проперов/Репаков", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматический поиск и попытка загрузки другого релиза, если неудачный релиз был получен из интерактивного поиска", + "EnableHelpText": "Включить создание файла метаданных для этого типа метаданных", + "AutoTaggingNegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {0}.", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Невозможно получить доступ к API Freebox. Проверьте настройку «URL-адрес API» для базового URL-адреса и версии", + "EnableInteractiveSearchHelpText": "Будет использовано при интерактивном поиске", + "DelayProfileSeriesTagsHelpText": "Применимо к сериаламс хотя бы одним подходящим тегом", + "EnableMediaInfoHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого требуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска или сети во время сканирования", + "AutomaticUpdatesDisabledDocker": "Автоматические обновления напрямую не поддерживаются при использовании механизма обновления Docker. Вам нужно будет обновить образ контейнера за пределами {AppName} или использовать скрипт", + "DelayingDownloadUntil": "Приостановить скачивание до {date} в {time}", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Для параметра NzbGet KeepHistory установлено значение 0. Это не позволяет {appName} видеть завершенные загрузки.", + "EnableRssHelpText": "Будет использоваться, когда {appName} будет периодически искать выпуски через RSS Sync", + "BindAddress": "Привязать адрес", + "DeleteEmptySeriesFoldersHelpText": "Удалять пустые папки во время сканирования диска, а так же после удаления файлов сериала", + "EpisodeRequested": "Запрошен эпизод", + "BindAddressHelpText": "Действительный IP-адрес, локальный адрес или '*' для всех интерфейсов", + "DeleteEpisodesFilesHelpText": "Удалить файлы и папку сериала", + "DownloadClientPriorityHelpText": "Приоритет клиента загрузки от 1 (самый высокий) до 50 (самый низкий). По умолчанию: 1. Для клиентов с одинаковым приоритетом используется циклический перебор", + "EpisodeTitleFootNote": "При необходимости можно управлять усечением до максимального количества байтов, включая многоточие (`...`). Поддерживается усечение как с конца (например, `{Episode Title:30}`), так и с начала (например, `{Episode Title:-30}`). При необходимости названия эпизодов будут автоматически обрезаны в соответствии с ограничениями файловой системы.", + "BlackholeWatchFolderHelpText": "Папка, из которой {appName} должно импортировать завершенные загрузки", + "DeleteReleaseProfileMessageText": "Вы действительно хотите удалить профиль релиза '{name}'?", + "BlocklistAndSearchMultipleHint": "Начать поиск для замены после внесения в черный список", + "DeletedReasonEpisodeMissingFromDisk": "Sonarr не смог найти файл на диске, поэтому файл был откреплён от эпизода в базе данных", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в qBittorrent. Обратите внимание, что принудительные торренты не подчиняются ограничениям на раздачу", + "BlocklistRelease": "Релиз из черного списка", + "DeletedReasonManual": "Файл был удален с помощью Sonarr вручную или с помощью другого инструмента через API", + "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Очередь торрентов не включена в настройках qBittorrent. Включите его в qBittorrent или выберите «Последний» в качестве приоритета", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "Пользователю {appName} не удалось добавить метку к клиенту {clientName}", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Чтобы использовать категории, у вас должен быть включен плагин меток в {clientName}", + "DownloadClientRTorrentProviderMessage": "rTorrent не будет приостанавливать торренты, если они соответствуют критериям раздачи. {appName} будет обрабатывать автоматическое удаление торрентов на основе текущих критериев раздачи в Настройки->Индексаторы, только если включена опция «Удаление завершенных». После импорта он также установит {importedView} в качестве представления rTorrent, которое можно использовать в сценариях rTorrent для настройки поведения", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магниты в rTorrent в остановленном состоянии. Это может привести к поломке магнет файлов", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Клиент загрузки {downloadClientName} настроен на удаление завершенных загрузок. Это может привести к удалению загрузок из вашего клиента до того, как {appName} сможет их импортировать", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Использование функции «Проверка перед загрузкой» влияет на возможность приложения {appName} отслеживать новые загрузки. Также Sabnzbd рекомендует вместо этого «Отменять задания, которые невозможно завершить», поскольку это более эффективно", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Вы должны отключить сортировку по дате для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", + "DownloadClientSettingsCategoryHelpText": "Добавление категории, специфичной для {appName}, позволяет избежать конфликтов с несвязанными загрузками, не относящимися к {appName}. Использование категории не является обязательным, но настоятельно рекомендуется.", + "DownloadClientSettingsPostImportCategoryHelpText": "Категория для приложения {appName}, которую необходимо установить после импорта загрузки. {appName} не удалит торренты в этой категории, даже если раздача завершена. Оставьте пустым, чтобы сохранить ту же категорию.", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Приоритет при выборе эпизодов, вышедших в эфир за последние 14 дней", + "DayOfWeekAt": "{day} в {time}", + "DeleteDownloadClient": "Удалить программу для скачивания" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index dfea03c0c..0d07d0da6 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1372,7 +1372,6 @@ "LanguagesLoadError": "无法加载语言", "MonitorMissingEpisodes": "缺失剧集", "OneMinute": "1分钟", - "OnUpgrade": "升级中", "RemotePathMappings": "远程路径映射", "RemotePathMappingsLoadError": "无法加载远程路径映射", "RemoveQueueItemConfirmation": "您确定要从队列中移除“{sourceTitle}”吗?", From c6c37a408a4a673bc9c654d93979443c56b152eb Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 16 Jul 2024 04:13:53 +0000 Subject: [PATCH 372/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dream <seth.gecko.rr@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ru.json | 31 +++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index e5b563a45..70acc7e7b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -619,8 +619,8 @@ "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} предпочитает, чтобы каждая загрузка имела отдельную папку. Если к папке/пути добавлен *, Sabnzbd не будет создавать эти папки заданий. Отправляйтесь в Sabnzbd, чтобы исправить это.", "ConnectionLost": "Соединение прервано", "CleanLibraryLevel": "Очистить уровень библиотеки", - "ColonReplacementFormatHelpText": "Изменить как Sonarr обрабатывает замену двоеточий", - "ConnectionLostReconnect": "Sonarr попытается соединиться автоматически или нажмите кнопку внизу", + "ColonReplacementFormatHelpText": "Изменить как {appName} обрабатывает замену двоеточий", + "ConnectionLostReconnect": "{appName} попытается соединиться автоматически или нажмите кнопку внизу.", "ConnectSettingsSummary": "Уведомления, подключения к серверам/проигрывателям и настраиваемые скрипты", "ContinuingOnly": "Только в стадии показа", "Connections": "Соединения", @@ -694,8 +694,8 @@ "DefaultNameCopiedSpecification": "{name} - Копировать", "Delete": "Удалить", "DownloadClientFreeboxSettingsAppId": "Идентификатор приложения", - "AddNewSeriesRootFolderHelpText": "Подпапка \"{0}\" будет создана автоматически", - "AnalyseVideoFilesHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Это может привести к высокой активности диска и сети во время сканирования.", + "AddNewSeriesRootFolderHelpText": "Подпапка '{folder}' будет создана автоматически", + "AnalyseVideoFilesHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого потребуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска и сети во время сканирования.", "ApplyTagsHelpTextHowToApplyDownloadClients": "Как применить теги к выбранным клиентам загрузки", "ChangeCategoryHint": "Перенести загружаемое в «Категорию после импорта» из клиента загрузки", "ChownGroupHelpTextWarning": "Это работает только если пользователь {appName} является владельцем файла. Проверьте, что программа для скачивания использует туже самую группу, что и {appName}.", @@ -709,8 +709,8 @@ "DownloadClientFloodSettingsRemovalInfo": "{appName} будет автоматически удалять торренты на основе текущих критериев раздачи в Настройки -> Индексаторы", "DownloadClientValidationSslConnectFailureDetail": "{appName} не может подключиться к {clientName} с помощью SSL. Эта проблема может быть связана с компьютером. Попробуйте настроить {appName} и {clientName} так, чтобы они не использовали SSL.", "AnEpisodeIsDownloading": "Эпизод загружается", - "ConnectionLostToBackend": "Sonarr потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", - "CustomFormatHelpText": "Sonarr оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", + "ConnectionLostToBackend": "{appName} потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", + "CustomFormatHelpText": "{appName} оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите действительный метод аутентификации", "CustomFormatUnknownCondition": "Неизвестное условие пользовательского формата '{implementation}'", "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это позволяет избежать конфликтов с несвязанными загрузками", @@ -718,12 +718,12 @@ "DownloadPropersAndRepacksHelpTextWarning": "Используйте настраиваемое форматирование для автоматических обновлений до Проперов/Репаков", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматический поиск и попытка загрузки другого релиза, если неудачный релиз был получен из интерактивного поиска", "EnableHelpText": "Включить создание файла метаданных для этого типа метаданных", - "AutoTaggingNegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {0}.", + "AutoTaggingNegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {implementationName} .", "DownloadClientFreeboxUnableToReachFreeboxApi": "Невозможно получить доступ к API Freebox. Проверьте настройку «URL-адрес API» для базового URL-адреса и версии", "EnableInteractiveSearchHelpText": "Будет использовано при интерактивном поиске", "DelayProfileSeriesTagsHelpText": "Применимо к сериаламс хотя бы одним подходящим тегом", "EnableMediaInfoHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого требуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска или сети во время сканирования", - "AutomaticUpdatesDisabledDocker": "Автоматические обновления напрямую не поддерживаются при использовании механизма обновления Docker. Вам нужно будет обновить образ контейнера за пределами {AppName} или использовать скрипт", + "AutomaticUpdatesDisabledDocker": "Автоматические обновления напрямую не поддерживаются при использовании механизма обновления Docker. Вам нужно будет обновить образ контейнера за пределами {appName} или использовать скрипт", "DelayingDownloadUntil": "Приостановить скачивание до {date} в {time}", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Для параметра NzbGet KeepHistory установлено значение 0. Это не позволяет {appName} видеть завершенные загрузки.", "EnableRssHelpText": "Будет использоваться, когда {appName} будет периодически искать выпуски через RSS Sync", @@ -737,10 +737,10 @@ "BlackholeWatchFolderHelpText": "Папка, из которой {appName} должно импортировать завершенные загрузки", "DeleteReleaseProfileMessageText": "Вы действительно хотите удалить профиль релиза '{name}'?", "BlocklistAndSearchMultipleHint": "Начать поиск для замены после внесения в черный список", - "DeletedReasonEpisodeMissingFromDisk": "Sonarr не смог найти файл на диске, поэтому файл был откреплён от эпизода в базе данных", + "DeletedReasonEpisodeMissingFromDisk": "{appName} не смог найти файл на диске, поэтому файл был откреплён от эпизода в базе данных", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в qBittorrent. Обратите внимание, что принудительные торренты не подчиняются ограничениям на раздачу", "BlocklistRelease": "Релиз из черного списка", - "DeletedReasonManual": "Файл был удален с помощью Sonarr вручную или с помощью другого инструмента через API", + "DeletedReasonManual": "Файл был удален с помощью {appName} вручную или с помощью другого инструмента через API", "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Очередь торрентов не включена в настройках qBittorrent. Включите его в qBittorrent или выберите «Последний» в качестве приоритета", "DownloadClientDelugeValidationLabelPluginFailureDetail": "Пользователю {appName} не удалось добавить метку к клиенту {clientName}", @@ -754,5 +754,14 @@ "DownloadClientSettingsPostImportCategoryHelpText": "Категория для приложения {appName}, которую необходимо установить после импорта загрузки. {appName} не удалит торренты в этой категории, даже если раздача завершена. Оставьте пустым, чтобы сохранить ту же категорию.", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Приоритет при выборе эпизодов, вышедших в эфир за последние 14 дней", "DayOfWeekAt": "{day} в {time}", - "DeleteDownloadClient": "Удалить программу для скачивания" + "DeleteDownloadClient": "Удалить программу для скачивания", + "MetadataSourceSettings": "Настройки источника метаданных", + "MetadataSettingsSeriesMetadataUrl": "URL-адрес метаданных сериала", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Руководство по эпизодам метаданных сериала", + "MetadataSource": "Источник метаданных", + "MetadataSourceSettingsSeriesSummary": "Информация о том, откуда {appName} получает информацию о сериалах и эпизодах", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Включите теги миниатюр изображений в <имя файла>.nfo (требуются метаданные эпизода)", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Включить элемент руководства по эпизодам в формате JSON в tvshow.nfo (требуются «метаданные сериала»)", + "MetadataSettingsSeriesSummary": "Создавать файлы метаданных при импорте эпизодов или обновлении сериалов", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo с полными метаданными сериала" } From 1aaa9a14bc2d64cdc0d9eaac2d303b240fd2d6ea Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Jul 2024 21:34:37 -0700 Subject: [PATCH 373/762] Bump version to 4.0.8 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a1bf4d2d..480c12e0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.7 + VERSION: 4.0.8 jobs: backend: From dca5239420e21f91c1d67bc8bbb14cdb13c8d5d9 Mon Sep 17 00:00:00 2001 From: Marc Carbonell <32465384+eagnoor@users.noreply.github.com> Date: Wed, 17 Jul 2024 06:33:34 +0200 Subject: [PATCH 374/762] Remove extraneous indentation in RemoveFileExtension --- src/NzbDrone.Core/Parser/Parser.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index fabdd8498..d8a3efa5c 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -940,15 +940,15 @@ namespace NzbDrone.Core.Parser public static string RemoveFileExtension(string title) { title = FileExtensionRegex.Replace(title, m => + { + var extension = m.Value.ToLower(); + if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension)) { - var extension = m.Value.ToLower(); - if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension)) - { - return string.Empty; - } + return string.Empty; + } - return m.Value; - }); + return m.Value; + }); return title; } From 703dee9383adfd1b2dc0bccc0dc13cb9c1ebd288 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:21:28 +0300 Subject: [PATCH 375/762] New: Rating votes tooltip and series filter --- .../AddNewSeries/AddNewSeriesSearchResult.js | 1 + frontend/src/Components/HeartRating.js | 31 +++++++++++++------ frontend/src/Series/Details/SeriesDetails.js | 2 ++ .../src/Series/Index/Table/SeriesIndexRow.tsx | 2 +- frontend/src/Store/Actions/seriesActions.js | 15 ++++++++- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js index 815447ca8..2efb480bc 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js @@ -145,6 +145,7 @@ class AddNewSeriesSearchResult extends Component { <Label size={sizes.LARGE}> <HeartRating rating={ratings.value} + votes={ratings.votes} iconSize={13} /> </Label> diff --git a/frontend/src/Components/HeartRating.js b/frontend/src/Components/HeartRating.js index fe53a4e5f..01744b143 100644 --- a/frontend/src/Components/HeartRating.js +++ b/frontend/src/Components/HeartRating.js @@ -1,29 +1,40 @@ import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './HeartRating.css'; -function HeartRating({ rating, iconSize }) { +function HeartRating({ rating, votes, iconSize }) { return ( - <span className={styles.rating}> - <Icon - className={styles.heart} - name={icons.HEART} - size={iconSize} - /> + <Tooltip + anchor={ + <span className={styles.rating}> + <Icon + className={styles.heart} + name={icons.HEART} + size={iconSize} + /> - {rating * 10}% - </span> + {rating * 10}% + </span> + } + tooltip={translate('CountVotes', { votes })} + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> ); } HeartRating.propTypes = { rating: PropTypes.number.isRequired, + votes: PropTypes.number.isRequired, iconSize: PropTypes.number.isRequired }; HeartRating.defaultProps = { + votes: 0, iconSize: 14 }; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index c2fb80b8d..babc171f4 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -412,10 +412,12 @@ class SeriesDetails extends Component { ratings.value ? <HeartRating rating={ratings.value} + votes={ratings.votes} iconSize={20} /> : null } + <SeriesGenres genres={genres} /> <span> diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 92d273c37..887776427 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -401,7 +401,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { if (name === 'ratings') { return ( <VirtualTableRowCell key={name} className={styles[name]}> - <HeartRating rating={ratings.value} /> + <HeartRating rating={ratings.value} votes={ratings.votes} /> </VirtualTableRowCell> ); } diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index 54524ba38..b25a78220 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -128,8 +128,16 @@ export const filterPredicates = { ratings: function(item, filterValue, type) { const predicate = filterTypePredicates[type]; + const { value = 0 } = item.ratings; - return predicate(item.ratings.value * 10, filterValue); + return predicate(value * 10, filterValue); + }, + + ratingVotes: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + const { votes = 0 } = item.ratings; + + return predicate(votes, filterValue); }, originalLanguage: function(item, filterValue, type) { @@ -347,6 +355,11 @@ export const filterBuilderProps = [ label: () => translate('Rating'), type: filterBuilderTypes.NUMBER }, + { + name: 'ratingVotes', + label: () => translate('RatingVotes'), + type: filterBuilderTypes.NUMBER + }, { name: 'certification', label: () => translate('Certification'), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index aeb1d0a09..6c3f0edad 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -265,6 +265,7 @@ "CountSelectedFile": "{selectedCount} selected file", "CountSelectedFiles": "{selectedCount} selected files", "CountSeriesSelected": "{count} series selected", + "CountVotes": "{votes} votes", "CreateEmptySeriesFolders": "Create Empty Series Folders", "CreateEmptySeriesFoldersHelpText": "Create missing series folders during disk scan", "CreateGroup": "Create Group", @@ -1585,6 +1586,7 @@ "QuickSearch": "Quick Search", "Range": "Range", "Rating": "Rating", + "RatingVotes": "Rating Votes", "ReadTheWikiForMoreInformation": "Read the Wiki for more information", "Real": "Real", "Reason": "Reason", From 0a28ff84e8efd08f5799e1b0e19cedcf4e8e9736 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 13 Jul 2024 19:21:25 -0700 Subject: [PATCH 376/762] Fixed: Parsing of anime with 3 digit number in middle of title --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 30c2907f2..411ad213e 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -135,6 +135,8 @@ namespace NzbDrone.Core.Test.ParserTests [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("[SubsPlease] Series Title - 100 Years Quest - 01 (1080p) [1107F3A9].mkv", "Series Title - 100 Years Quest", 1, 0, 0)] + [TestCase("[SubsPlease] Series Title 100 Years Quest - 01 (1080p) [1107F3A9].mkv", "Series Title 100 Years Quest", 1, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index d8a3efa5c..ef51c6290 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -106,14 +106,14 @@ namespace NzbDrone.Core.Parser 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)", - 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)", 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)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - [SubGroup] Title with trailing number S## (Full season) new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?![ex]?\d+))).+?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 06936c4f226a69ef78b5d1298355761dfb8581b8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 13 Jul 2024 20:10:06 -0700 Subject: [PATCH 377/762] Fixed: Parsing of Chinese anime with ordinal number in English title --- .../ParserTests/UnicodeReleaseParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs index e236c1456..f46f97327 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs @@ -51,6 +51,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Skymoon-Raws] Anime-Series Title S02 - 01 [ViuTV][CHT][WEB-DL][1080p][AVC AAC][MP4+ASS]", "Anime-Series Title S2", "Skymoon-Raws", 1)] [TestCase("[orion origin] Anime-Series Title S02[07][1080p][H264 AAC][CHS][ENG&JPN stidio]", "Anime-Series Title S2", "orion origin", 7)] [TestCase("[UHA-WINGS][Anime-Series Title S02][01][x264 1080p][CHT].mp4", "Anime-Series Title S2", "UHA-WINGS", 1)] + [TestCase("[Suzuya Raws] 腼腆英雄 东京夺还篇 / Series 2nd Season - 01 [CR WebRip 1080p HEVC-10bit AAC][Multi-Subs]", "Series 2nd Season", "Suzuya Raws", 1)] public void should_parse_chinese_anime_season_episode_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber) { postTitle = XmlCleaner.ReplaceUnicode(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index ef51c6290..78caeb21e 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), // GM-Team releases with lots of square brackets new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled) From d3f14d5f5ea182e6616538014197cb058f41517e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 13 Jul 2024 20:10:25 -0700 Subject: [PATCH 378/762] Fixed: Parse Chinese anime formats with reverse title order --- .../ParserTests/UnicodeReleaseParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs index f46f97327..24844a9a9 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs @@ -52,6 +52,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[orion origin] Anime-Series Title S02[07][1080p][H264 AAC][CHS][ENG&JPN stidio]", "Anime-Series Title S2", "orion origin", 7)] [TestCase("[UHA-WINGS][Anime-Series Title S02][01][x264 1080p][CHT].mp4", "Anime-Series Title S2", "UHA-WINGS", 1)] [TestCase("[Suzuya Raws] 腼腆英雄 东京夺还篇 / Series 2nd Season - 01 [CR WebRip 1080p HEVC-10bit AAC][Multi-Subs]", "Series 2nd Season", "Suzuya Raws", 1)] + [TestCase("[ANi] SERIES / SERIES 靦腆英雄 - 11 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]", "SERIES", "ANi", 11)] public void should_parse_chinese_anime_season_episode_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber) { postTitle = XmlCleaner.ReplaceUnicode(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 78caeb21e..acdcf849c 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -40,6 +40,9 @@ namespace NzbDrone.Core.Parser // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), + // Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<title>[^\]]+?)(?:\s/\s))(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), From e35b39b4b1c88e13f9b1515c68b4d0942f84fa6d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 11:56:25 -0700 Subject: [PATCH 379/762] New: Add option to show tags on series Poster and Overview Closes #6946 --- frontend/src/App/State/SeriesAppState.ts | 2 ++ ...SeriesIndexOverviewOptionsModalContent.tsx | 12 +++++++++ .../Index/Overview/SeriesIndexOverview.css | 14 ++++++++++- .../Overview/SeriesIndexOverview.css.d.ts | 2 ++ .../Index/Overview/SeriesIndexOverview.tsx | 25 +++++++++++++------ .../SeriesIndexPosterOptionsModalContent.tsx | 13 ++++++++++ .../Index/Posters/SeriesIndexPoster.css | 14 +++++++++++ .../Index/Posters/SeriesIndexPoster.css.d.ts | 2 ++ .../Index/Posters/SeriesIndexPoster.tsx | 11 ++++++++ .../Index/Posters/SeriesIndexPosters.tsx | 5 ++++ .../src/Store/Actions/seriesIndexActions.js | 2 ++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 12 files changed, 95 insertions(+), 9 deletions(-) diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts index f9c216bdc..8d13f0c0b 100644 --- a/frontend/src/App/State/SeriesAppState.ts +++ b/frontend/src/App/State/SeriesAppState.ts @@ -20,6 +20,7 @@ export interface SeriesIndexAppState { showTitle: boolean; showMonitored: boolean; showQualityProfile: boolean; + showTags: boolean; showSearchAction: boolean; }; @@ -34,6 +35,7 @@ export interface SeriesIndexAppState { showSeasonCount: boolean; showPath: boolean; showSizeOnDisk: boolean; + showTags: boolean; showSearchAction: boolean; }; diff --git a/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx b/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx index f9d17d222..731403829 100644 --- a/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx +++ b/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx @@ -55,6 +55,7 @@ function SeriesIndexOverviewOptionsModalContent( showSeasonCount, showPath, showSizeOnDisk, + showTags, showSearchAction, } = useSelector(selectOverviewOptions); @@ -185,6 +186,17 @@ function SeriesIndexOverviewOptionsModalContent( /> </FormGroup> + <FormGroup> + <FormLabel>{translate('ShowTags')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showTags" + value={showTags} + onChange={onOverviewOptionChange} + /> + </FormGroup> + <FormGroup> <FormLabel>{translate('ShowSearch')}</FormLabel> diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css index 1f482a2d6..999f15a41 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css @@ -73,14 +73,26 @@ $hoverScale: 1.05; flex: 1 0 auto; } +.overviewContainer { + display: flex; + justify-content: space-between; + flex: 0 1 1000px; + flex-direction: column; +} + .overview { composes: link; - flex: 0 1 1000px; overflow: hidden; min-height: 0; } +.tags { + display: flex; + justify-content: space-around; + overflow: hidden; +} + @media only screen and (max-width: $breakpointSmall) { .overview { display: none; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts index de94277cc..5dfbab8ee 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts @@ -8,8 +8,10 @@ interface CssExports { 'info': string; 'link': string; 'overview': string; + 'overviewContainer': string; 'poster': string; 'posterContainer': string; + 'tags': string; 'title': string; 'titleRow': string; } diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx index d38c787d3..f7d7c3b50 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx @@ -5,6 +5,7 @@ import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; @@ -70,6 +71,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) { overview, statistics = {} as Statistics, images, + tags, network, } = series; @@ -205,15 +207,22 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) { </div> <div className={styles.details}> - <Link className={styles.overview} to={link}> - <TextTruncate - line={Math.floor( - overviewHeight / (defaultFontSize * lineHeight) - )} - text={overview} - /> - </Link> + <div className={styles.overviewContainer}> + <Link className={styles.overview} to={link}> + <TextTruncate + line={Math.floor( + overviewHeight / (defaultFontSize * lineHeight) + )} + text={overview} + /> + </Link> + {overviewOptions.showTags ? ( + <div className={styles.tags}> + <TagListConnector tags={tags} /> + </div> + ) : null} + </div> <SeriesIndexOverviewInfo height={overviewHeight} monitored={monitored} diff --git a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx index 81e0f942d..4b292eee4 100644 --- a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx +++ b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx @@ -52,6 +52,7 @@ function SeriesIndexPosterOptionsModalContent( showTitle, showMonitored, showQualityProfile, + showTags, showSearchAction, } = posterOptions; @@ -130,6 +131,18 @@ function SeriesIndexPosterOptionsModalContent( /> </FormGroup> + <FormGroup> + <FormLabel>{translate('ShowTags')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showTags" + value={showTags} + helpText={translate('ShowTagsHelpText')} + onChange={onPosterOptionChange} + /> + </FormGroup> + <FormGroup> <FormLabel>{translate('ShowSearch')}</FormLabel> diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css index 83dda335a..bc708f6cd 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css @@ -57,6 +57,20 @@ $hoverScale: 1.05; font-size: $smallFontSize; } +.tags { + display: flex; + align-items: center; + justify-content: space-around; + padding: 0 3px; + height: 21px; + background-color: var(--seriesBackgroundColor); +} + +.tagsList { + display: flex; + overflow: hidden; +} + .ended { position: absolute; top: 0; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts index a40dbcefd..ad1ccb597 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts @@ -10,6 +10,8 @@ interface CssExports { 'nextAiring': string; 'overlayTitle': string; 'posterContainer': string; + 'tags': string; + 'tagsList': string; 'title': string; } export const cssExports: CssExports; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index b2015eaf5..474a226d9 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -5,6 +5,7 @@ import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; @@ -41,6 +42,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { showTitle, showMonitored, showQualityProfile, + showTags, showSearchAction, } = useSelector(selectPosterOptions); @@ -60,6 +62,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { added, statistics = {} as Statistics, images, + tags, } = series; const { @@ -208,6 +211,14 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { </div> ) : null} + {showTags && tags.length ? ( + <div className={styles.tags}> + <div className={styles.tagsList}> + <TagListConnector tags={tags} /> + </div> + </div> + ) : null} + {nextAiring ? ( <div className={styles.nextAiring} diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index bf1915761..32b238a6c 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -141,6 +141,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { showTitle, showMonitored, showQualityProfile, + showTags, } = posterOptions; const nextAiringHeight = 19; @@ -164,6 +165,10 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { heights.push(19); } + if (showTags) { + heights.push(21); + } + switch (sortKey) { case 'network': case 'seasons': diff --git a/frontend/src/Store/Actions/seriesIndexActions.js b/frontend/src/Store/Actions/seriesIndexActions.js index 65c20870f..cc1d7c574 100644 --- a/frontend/src/Store/Actions/seriesIndexActions.js +++ b/frontend/src/Store/Actions/seriesIndexActions.js @@ -29,6 +29,7 @@ export const defaultState = { showTitle: false, showMonitored: true, showQualityProfile: true, + showTags: false, showSearchAction: false }, @@ -43,6 +44,7 @@ export const defaultState = { showSeasonCount: true, showPath: false, showSizeOnDisk: false, + showTags: false, showSearchAction: false }, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 6c3f0edad..751800eaf 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1864,6 +1864,8 @@ "ShowSeasonCount": "Show Season Count", "ShowSeriesTitleHelpText": "Show series title under poster", "ShowSizeOnDisk": "Show Size on Disk", + "ShowTags": "Show Tags", + "ShowTagsHelpText": "Show tags under poster", "ShowTitle": "Show Title", "ShowUnknownSeriesItems": "Show Unknown Series Items", "ShowUnknownSeriesItemsHelpText": "Show items without a series in the queue, this could include removed series, movies or anything else in {appName}'s category", From 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 16 Jul 2024 21:34:43 -0700 Subject: [PATCH 380/762] New: Use natural sorting for lists of items in the UI Closes #6955 --- .../Components/Filter/Builder/FilterBuilderRow.js | 3 ++- .../Builder/FilterBuilderRowValueConnector.js | 4 ++-- .../Filter/Builder/SeriesFilterBuilderRowValue.tsx | 4 ++-- .../CustomFilters/CustomFiltersModalContent.js | 3 ++- .../Form/DownloadClientSelectInputConnector.js | 4 ++-- .../Components/Form/IndexerSelectInputConnector.js | 4 ++-- .../Form/QualityProfileSelectInputConnector.js | 4 ++-- frontend/src/Components/Menu/FilterMenuContent.js | 3 ++- frontend/src/Components/TagList.js | 3 ++- .../Series/SelectSeriesModalContent.tsx | 5 ++--- frontend/src/Series/Details/SeriesTagsConnector.js | 5 +++-- .../CustomFormats/CustomFormatsConnector.js | 4 ++-- .../DownloadClients/DownloadClientsConnector.js | 4 ++-- .../ImportLists/ImportLists/ImportListsConnector.js | 4 ++-- .../Settings/Indexers/Indexers/IndexersConnector.js | 4 ++-- .../Metadata/Metadata/MetadatasConnector.js | 4 ++-- .../Notifications/NotificationsConnector.js | 4 ++-- .../Profiles/Quality/QualityProfileFormatItems.js | 3 ++- .../Profiles/Quality/QualityProfilesConnector.js | 4 ++-- .../src/Settings/Tags/AutoTagging/AutoTaggings.js | 4 ++-- frontend/src/Store/Actions/releaseActions.js | 4 ++-- frontend/src/Store/Actions/seriesActions.js | 8 ++++---- .../createEnabledDownloadClientsSelector.ts | 8 ++++++-- .../Store/Selectors/createRootFoldersSelector.ts | 5 ++--- ...onSelector.js => createSortedSectionSelector.ts} | 8 ++++++-- .../System/Tasks/Queued/QueuedTaskRowNameCell.tsx | 5 ++--- frontend/src/Utilities/Array/sortByName.js | 5 ----- frontend/src/Utilities/Array/sortByProp.ts | 13 +++++++++++++ frontend/src/typings/Helpers/KeysMatching.ts | 7 +++++++ 29 files changed, 83 insertions(+), 57 deletions(-) rename frontend/src/Store/Selectors/{createSortedSectionSelector.js => createSortedSectionSelector.ts} (68%) delete mode 100644 frontend/src/Utilities/Array/sortByName.js create mode 100644 frontend/src/Utilities/Array/sortByProp.ts create mode 100644 frontend/src/typings/Helpers/KeysMatching.ts diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 01c24b460..46a38a258 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; +import sortByProp from 'Utilities/Array/sortByProp'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; @@ -224,7 +225,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort((a, b) => a.value.localeCompare(b.value)); + }).sort(sortByProp('value')); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index a7aed80b6..d1419327a 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByName); + }, []).sort(sortByProp('name')); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx index 2eae79c80..88b34509a 100644 --- a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; @@ -11,7 +11,7 @@ function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) { const tagList = allSeries .map((series) => ({ id: series.id, name: series.title })) - .sort(sortByName); + .sort(sortByProp('name')); return <FilterBuilderRowValue {...props} tagList={tagList} />; } diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 28eb91599..99cb6ec5c 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,6 +5,7 @@ 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 sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) { <ModalBody> { customFilters - .sort((a, b) => a.label.localeCompare(b.label)) + .sort((a, b) => sortByProp(a, b, 'label')) .map((customFilter) => { return ( <CustomFilter diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index fb0430f19..c21f0ded6 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; @@ -23,7 +23,7 @@ function createMapStateToProps() { const filteredItems = items.filter((item) => item.protocol === protocolFilter); - const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { + const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => { return { key: downloadClient.id, value: downloadClient.name, diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js index 91c31198f..5f62becbb 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchIndexers } from 'Store/Actions/settingsActions'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; @@ -20,7 +20,7 @@ function createMapStateToProps() { items } = indexers; - const values = _.map(items.sort(sortByName), (indexer) => { + const values = _.map(items.sort(sortByProp('name')), (indexer) => { return { key: indexer.id, value: indexer.name diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 48fc6bc35..055180f12 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByName), + createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js index 4ee406224..7bc23c066 100644 --- a/frontend/src/Components/Menu/FilterMenuContent.js +++ b/frontend/src/Components/Menu/FilterMenuContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import FilterMenuItem from './FilterMenuItem'; import MenuContent from './MenuContent'; @@ -47,7 +48,7 @@ class FilterMenuContent extends Component { { customFilters - .sort((a, b) => a.label.localeCompare(b.label)) + .sort(sortByProp('label')) .map((filter) => { return ( <FilterMenuItem diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index 6da96849c..fe700b8fe 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; +import sortByProp from 'Utilities/Array/sortByProp'; import Label from './Label'; import styles from './TagList.css'; @@ -8,7 +9,7 @@ function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .sort((a, b) => a.label.localeCompare(b.label)); + .sort(sortByProp('label')); return ( <div className={styles.tags}> diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx index 15e377209..ad5aee15e 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx @@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props'; import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import dimensions from 'Styles/Variables/dimensions'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import SelectSeriesModalTableHeader from './SelectSeriesModalTableHeader'; import SelectSeriesRow from './SelectSeriesRow'; @@ -163,9 +164,7 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { ); const items = useMemo(() => { - const sorted = [...allSeries].sort((a, b) => - a.sortTitle.localeCompare(b.sortTitle) - ); + const sorted = [...allSeries].sort(sortByProp('sortTitle')); return sorted.filter( (item) => diff --git a/frontend/src/Series/Details/SeriesTagsConnector.js b/frontend/src/Series/Details/SeriesTagsConnector.js index 0f04bf1ca..07d1ce667 100644 --- a/frontend/src/Series/Details/SeriesTagsConnector.js +++ b/frontend/src/Series/Details/SeriesTagsConnector.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import SeriesTags from './SeriesTags'; function createMapStateToProps() { @@ -12,8 +13,8 @@ function createMapStateToProps() { const tags = series.tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .map((tag) => tag.label) - .sort((a, b) => a.localeCompare(b)); + .sort(sortByProp('label')) + .map((tag) => tag.label); return { tags diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js index 8e828620b..0417d9b21 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import CustomFormats from './CustomFormats'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.customFormats', sortByName), + createSortedSectionSelector('settings.customFormats', sortByProp('name')), (customFormats) => customFormats ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index d9e543469..0dc410fcb 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByName), + createSortedSectionSelector('settings.downloadClients', sortByProp('name')), createTagsSelector(), (downloadClients, tagList) => { return { diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 2b29f6eb1..f3094d6c6 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.importLists', sortByName), + createSortedSectionSelector('settings.importLists', sortByProp('name')), (importLists) => importLists ); } diff --git a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js index cb6e830fd..88c571a60 100644 --- a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js +++ b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Indexers from './Indexers'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexers', sortByName), + createSortedSectionSelector('settings.indexers', sortByProp('name')), createTagsSelector(), (indexers, tagList) => { return { diff --git a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js index fb52ac33b..8675f4742 100644 --- a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js +++ b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchMetadata } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Metadatas from './Metadatas'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.metadata', sortByName), + createSortedSectionSelector('settings.metadata', sortByProp('name')), (metadata) => metadata ); } diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index b306f742a..6351c6f8a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByName), + createSortedSectionSelector('settings.notifications', sortByProp('name')), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js index 7b90dec6c..61cbefba1 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js @@ -20,7 +20,8 @@ function calcOrder(profileFormatItems) { if (b.score !== a.score) { return b.score - a.score; } - return a.name > b.name ? 1 : -1; + + return a.localeCompare(b.name, undefined, { numeric: true }); }).map((x) => items[x.format]); } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js index 581882ffd..4cb318463 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import QualityProfiles from './QualityProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByName), + createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), (qualityProfiles) => qualityProfiles ); } diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js index 45c8e4b85..f27dc3b5a 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js @@ -9,7 +9,7 @@ import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { cloneAutoTagging, deleteAutoTagging, fetchAutoTaggings } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import AutoTagging from './AutoTagging'; import EditAutoTaggingModal from './EditAutoTaggingModal'; @@ -23,7 +23,7 @@ export default function AutoTaggings() { isFetching, isPopulated } = useSelector( - createSortedSectionSelector('settings.autoTaggings', sortByName) + createSortedSectionSelector('settings.autoTaggings', sortByProp('name')) ); const tagList = useSelector(createTagsSelector()); diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 6d7495321..c7c8ce0e4 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -1,7 +1,7 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import translate from 'Utilities/String/translate'; import createFetchHandler from './Creators/createFetchHandler'; @@ -232,7 +232,7 @@ export const defaultState = { return acc; }, []); - return genreList.sort(sortByName); + return genreList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index b25a78220..3aa9b7237 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -3,7 +3,7 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import translate from 'Utilities/String/translate'; @@ -254,7 +254,7 @@ export const filterBuilderProps = [ return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { @@ -323,7 +323,7 @@ export const filterBuilderProps = [ return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { @@ -342,7 +342,7 @@ export const filterBuilderProps = [ return acc; }, []); - return languageList.sort(sortByName); + return languageList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts index ac31e5210..3a581587b 100644 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts @@ -2,13 +2,17 @@ import { createSelector } from 'reselect'; import { DownloadClientAppState } from 'App/State/SettingsAppState'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import DownloadClient from 'typings/DownloadClient'; +import sortByProp from 'Utilities/Array/sortByProp'; export default function createEnabledDownloadClientsSelector( protocol: DownloadProtocol ) { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByName), + createSortedSectionSelector<DownloadClient>( + 'settings.downloadClients', + sortByProp('name') + ), (downloadClients: DownloadClientAppState) => { const { isFetching, isPopulated, error, items } = downloadClients; diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts index 7e01b57ec..3eb486191 100644 --- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts +++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts @@ -2,12 +2,11 @@ import { createSelector } from 'reselect'; import RootFolderAppState from 'App/State/RootFolderAppState'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import RootFolder from 'typings/RootFolder'; +import sortByProp from 'Utilities/Array/sortByProp'; export default function createRootFoldersSelector() { return createSelector( - createSortedSectionSelector('rootFolders', (a: RootFolder, b: RootFolder) => - a.path.localeCompare(b.path) - ), + createSortedSectionSelector<RootFolder>('rootFolders', sortByProp('path')), (rootFolders: RootFolderAppState) => rootFolders ); } diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.js b/frontend/src/Store/Selectors/createSortedSectionSelector.ts similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.js rename to frontend/src/Store/Selectors/createSortedSectionSelector.ts index 331d890c9..abee01f75 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.js +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.ts @@ -1,14 +1,18 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector(section, comparer) { +function createSortedSectionSelector<T>( + section: string, + comparer: (a: T, b: T) => number +) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); + return { ...sectionState, - items: [...sectionState.items].sort(comparer) + items: [...sectionState.items].sort(comparer), }; } ); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx index 70058af02..452895893 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { CommandBody } from 'Commands/Command'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import styles from './QueuedTaskRowNameCell.css'; @@ -39,9 +40,7 @@ export default function QueuedTaskRowNameCell( } const series = useSelector(createMultiSeriesSelector(seriesIds)); - const sortedSeries = series.sort((a, b) => - a.sortTitle.localeCompare(b.sortTitle) - ); + const sortedSeries = series.sort(sortByProp('sortTitle')); return ( <TableRowCell> diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js deleted file mode 100644 index 1956d3bac..000000000 --- a/frontend/src/Utilities/Array/sortByName.js +++ /dev/null @@ -1,5 +0,0 @@ -function sortByName(a, b) { - return a.name.localeCompare(b.name); -} - -export default sortByName; diff --git a/frontend/src/Utilities/Array/sortByProp.ts b/frontend/src/Utilities/Array/sortByProp.ts new file mode 100644 index 000000000..8fbde08c9 --- /dev/null +++ b/frontend/src/Utilities/Array/sortByProp.ts @@ -0,0 +1,13 @@ +import { StringKey } from 'typings/Helpers/KeysMatching'; + +export function sortByProp< + // eslint-disable-next-line no-use-before-define + T extends Record<K, string>, + K extends StringKey<T> +>(sortKey: K) { + return (a: T, b: T) => { + return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true }); + }; +} + +export default sortByProp; diff --git a/frontend/src/typings/Helpers/KeysMatching.ts b/frontend/src/typings/Helpers/KeysMatching.ts new file mode 100644 index 000000000..0e20206ef --- /dev/null +++ b/frontend/src/typings/Helpers/KeysMatching.ts @@ -0,0 +1,7 @@ +type KeysMatching<T, V> = { + [K in keyof T]-?: T[K] extends V ? K : never; +}[keyof T]; + +export type StringKey<T> = KeysMatching<T, string>; + +export default KeysMatching; From 6a4824c02932ee1bd57c1f4f0644f8bc693f6006 Mon Sep 17 00:00:00 2001 From: diamondpete <87245367+diamondpete@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:36:29 -0400 Subject: [PATCH 381/762] Fixed: Remove apostrophe, backtick in contractions --- .../FileNameBuilderTests/CleanTitleFixture.cs | 8 ++++++++ src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs index 674522748..c4b1de610 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs @@ -72,6 +72,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestCase("I'm the Boss", "Im the Boss")] [TestCase("The Title's", "The Title's")] [TestCase("I'm after I'm", "Im after I'm")] + [TestCase("I've Been Caught", "Ive Been Caught")] + [TestCase("I'm Lost", "Im Lost")] + [TestCase("That'll Be The Day", "Thatll Be The Day")] + [TestCase("I'd Rather Be Alone", "Id Rather Be Alone")] + [TestCase("I Can't Die", "I Cant Die")] + [TestCase("Won`t Get Fooled Again", "Wont Get Fooled Again")] + [TestCase("Don’t Blink", "Dont Blink")] + [TestCase("The ` Legend of Kings", "The Legend of Kings")] // [TestCase("", "")] public void should_get_expected_title_back(string title, string expected) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 3755f17f7..952b8f593 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Organizer private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]+$", RegexOptions.Compiled); - private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|’|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|`|’|:|\?|,)(?=(?:(?:s|m|t|ve|ll|d|re)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); // TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) From 7b8d606a1bed6257d7942de47576c1505fd9cb57 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Wed, 17 Jul 2024 06:38:15 +0200 Subject: [PATCH 382/762] New: Wrap specifications in Custom Format and Auto Tagging modals --- .../CustomFormats/EditCustomFormatModalContent.css | 5 +++++ .../CustomFormats/EditCustomFormatModalContent.css.d.ts | 1 + .../Tags/AutoTagging/EditAutoTaggingModalContent.css | 5 +++++ .../Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts | 1 + 4 files changed, 12 insertions(+) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css index b7d3da255..24830ef42 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css @@ -25,3 +25,8 @@ border-radius: 4px; background-color: var(--cardCenterBackgroundColor); } + +.customFormats { + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts index 1339caf02..1aab6062e 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'addSpecification': string; 'center': string; + 'customFormats': string; 'deleteButton': string; 'rightButtons': string; } diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css index a197dbcd4..d503b0af3 100644 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css @@ -25,3 +25,8 @@ border-radius: 4px; background-color: var(--cardCenterBackgroundColor); } + +.autoTaggings { + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts index 1339caf02..2a7f6b41e 100644 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'addSpecification': string; + 'autoTaggings': string; 'center': string; 'deleteButton': string; 'rightButtons': string; From 4b5ef4907bcbfd174144a32bd9b47d8eb2ee1a87 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 16 Jul 2024 17:28:47 -0700 Subject: [PATCH 383/762] Set default value for CustomColonReplacementFormat if not provided --- src/Sonarr.Api.V3/Config/NamingExampleResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs index ed1bfbd92..d631fc00f 100644 --- a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs +++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs @@ -46,7 +46,7 @@ namespace Sonarr.Api.V3.Config ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle, ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat, - CustomColonReplacementFormat = resource.CustomColonReplacementFormat, + CustomColonReplacementFormat = resource.CustomColonReplacementFormat ?? "", StandardEpisodeFormat = resource.StandardEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat, From 19466aa29050e1b13b1db8cc61662b10d76a82e4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 16 Jul 2024 21:39:40 -0700 Subject: [PATCH 384/762] Fixed: Assume category path from qBittorent starting with '//' is a Windows UNC path Radarr/Radarr#10162 --- .../QBittorrentTests/QBittorrentFixture.cs | 28 +++++++++++++++++++ .../Clients/QBittorrent/QBittorrent.cs | 10 ++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 1d04855b5..4b126835b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -560,6 +560,34 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\QBittorrent".AsOsAgnostic()); } + [Test] + public void should_correct_category_output_path() + { + var config = new QBittorrentPreferences + { + SavePath = @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic() + }; + + Mocker.GetMock<IQBittorrentProxy>() + .Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>())) + .Returns(config); + + Mocker.GetMock<IQBittorrentProxy>() + .Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>())) + .Returns(new Version(2, 0)); + + Mocker.GetMock<IQBittorrentProxy>() + .Setup(s => s.GetLabels(It.IsAny<QBittorrentSettings>())) + .Returns(new Dictionary<string, QBittorrentLabel> + { { "tv", new QBittorrentLabel { Name = "tv", SavePath = "//server/store/downloads" } } }); + + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"\\server\store\downloads"); + } + [Test] public async Task Download_should_handle_http_redirect_to_magnet() { diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index cea3a8c8b..bf1d1a14f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -377,7 +377,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { if (Proxy.GetLabels(Settings).TryGetValue(Settings.TvCategory, out var label) && label.SavePath.IsNotNullOrWhiteSpace()) { - var labelDir = new OsPath(label.SavePath); + var savePath = label.SavePath; + + if (savePath.StartsWith("//")) + { + _logger.Trace("Replacing double forward slashes in path '{0}'. If this is not meant to be a Windows UNC path fix the 'Save Path' in qBittorrent's {1} category", savePath, Settings.TvCategory); + savePath = savePath.Replace('/', '\\'); + } + + var labelDir = new OsPath(savePath); if (labelDir.IsRooted) { From c023fc700896c7f0751c4ac63c4e1a89d6e1a9bb Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 14:46:59 -0700 Subject: [PATCH 385/762] New: Show update settings on all platforms --- .../src/Settings/General/UpdateSettings.js | 89 +++++++++---------- .../HealthCheck/Checks/UpdateCheckFixture.cs | 31 ++----- .../Configuration/ConfigFileProvider.cs | 2 +- .../HealthCheck/Checks/UpdateCheck.cs | 2 +- .../Update/InstallUpdateService.cs | 4 +- 5 files changed, 52 insertions(+), 76 deletions(-) diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 4558650c0..a954b59d9 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -17,7 +17,6 @@ function UpdateSettings(props) { const { advancedSettings, settings, - isWindows, packageUpdateMechanism, onInputChange } = props; @@ -68,63 +67,59 @@ function UpdateSettings(props) { /> </FormGroup> - { - isWindows ? - null : - <div> - <FormGroup - advancedSettings={advancedSettings} - isAdvanced={true} - size={sizes.MEDIUM} - > - <FormLabel>{translate('Automatic')}</FormLabel> + <div> + <FormGroup + advancedSettings={advancedSettings} + isAdvanced={true} + size={sizes.MEDIUM} + > + <FormLabel>{translate('Automatic')}</FormLabel> - <FormInputGroup - type={inputTypes.CHECK} - name="updateAutomatically" - helpText={translate('UpdateAutomaticallyHelpText')} - helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined} - onChange={onInputChange} - {...updateAutomatically} - /> - </FormGroup> + <FormInputGroup + type={inputTypes.CHECK} + name="updateAutomatically" + helpText={translate('UpdateAutomaticallyHelpText')} + helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined} + onChange={onInputChange} + {...updateAutomatically} + /> + </FormGroup> + <FormGroup + advancedSettings={advancedSettings} + isAdvanced={true} + > + <FormLabel>{translate('Mechanism')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="updateMechanism" + values={updateOptions} + helpText={translate('UpdateMechanismHelpText')} + helpLink="https://wiki.servarr.com/sonarr/settings#updates" + onChange={onInputChange} + {...updateMechanism} + /> + </FormGroup> + + { + updateMechanism.value === 'script' && <FormGroup advancedSettings={advancedSettings} isAdvanced={true} > - <FormLabel>{translate('Mechanism')}</FormLabel> + <FormLabel>{translate('ScriptPath')}</FormLabel> <FormInputGroup - type={inputTypes.SELECT} - name="updateMechanism" - values={updateOptions} - helpText={translate('UpdateMechanismHelpText')} - helpLink="https://wiki.servarr.com/sonarr/settings#updates" + type={inputTypes.TEXT} + name="updateScriptPath" + helpText={translate('UpdateScriptPathHelpText')} onChange={onInputChange} - {...updateMechanism} + {...updateScriptPath} /> </FormGroup> - - { - updateMechanism.value === 'script' && - <FormGroup - advancedSettings={advancedSettings} - isAdvanced={true} - > - <FormLabel>{translate('ScriptPath')}</FormLabel> - - <FormInputGroup - type={inputTypes.TEXT} - name="updateScriptPath" - helpText={translate('UpdateScriptPathHelpText')} - onChange={onInputChange} - {...updateScriptPath} - /> - </FormGroup> - } - </div> - } + } + </div> </FieldSet> ); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs index 64eeb9169..7d859eb9d 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Localization; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Update; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.HealthCheck.Checks { @@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Returns("Some Warning Message"); } - [Test] - public void should_return_error_when_app_folder_is_write_protected() - { - WindowsOnly(); - - Mocker.GetMock<IAppFolderInfo>() - .Setup(s => s.StartUpFolder) - .Returns(@"C:\NzbDrone"); - - Mocker.GetMock<IDiskProvider>() - .Setup(c => c.FolderWritable(It.IsAny<string>())) - .Returns(false); - - Subject.Check().ShouldBeError(); - } - [Test] public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled() { - PosixOnly(); - - const string startupFolder = @"/opt/nzbdrone"; + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); Mocker.GetMock<IConfigFileProvider>() .Setup(s => s.UpdateAutomatically) @@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled() { - PosixOnly(); - - const string startupFolder = @"/opt/nzbdrone"; - const string uiFolder = @"/opt/nzbdrone/UI"; + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); + var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic(); Mocker.GetMock<IConfigFileProvider>() .Setup(s => s.UpdateAutomatically) @@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled() { - PosixOnly(); + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); Mocker.GetMock<IConfigFileProvider>() .Setup(s => s.UpdateAutomatically) @@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock<IAppFolderInfo>() .Setup(s => s.StartUpFolder) - .Returns(@"/opt/nzbdrone"); + .Returns(startupFolder); Mocker.GetMock<IDiskProvider>() .Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never()); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 3329dee2c..be132cc6c 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -270,7 +270,7 @@ namespace NzbDrone.Core.Configuration } } - public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false); + public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false); public UpdateMechanism UpdateMechanism => Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 09b3eea1f..c723e1c4e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.HealthCheck.Checks var startupFolder = _appFolderInfo.StartUpFolder; var uiFolder = Path.Combine(startupFolder, "UI"); - if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) && + if (_configFileProvider.UpdateAutomatically && _configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && !_osInfo.IsDocker) { diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index eea51684a..b8f827972 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Update { EnsureAppDataSafety(); - if (OsInfo.IsWindows || _configFileProvider.UpdateMechanism != UpdateMechanism.Script) + if (_configFileProvider.UpdateMechanism != UpdateMechanism.Script) { var startupFolder = _appFolderInfo.StartUpFolder; var uiFolder = Path.Combine(startupFolder, "UI"); @@ -143,7 +143,7 @@ namespace NzbDrone.Core.Update _backupService.Backup(BackupType.Update); - if (OsInfo.IsNotWindows && _configFileProvider.UpdateMechanism == UpdateMechanism.Script) + if (_configFileProvider.UpdateMechanism == UpdateMechanism.Script) { InstallUpdateWithScript(updateSandboxFolder); return true; From 0e95ba2021b23cc65bce0a0620dd48e355250dab Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 14 Jul 2024 16:42:35 -0700 Subject: [PATCH 386/762] New: Allow major version updates to be installed --- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppState.ts | 1 + frontend/src/App/State/SettingsAppState.ts | 6 +- frontend/src/App/State/SystemAppState.ts | 5 +- .../Overview/SeriesIndexOverviewInfo.tsx | 2 +- frontend/src/System/Updates/UpdateChanges.js | 46 --- frontend/src/System/Updates/UpdateChanges.tsx | 33 ++ frontend/src/System/Updates/Updates.js | 249 -------------- frontend/src/System/Updates/Updates.tsx | 305 ++++++++++++++++++ .../src/System/Updates/UpdatesConnector.js | 98 ------ frontend/src/typings/Settings/General.ts | 45 +++ .../src/typings/{ => Settings}/UiSettings.ts | 2 +- frontend/src/typings/SystemStatus.ts | 1 + frontend/src/typings/Update.ts | 20 ++ src/NzbDrone.Core/Localization/Core/en.json | 4 + .../Commands/ApplicationUpdateCheckCommand.cs | 2 + .../Commands/ApplicationUpdateCommand.cs | 1 + .../Update/InstallUpdateService.cs | 14 +- .../Update/UpdateCheckService.cs | 2 +- .../Update/UpdatePackageProvider.cs | 1 + 20 files changed, 437 insertions(+), 404 deletions(-) delete mode 100644 frontend/src/System/Updates/UpdateChanges.js create mode 100644 frontend/src/System/Updates/UpdateChanges.tsx delete mode 100644 frontend/src/System/Updates/Updates.js create mode 100644 frontend/src/System/Updates/Updates.tsx delete mode 100644 frontend/src/System/Updates/UpdatesConnector.js create mode 100644 frontend/src/typings/Settings/General.ts rename frontend/src/typings/{ => Settings}/UiSettings.ts (79%) create mode 100644 frontend/src/typings/Update.ts diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c2c95f96d..34e23ac3f 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -30,7 +30,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector'; import Logs from 'System/Logs/Logs'; import Status from 'System/Status/Status'; import Tasks from 'System/Tasks/Tasks'; -import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import Updates from 'System/Updates/Updates'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector'; @@ -248,7 +248,7 @@ function AppRoutes(props) { <Route path="/system/updates" - component={UpdatesConnector} + component={Updates} /> <Route diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 222a8e26f..7f7ae00a8 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -46,6 +46,7 @@ export interface CustomFilter { } export interface AppSectionState { + version: string; dimensions: { isSmallScreen: boolean; width: number; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e4322db69..d6624ff74 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -14,13 +14,16 @@ import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; -import { UiSettings } from 'typings/UiSettings'; +import General from 'typings/Settings/General'; +import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState extends AppSectionState<DownloadClient>, AppSectionDeleteState, AppSectionSaveState {} +export type GeneralAppState = AppSectionItemState<General>; + export interface ImportListAppState extends AppSectionState<ImportList>, AppSectionDeleteState, @@ -58,6 +61,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + general: GeneralAppState; importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index d43c1d0ee..3c150fcfb 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,9 +1,12 @@ import SystemStatus from 'typings/SystemStatus'; -import { AppSectionItemState } from './AppSectionState'; +import Update from 'typings/Update'; +import AppSectionState, { AppSectionItemState } from './AppSectionState'; export type SystemStatusAppState = AppSectionItemState<SystemStatus>; +export type UpdateAppState = AppSectionState<Update>; interface SystemAppState { + updates: UpdateAppState; status: SystemStatusAppState; } diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 5bd4dd7c2..2bbe8a66c 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -5,7 +5,7 @@ import { icons } from 'Helpers/Props'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import dimensions from 'Styles/Variables/dimensions'; import QualityProfile from 'typings/QualityProfile'; -import { UiSettings } from 'typings/UiSettings'; +import UiSettings from 'typings/Settings/UiSettings'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js deleted file mode 100644 index 3588069a0..000000000 --- a/frontend/src/System/Updates/UpdateChanges.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import styles from './UpdateChanges.css'; - -class UpdateChanges extends Component { - - // - // Render - - render() { - const { - title, - changes - } = this.props; - - if (changes.length === 0) { - return null; - } - - return ( - <div> - <div className={styles.title}>{title}</div> - <ul> - { - changes.map((change, index) => { - return ( - <li key={index}> - <InlineMarkdown data={change} /> - </li> - ); - }) - } - </ul> - </div> - ); - } - -} - -UpdateChanges.propTypes = { - title: PropTypes.string.isRequired, - changes: PropTypes.arrayOf(PropTypes.string) -}; - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx new file mode 100644 index 000000000..495a9783d --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +interface UpdateChangesProps { + title: string; + changes: string[]; +} + +function UpdateChanges(props: UpdateChangesProps) { + const { title, changes } = props; + + if (changes.length === 0) { + return null; + } + + return ( + <div> + <div className={styles.title}>{title}</div> + <ul> + {changes.map((change, index) => { + return ( + <li key={index}> + <InlineMarkdown data={change} /> + </li> + ); + })} + </ul> + </div> + ); +} + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js deleted file mode 100644 index bf2563cb1..000000000 --- a/frontend/src/System/Updates/Updates.js +++ /dev/null @@ -1,249 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -class Updates extends Component { - - // - // Render - - render() { - const { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - isInstallingUpdate, - updateMechanism, - updateMechanismMessage, - shortDateFormat, - longDateFormat, - timeFormat, - onInstallLatestPress - } = this.props; - - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const externalUpdaterPrefix = translate('UpdateSonarrDirectlyLoadError'); - const externalUpdaterMessages = { - external: translate('ExternalUpdater'), - apt: translate('AptUpdater'), - docker: translate('DockerUpdater') - }; - - return ( - <PageContent title={translate('Updates')}> - <PageContentBody> - { - !isPopulated && !hasError && - <LoadingIndicator /> - } - - { - noUpdates && - <Alert kind={kinds.INFO}> - {translate('NoUpdatesAreAvailable')} - </Alert> - } - - { - hasUpdateToInstall && - <div className={styles.messageContainer}> - { - updateMechanism === 'builtIn' || updateMechanism === 'script' ? - <SpinnerButton - className={styles.updateAvailable} - kind={kinds.PRIMARY} - isSpinning={isInstallingUpdate} - onPress={onInstallLatestPress} - > - {translate('InstallLatest')} - </SpinnerButton> : - - <Fragment> - <Icon - name={icons.WARNING} - kind={kinds.WARNING} - size={30} - /> - - <div className={styles.message}> - {externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} /> - </div> - </Fragment> - } - - { - isFetching && - <LoadingIndicator - className={styles.loading} - size={20} - /> - } - </div> - } - - { - noUpdateToInstall && - <div className={styles.messageContainer}> - <Icon - className={styles.upToDateIcon} - name={icons.CHECK_CIRCLE} - size={30} - /> - <div className={styles.message}> - {translate('OnLatestVersion')} - </div> - - { - isFetching && - <LoadingIndicator - className={styles.loading} - size={20} - /> - } - </div> - } - - { - hasUpdates && - <div> - { - items.map((update) => { - const hasChanges = !!update.changes; - - return ( - <div - key={update.version} - className={styles.update} - > - <div className={styles.info}> - <div className={styles.version}>{update.version}</div> - <div className={styles.space}>—</div> - <div - className={styles.date} - title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)} - > - {formatDate(update.releaseDate, shortDateFormat)} - </div> - - { - update.branch === 'main' ? - null : - <Label - className={styles.label} - > - {update.branch} - </Label> - } - - { - update.version === currentVersion ? - <Label - className={styles.label} - kind={kinds.SUCCESS} - title={formatDateTime(update.installedOn, longDateFormat, timeFormat)} - > - {translate('CurrentlyInstalled')} - </Label> : - null - } - - { - update.version !== currentVersion && update.installedOn ? - <Label - className={styles.label} - kind={kinds.INVERSE} - title={formatDateTime(update.installedOn, longDateFormat, timeFormat)} - > - {translate('PreviouslyInstalled')} - </Label> : - null - } - </div> - - { - !hasChanges && - <div> - {translate('MaintenanceRelease')} - </div> - } - - { - hasChanges && - <div className={styles.changes}> - <UpdateChanges - title={translate('New')} - changes={update.changes.new} - /> - - <UpdateChanges - title={translate('Fixed')} - changes={update.changes.fixed} - /> - </div> - } - </div> - ); - }) - } - </div> - } - - { - !!updatesError && - <div> - {translate('FailedToFetchUpdates')} - </div> - } - - { - !!generalSettingsError && - <div> - {translate('FailedToUpdateSettings')} - </div> - } - </PageContentBody> - </PageContent> - ); - } - -} - -Updates.propTypes = { - currentVersion: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - updatesError: PropTypes.object, - generalSettingsError: PropTypes.object, - items: PropTypes.array.isRequired, - isInstallingUpdate: PropTypes.bool.isRequired, - updateMechanism: PropTypes.string, - updateMechanismMessage: PropTypes.string, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onInstallLatestPress: PropTypes.func.isRequired -}; - -export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx new file mode 100644 index 000000000..e3a3076c1 --- /dev/null +++ b/frontend/src/System/Updates/Updates.tsx @@ -0,0 +1,305 @@ +import React, { + Fragment, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { UpdateMechanism } from 'typings/Settings/General'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; + +function createUpdatesSelector() { + return createSelector( + (state: AppState) => state.system.updates, + (state: AppState) => state.settings.general, + (updates, generalSettings) => { + const { error: updatesError, items } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + updateMechanism: generalSettings.item.updateMechanism, + }; + } + ); +} + +function Updates() { + const currentVersion = useSelector((state: AppState) => state.app.version); + const { packageUpdateMechanismMessage } = useSelector( + createSystemStatusSelector() + ); + const { shortDateFormat, longDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + const isInstallingUpdate = useSelector( + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) + ); + + const { + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + updateMechanism, + } = useSelector(createUpdatesSelector()); + + const dispatch = useDispatch(); + const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + + const externalUpdaterPrefix = translate('UpdateSonarrDirectlyLoadError'); + const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = { + external: translate('ExternalUpdater'), + apt: translate('AptUpdater'), + docker: translate('DockerUpdater'), + }; + + const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { + const majorVersion = parseInt( + currentVersion.match(VERSION_REGEX)?.[0] ?? '0' + ); + + const latestVersion = items[0]?.version; + const latestMajorVersion = parseInt( + latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' + ); + + return { + isMajorUpdate: latestMajorVersion > majorVersion, + hasUpdateToInstall: items.some( + (update) => update.installable && update.latest + ), + }; + }, [currentVersion, items]); + + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const handleInstallLatestPress = useCallback(() => { + if (isMajorUpdate) { + setIsMajorUpdateModalOpen(true); + } else { + dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); + } + }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); + + const handleInstallLatestMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + + dispatch( + executeCommand({ + name: commandNames.APPLICATION_UPDATE, + installMajorUpdate: true, + }) + ); + }, [setIsMajorUpdateModalOpen, dispatch]); + + const handleCancelMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + }, [setIsMajorUpdateModalOpen]); + + useEffect(() => { + dispatch(fetchUpdates()); + dispatch(fetchGeneralSettings()); + }, [dispatch]); + + return ( + <PageContent title={translate('Updates')}> + <PageContentBody> + {isPopulated || hasError ? null : <LoadingIndicator />} + + {noUpdates ? ( + <Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert> + ) : null} + + {hasUpdateToInstall ? ( + <div className={styles.messageContainer}> + {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( + <SpinnerButton + kind={kinds.PRIMARY} + isSpinning={isInstallingUpdate} + onPress={handleInstallLatestPress} + > + {translate('InstallLatest')} + </SpinnerButton> + ) : ( + <Fragment> + <Icon name={icons.WARNING} kind={kinds.WARNING} size={30} /> + + <div className={styles.message}> + {externalUpdaterPrefix}{' '} + <InlineMarkdown + data={ + packageUpdateMechanismMessage || + externalUpdaterMessages[updateMechanism] || + externalUpdaterMessages.external + } + /> + </div> + </Fragment> + )} + + {isFetching ? ( + <LoadingIndicator className={styles.loading} size={20} /> + ) : null} + </div> + ) : null} + + {noUpdateToInstall && ( + <div className={styles.messageContainer}> + <Icon + className={styles.upToDateIcon} + name={icons.CHECK_CIRCLE} + size={30} + /> + <div className={styles.message}>{translate('OnLatestVersion')}</div> + + {isFetching && ( + <LoadingIndicator className={styles.loading} size={20} /> + )} + </div> + )} + + {hasUpdates && ( + <div> + {items.map((update) => { + const hasChanges = !!update.changes; + + return ( + <div key={update.version} className={styles.update}> + <div className={styles.info}> + <div className={styles.version}>{update.version}</div> + <div className={styles.space}>—</div> + <div + className={styles.date} + title={formatDateTime( + update.releaseDate, + longDateFormat, + timeFormat + )} + > + {formatDate(update.releaseDate, shortDateFormat)} + </div> + + {update.branch === 'main' ? null : ( + <Label className={styles.label}>{update.branch}</Label> + )} + + {update.version === currentVersion ? ( + <Label + className={styles.label} + kind={kinds.SUCCESS} + title={formatDateTime( + update.installedOn, + longDateFormat, + timeFormat + )} + > + {translate('CurrentlyInstalled')} + </Label> + ) : null} + + {update.version !== currentVersion && update.installedOn ? ( + <Label + className={styles.label} + kind={kinds.INVERSE} + title={formatDateTime( + update.installedOn, + longDateFormat, + timeFormat + )} + > + {translate('PreviouslyInstalled')} + </Label> + ) : null} + </div> + + {hasChanges ? ( + <div> + <UpdateChanges + title={translate('New')} + changes={update.changes.new} + /> + + <UpdateChanges + title={translate('Fixed')} + changes={update.changes.fixed} + /> + </div> + ) : ( + <div>{translate('MaintenanceRelease')}</div> + )} + </div> + ); + })} + </div> + )} + + {updatesError ? <div>{translate('FailedToFetchUpdates')}</div> : null} + + {generalSettingsError ? ( + <div>{translate('FailedToUpdateSettings')}</div> + ) : null} + + <ConfirmModal + isOpen={isMajorUpdateModalOpen} + kind={kinds.WARNING} + title={translate('InstallMajorVersionUpdate')} + message={ + <div> + <div>{translate('InstallMajorVersionUpdateMessage')}</div> + <div> + <InlineMarkdown + data={translate('InstallMajorVersionUpdateMessageLink', { + domain: 'sonarr.tv', + url: 'https://sonarr.tv/#downloads', + })} + /> + </div> + </div> + } + confirmLabel={translate('Install')} + onConfirm={handleInstallLatestMajorVersionPress} + onCancel={handleCancelMajorVersionPress} + /> + </PageContentBody> + </PageContent> + ); +} + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js deleted file mode 100644 index 77d75dbda..000000000 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ /dev/null @@ -1,98 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import Updates from './Updates'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - createSystemStatusSelector(), - (state) => state.system.updates, - (state) => state.settings.general, - createUISettingsSelector(), - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), - ( - currentVersion, - status, - updates, - generalSettings, - uiSettings, - isInstallingUpdate - ) => { - const { - error: updatesError, - items - } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - isInstallingUpdate, - updateMechanism: generalSettings.item.updateMechanism, - updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchUpdates: fetchUpdates, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchExecuteCommand: executeCommand -}; - -class UpdatesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - this.props.dispatchFetchGeneralSettings(); - } - - // - // Listeners - - onInstallLatestPress = () => { - this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); - }; - - // - // Render - - render() { - return ( - <Updates - onInstallLatestPress={this.onInstallLatestPress} - {...this.props} - /> - ); - } -} - -UpdatesConnector.propTypes = { - dispatchFetchUpdates: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchExecuteCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts new file mode 100644 index 000000000..c867bed74 --- /dev/null +++ b/frontend/src/typings/Settings/General.ts @@ -0,0 +1,45 @@ +export type UpdateMechanism = + | 'builtIn' + | 'script' + | 'external' + | 'apt' + | 'docker'; + +export default interface General { + bindAddress: string; + port: number; + sslPort: number; + enableSsl: boolean; + launchBrowser: boolean; + authenticationMethod: string; + authenticationRequired: string; + analyticsEnabled: boolean; + username: string; + password: string; + passwordConfirmation: string; + logLevel: string; + consoleLogLevel: string; + branch: string; + apiKey: string; + sslCertPath: string; + sslCertPassword: string; + urlBase: string; + instanceName: string; + applicationUrl: string; + updateAutomatically: boolean; + updateMechanism: UpdateMechanism; + updateScriptPath: string; + proxyEnabled: boolean; + proxyType: string; + proxyHostname: string; + proxyPort: number; + proxyUsername: string; + proxyPassword: string; + proxyBypassFilter: string; + proxyBypassLocalAddresses: boolean; + certificateValidation: string; + backupFolder: string; + backupInterval: number; + backupRetention: number; + id: number; +} diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts similarity index 79% rename from frontend/src/typings/UiSettings.ts rename to frontend/src/typings/Settings/UiSettings.ts index 3c23a0356..656c4518b 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -1,4 +1,4 @@ -export interface UiSettings { +export default interface UiSettings { theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index e72be2c5c..47f2b3552 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -19,6 +19,7 @@ interface SystemStatus { osName: string; osVersion: string; packageUpdateMechanism: string; + packageUpdateMechanismMessage: string; runtimeName: string; runtimeVersion: string; sqliteVersion: string; diff --git a/frontend/src/typings/Update.ts b/frontend/src/typings/Update.ts new file mode 100644 index 000000000..1e1ff652b --- /dev/null +++ b/frontend/src/typings/Update.ts @@ -0,0 +1,20 @@ +export interface Changes { + new: string[]; + fixed: string[]; +} + +interface Update { + version: string; + branch: string; + releaseDate: string; + fileName: string; + url: string; + installed: boolean; + installedOn: string; + installable: boolean; + latest: boolean; + changes: Changes; + hash: string; +} + +export default Update; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 751800eaf..468d9f4b8 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1023,7 +1023,11 @@ "IndexersSettingsSummary": "Indexers and indexer options", "Info": "Info", "InfoUrl": "Info URL", + "Install": "Install", "InstallLatest": "Install Latest", + "InstallMajorVersionUpdate": "Install Update", + "InstallMajorVersionUpdateMessage": "This update will install a new major version and may not be compatible with your system. Are you sure you want to install this update?", + "InstallMajorVersionUpdateMessageLink": "Please check [{domain}]({url}) for more information.", "InstanceName": "Instance Name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InteractiveImport": "Interactive Import", diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs index ece18a111..fa2cfbf41 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs @@ -7,5 +7,7 @@ namespace NzbDrone.Core.Update.Commands public override bool SendUpdatesToClient => true; public override string CompletionMessage => null; + + public bool InstallMajorUpdate { get; set; } } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 59a827a0b..6980af708 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -4,6 +4,7 @@ namespace NzbDrone.Core.Update.Commands { public class ApplicationUpdateCommand : Command { + public bool InstallMajorUpdate { get; set; } public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index b8f827972..9c645790d 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Update } } - private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger) + private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger, bool installMajorUpdate) { _logger.ProgressDebug("Checking for updates"); @@ -243,7 +243,13 @@ namespace NzbDrone.Core.Update return null; } - if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) + if (latestAvailable.Version.Major > BuildInfo.Version.Major && !installMajorUpdate) + { + _logger.ProgressInfo("Unable to install major update, please update update manually from System: Updates"); + return null; + } + + if (!_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) { _logger.ProgressDebug("Auto-update not enabled, not installing available update."); return null; @@ -272,7 +278,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCheckCommand message) { - if (GetUpdatePackage(message.Trigger) != null) + if (GetUpdatePackage(message.Trigger, true) != null) { _commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger); } @@ -280,7 +286,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCommand message) { - var latestAvailable = GetUpdatePackage(message.Trigger); + var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate); if (latestAvailable != null) { diff --git a/src/NzbDrone.Core/Update/UpdateCheckService.cs b/src/NzbDrone.Core/Update/UpdateCheckService.cs index 4f0e7d4ec..46e4b6b63 100644 --- a/src/NzbDrone.Core/Update/UpdateCheckService.cs +++ b/src/NzbDrone.Core/Update/UpdateCheckService.cs @@ -1,4 +1,4 @@ -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Update diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index b0015d48d..02bff9c4a 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtime", "netcore") .AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("dbType", _mainDatabase.DatabaseType) + .AddQueryParam("includeMajorVersion", true) .SetSegment("branch", branch); if (_analyticsService.IsEnabled) From f59c0b16ca86f1ae20be739d3a5ca559f85f595e Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 17 Jul 2024 04:33:55 +0000 Subject: [PATCH 387/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dream <seth.gecko.rr@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 23 +- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/ru.json | 320 ++++++++++++++++-- 3 files changed, 304 insertions(+), 42 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 38b150e65..6c37b0f3f 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -179,7 +179,7 @@ "BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente", "AddNewSeries": "Añadir Nueva Serie", "AddNewSeriesError": "Falló al cargar los resultados de la búsqueda, inténtelo de nuevo.", - "AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empiece escribiendo el nombre de la serie que desea añadir.", + "AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empieza escribiendo el nombre de la serie que quieres añadir.", "AddNewSeriesRootFolderHelpText": "La subcarpeta '{folder}' será creada automáticamente", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con límites no alcanzados", "AddNewSeriesSearchForMissingEpisodes": "Empezar la búsqueda de episodios faltantes", @@ -1150,7 +1150,7 @@ "KeyboardShortcutsFocusSearchBox": "Enfocar Campo de Búsqueda", "KeyboardShortcutsOpenModal": "Abrir esta Ventana Modal", "Languages": "Idiomas", - "LibraryImportSeriesHeader": "Importe las series que ya posee", + "LibraryImportSeriesHeader": "Importa las series que ya tengas", "LibraryImportTips": "Algunos consejos para que la importación vaya sobre ruedas:", "IndexerValidationTestAbortedDueToError": "El test fue abortado debido a un error: {exceptionMessage}", "Large": "Grande", @@ -1182,10 +1182,10 @@ "LogOnly": "Sólo Registro", "LongDateFormat": "Formato de Fecha Larga", "ManageEpisodesSeason": "Gestionar los archivos de Episodios de esta temporada", - "LibraryImportTipsDontUseDownloadsFolder": "No lo utilice para importar descargas desde su cliente de descargas, esto es sólo para bibliotecas organizadas existentes, no para archivos sin clasificar.", + "LibraryImportTipsDontUseDownloadsFolder": "No lo utilices para importar descargas desde tu cliente de descarga, esto es sólo para bibliotecas organizadas existentes, no para archivos sin clasificar.", "LocalAirDate": "Fecha de emisión local", "NotificationsEmailSettingsUseEncryptionHelpText": "Si prefiere utilizar el cifrado si está configurado en el servidor, utilizar siempre el cifrado mediante SSL (sólo puerto 465) o StartTLS (cualquier otro puerto) o no utilizar nunca el cifrado", - "LibraryImportTipsSeriesUseRootFolder": "Dirija {appName} a la carpeta que contiene todas sus series de TV, no a una en concreto. Por ejemplo, \"`{goodFolderExample}`\" y no \"`{badFolderExample}`\". Además, cada serie debe estar en su propia carpeta dentro de la carpeta raíz/biblioteca.", + "LibraryImportTipsSeriesUseRootFolder": "Dirije {appName} a la carpeta que contenga todas tus series de TV, no a una en concreto. Por ejemplo, \"`{goodFolderExample}`\" y no \"`{badFolderExample}`\". Además, cada serie debe estar en su propia carpeta dentro de la carpeta raíz/biblioteca.", "ListSyncTag": "Etiqueta de Sincronización de Lista", "ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)", "MetadataLoadError": "No se puede cargar Metadatos", @@ -1457,7 +1457,7 @@ "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", + "SearchForAllMissingEpisodes": "Buscar todos los episodios faltantes", "SeasonInformation": "Información de temporada", "SeasonNumber": "Número de temporada", "SeasonCount": "Recuento de temporada", @@ -1541,7 +1541,7 @@ "SourcePath": "Ruta de la fuente", "SourceRelativePath": "Ruta relativa de la fuente", "Special": "Especial", - "SourceTitle": "Título de la fuente", + "SourceTitle": "Título de origen", "SpecialEpisode": "Episodio especial", "Specials": "Especiales", "SpecialsFolderFormat": "Formato de carpeta de los especiales", @@ -1572,7 +1572,7 @@ "SelectQuality": "Seleccionar calidad", "SelectLanguage": "Seleccionar idioma", "SelectSeason": "Seleccionar temporada", - "SeriesIndexFooterMissingUnmonitored": "Episodios perdidos (Serie no monitorizada)", + "SeriesIndexFooterMissingUnmonitored": "Episodios faltantes (Serie no monitorizada)", "ShowEpisodeInformation": "Mostrar información de episodio", "ShowPath": "Mostrar ruta", "ShowNetwork": "Mostrar red", @@ -1808,7 +1808,7 @@ "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", + "SearchForMissing": "Buscar faltantes", "SeasonFinale": "Final de temporada", "SearchSelected": "Buscar seleccionados", "SeasonFolderFormat": "Formato de carpeta de temporada", @@ -1990,7 +1990,7 @@ "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)", + "SeriesIndexFooterMissingMonitored": "Episodios faltantes (Serie monitorizada)", "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Total: {totalEpisodeCount}, Descargando: {downloadingCount})", "UpgradesAllowed": "Actualizaciones permitidas", "VideoCodec": "Códec de vídeo", @@ -2038,7 +2038,7 @@ "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?", + "SearchForAllMissingEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios faltantes?", "SeriesType": "Tipo de serie", "TagCannotBeDeletedWhileInUse": "La etiqueta no puede ser borrada mientras esté en uso", "UnmonitorSpecialEpisodes": "Dejar de monitorizar especiales", @@ -2084,5 +2084,6 @@ "CustomColonReplacementFormatHelpText": "Caracteres que serán usados como reemplazo para los dos puntos", "CustomColonReplacementFormatHint": "Caracteres válidos del sistema de archivos como dos puntos (letra)", "OnFileImport": "Al importar un archivo", - "OnImportComplete": "Al completar la importación" + "OnImportComplete": "Al completar la importación", + "OnFileUpgrade": "Al actualizar archivo" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 9065661fe..14a1b943b 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2084,5 +2084,6 @@ "OnFileImport": "Ao Importar o Arquivo", "OnImportComplete": "Ao Completar Importação", "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição aos dois pontos", - "NotificationsPlexSettingsServer": "Servidor" + "NotificationsPlexSettingsServer": "Servidor", + "OnFileUpgrade": "Ao Atualizar o Arquivo" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 70acc7e7b..02ce46079 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -319,13 +319,13 @@ "DetailedProgressBar": "Подробный индикатор выполнения", "Directory": "Каталог", "DownloadClientDelugeValidationLabelPluginFailure": "Не удалось настроить метку", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Вы должны войти в свою Diskstation как {username} и вручную настроить ее в настройках DownloadStation в разделе BT/HTTP/FTP/NZB -> Местоположение", - "DownloadClientFloodSettingsPostImportTagsHelpText": "Добавить теги после импорта загрузки", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Вы должны войти в свою Diskstation как {username} и вручную настроить ее в настройках DownloadStation в разделе BT/HTTP/FTP/NZB -> Местоположение.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Добавить теги после импорта загрузки.", "DownloadClientFreeboxSettingsApiUrlHelpText": "Определите базовый URL-адрес Freebox API с версией API, например '{url}', по умолчанию — '{defaultApiUrl}'", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Первый и последний Первый", "DownloadClientQbittorrentValidationCategoryAddFailure": "Не удалось настроить категорию", - "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "Пользователю {appName} не удалось добавить метку в qBittorrent", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категории не поддерживаются до версии qBittorrent 3.3.0. Пожалуйста, обновите версию или повторите попытку, указав пустую категорию", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "Пользователю {appName} не удалось добавить метку в qBittorrent.", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категории не поддерживаются до версии qBittorrent 3.3.0. Пожалуйста, обновите версию или повторите попытку, указав пустую категорию.", "DownloadClientQbittorrentValidationQueueingNotEnabled": "Очередь не включена", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent настроен на удаление торрентов, когда они достигают предельного рейтинга (Ratio)", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Отключите опцию «Проверить перед загрузкой» в Sabnbzd", @@ -351,7 +351,7 @@ "AuthenticationRequiredWarning": "Чтобы предотвратить удаленный доступ без авторизации, {appName} теперь требует, чтобы авторизация была включена. При желании вы можете отключить авторизацию с локальных адресов.", "AutoTagging": "Автоматическая маркировка", "AutoTaggingLoadError": "Не удается загрузить автоматическую маркировку", - "AutoTaggingRequiredHelpText": "Это условие {implementationName} должно соответствовать правилу автоматической пометки. В противном случае достаточно одного совпадения {implementationName}", + "AutoTaggingRequiredHelpText": "Это условие {implementationName} должно соответствовать правилу автоматической пометки. В противном случае достаточно одного совпадения {implementationName}.", "Automatic": "Автоматически", "AutomaticAdd": "Автоматическое добавление", "AutomaticSearch": "Автоматический поиск", @@ -396,7 +396,7 @@ "DownloadClientDownloadStationValidationFolderMissingDetail": "Папка '{downloadDir}' не существует, ее необходимо создать вручную внутри общей папки '{sharedFolder}'.", "DownloadClientDownloadStationValidationSharedFolderMissing": "Общая папка не существует", "DownloadClientFloodSettingsAdditionalTags": "Дополнительные теги", - "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавляет свойства мультимедиа в виде тегов. Подсказки являются примерами", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавляет свойства мультимедиа в виде тегов. Подсказки являются примерами.", "DownloadClientFloodSettingsStartOnAdd": "Начать добавление", "DownloadClientFloodSettingsUrlBaseHelpText": "Добавляет префикс к Flood API, например {url}", "DownloadClientFreeboxAuthenticationError": "Не удалось выполнить аутентификацию в API Freebox. Причина: {errorDescription}", @@ -406,14 +406,14 @@ "DownloadClientFreeboxUnableToReachFreebox": "Невозможно получить доступ к API Freebox. Проверьте настройки «Хост», «Порт» или «Использовать SSL». (Ошибка: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "Загрузка содержит несколько файлов и находится не в папке задания: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Для этой опции требуется как минимум NzbGet версии 16.0", - "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Для параметра NzbGet KeepHistory установлено слишком высокое значение", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Для параметра NzbGet KeepHistory установлено слишком высокое значение.", "DownloadClientNzbgetValidationKeepHistoryZero": "Параметр NzbGet KeepHistory должен быть больше 0", "DownloadClientOptionsLoadError": "Не удалось загрузить параметры клиента загрузки", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Эта папка должна быть доступна из XBMC", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Файлы .strm в этой папке будут импортированы дроном", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Использовать ли настроенный макет контента qBittorrent, исходный макет из торрента или всегда создавать подпапку (qBittorrent 4.3.2+)", "DownloadClientQbittorrentSettingsSequentialOrder": "Последовательный порядок", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Используйте безопасное соединение. См. «Параметры» -> «Веб-интерфейс» -> «Использовать HTTPS вместо HTTP» в qBittorrent", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Используйте безопасное соединение. См. «Параметры» -> «Веб-интерфейс» -> «Использовать HTTPS вместо HTTP» в qBittorrent.", "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent не может разрешить магнитную ссылку с отключенным DHT", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent загружает метаданные", "DownloadClientQbittorrentTorrentStatePathError": "Невозможно импортировать. Путь соответствует базовому каталогу загрузки клиента, возможно, для этого торрента отключен параметр «Сохранить папку верхнего уровня» или для параметра «Макет содержимого торрента» НЕ установлено значение «Исходный» или «Создать подпапку»?", @@ -483,11 +483,11 @@ "VersionNumber": "Версия {version}", "Version": "Версия", "UsenetDisabled": "Usenet отключён", - "Wanted": "Разыскиваемый", + "Wanted": "Разыскиваемые", "WaitingToProcess": "Ожидает обработки", "WaitingToImport": "Ожидание импорта", "VisitTheWikiForMoreDetails": "Перейти в wiki: ", - "Continuing": "В стадии показа (или между сезонами)", + "Continuing": "Продолжается", "BlackholeFolderHelpText": "Папка, в которой {appName} будет хранить файл {extension}", "BlackholeWatchFolder": "Смотреть папку", "Category": "Категория", @@ -522,7 +522,7 @@ "DownloadClientSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {clientName}, например {url}", "UpgradeUntilEpisodeHelpText": "{appName} перестанет скачивать фильмы после достижения указанного качества", "UseProxy": "Использовать прокси", - "AddNewSeriesHelpText": "Добавить новый сериал очень просто! Начни печатать название сериала, который хочешь добавить", + "AddNewSeriesHelpText": "Добавить новый сериал очень просто! Начни печатать название сериала, который хочешь добавить.", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Начать поиск отклонённых эпизодов", "EditSelectedSeries": "Редактировать выбранный сериал", "CompletedDownloadHandling": "Обработка завершенных скачиваний", @@ -570,7 +570,7 @@ "ClearBlocklist": "Очистить черный список", "ClickToChangeReleaseGroup": "Нажмите, чтобы изменить релиз-группу", "CustomFormatJson": "Настраиваемый формат JSON", - "CustomFormats": "Настраиваемое форматирование", + "CustomFormats": "Пользовательский формат", "CustomFormatUnknownConditionOption": "Неизвестный параметр «{key}» для условия '{implementation}'", "CustomFormatsSpecificationMinimumSizeHelpText": "Релиз должен быть больше этого размера", "CustomFormatsSpecificationMaximumSizeHelpText": "Релиз должен быть меньше или равен этому размеру", @@ -597,7 +597,7 @@ "CalendarLegendEpisodeUnairedTooltip": "Эпизод еще не вышел в эфир", "Cancel": "Отменить", "Destination": "Место назначения", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. в {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. в {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Отключить сортировку фильмов", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Отключить сортировку дат", "DownloadClientUTorrentTorrentStateError": "uTorrent сообщает об ошибке", @@ -608,8 +608,8 @@ "Downloading": "Скачивается", "DownloadClientSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в {clientName}", "Warn": "Предупреждение", - "CustomFormatsSettingsTriggerInfo": "Пользовательский формат будет применен к релизу или файлу, если он соответствует хотя бы одному из каждого из выбранных типов условий", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не будет пытаться импортировать завершенные загрузки без категории", + "CustomFormatsSettingsTriggerInfo": "Пользовательский формат будет применен к релизу или файлу, если он соответствует хотя бы одному из каждого из выбранных типов условий.", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не будет пытаться импортировать завершенные загрузки без категории.", "DownloadClientRTorrentSettingsUrlPath": "URL-путь", "DownloadClientSettingsUseSslHelpText": "Использовать безопасное соединение при подключении к {clientName}", "DownloadClientSettingsCategorySubFolderHelpText": "Добавление категории, специфичной для {appName}, позволяет избежать конфликтов с несвязанными загрузками, не относящимися к {appName}. Использование категории не является обязательным, но настоятельно рекомендуется. Создает подкаталог [category] в выходном каталоге.", @@ -627,7 +627,7 @@ "DailyEpisodeTypeDescription": "Эпизоды, выходящие ежедневно или реже, в которых используются год-месяц-день (2023-08-04)", "DailyEpisodeTypeFormat": "Дата ({format})", "Database": "База данных", - "DefaultDelayProfileSeries": "Это профиль по умолчанию. Он относится ко всем сериалам, у которых нет явного профиля", + "DefaultDelayProfileSeries": "Это профиль по умолчанию. Он относится ко всем сериалам, у которых нет явного профиля.", "DefaultNameCopiedProfile": "{name} - Копировать", "DeleteEmptyFolders": "Удалить пустые папки", "CutoffUnmetLoadError": "Ошибка при загрузке элементов не выполнивших порог", @@ -695,7 +695,7 @@ "Delete": "Удалить", "DownloadClientFreeboxSettingsAppId": "Идентификатор приложения", "AddNewSeriesRootFolderHelpText": "Подпапка '{folder}' будет создана автоматически", - "AnalyseVideoFilesHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого потребуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска и сети во время сканирования.", + "AnalyseVideoFilesHelpText": "Извлекать из файлов видео тех. данные. Для этого потребуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска и сети во время сканирования.", "ApplyTagsHelpTextHowToApplyDownloadClients": "Как применить теги к выбранным клиентам загрузки", "ChangeCategoryHint": "Перенести загружаемое в «Категорию после импорта» из клиента загрузки", "ChownGroupHelpTextWarning": "Это работает только если пользователь {appName} является владельцем файла. Проверьте, что программа для скачивания использует туже самую группу, что и {appName}.", @@ -713,16 +713,16 @@ "CustomFormatHelpText": "{appName} оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите действительный метод аутентификации", "CustomFormatUnknownCondition": "Неизвестное условие пользовательского формата '{implementation}'", - "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это позволяет избежать конфликтов с несвязанными загрузками", + "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это позволяет избежать конфликтов с несвязанными загрузками.", "DownloadPropersAndRepacksHelpTextCustomFormat": "Используйте 'Не предпочитать' для сортировки по рейтингу пользовательского формата по сравнению с Propers / Repacks", - "DownloadPropersAndRepacksHelpTextWarning": "Используйте настраиваемое форматирование для автоматических обновлений до Проперов/Репаков", + "DownloadPropersAndRepacksHelpTextWarning": "Используйте пользовательский формат для автоматических обновлений до Проперов/Репаков", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматический поиск и попытка загрузки другого релиза, если неудачный релиз был получен из интерактивного поиска", "EnableHelpText": "Включить создание файла метаданных для этого типа метаданных", "AutoTaggingNegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {implementationName} .", - "DownloadClientFreeboxUnableToReachFreeboxApi": "Невозможно получить доступ к API Freebox. Проверьте настройку «URL-адрес API» для базового URL-адреса и версии", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Невозможно получить доступ к API Freebox. Проверьте настройку «URL-адрес API» для базового URL-адреса и версии.", "EnableInteractiveSearchHelpText": "Будет использовано при интерактивном поиске", "DelayProfileSeriesTagsHelpText": "Применимо к сериаламс хотя бы одним подходящим тегом", - "EnableMediaInfoHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого требуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска или сети во время сканирования", + "EnableMediaInfoHelpText": "Извлекать из файлов видео разрешение, длительность и информацию о кодеках. Для этого требуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска или сети во время сканирования.", "AutomaticUpdatesDisabledDocker": "Автоматические обновления напрямую не поддерживаются при использовании механизма обновления Docker. Вам нужно будет обновить образ контейнера за пределами {appName} или использовать скрипт", "DelayingDownloadUntil": "Приостановить скачивание до {date} в {time}", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Для параметра NzbGet KeepHistory установлено значение 0. Это не позволяет {appName} видеть завершенные загрузки.", @@ -732,7 +732,7 @@ "EpisodeRequested": "Запрошен эпизод", "BindAddressHelpText": "Действительный IP-адрес, локальный адрес или '*' для всех интерфейсов", "DeleteEpisodesFilesHelpText": "Удалить файлы и папку сериала", - "DownloadClientPriorityHelpText": "Приоритет клиента загрузки от 1 (самый высокий) до 50 (самый низкий). По умолчанию: 1. Для клиентов с одинаковым приоритетом используется циклический перебор", + "DownloadClientPriorityHelpText": "Приоритет клиента загрузки от 1 (самый высокий) до 50 (самый низкий). По умолчанию: 1. Для клиентов с одинаковым приоритетом используется циклический перебор.", "EpisodeTitleFootNote": "При необходимости можно управлять усечением до максимального количества байтов, включая многоточие (`...`). Поддерживается усечение как с конца (например, `{Episode Title:30}`), так и с начала (например, `{Episode Title:-30}`). При необходимости названия эпизодов будут автоматически обрезаны в соответствии с ограничениями файловой системы.", "BlackholeWatchFolderHelpText": "Папка, из которой {appName} должно импортировать завершенные загрузки", "DeleteReleaseProfileMessageText": "Вы действительно хотите удалить профиль релиза '{name}'?", @@ -742,13 +742,13 @@ "BlocklistRelease": "Релиз из черного списка", "DeletedReasonManual": "Файл был удален с помощью {appName} вручную или с помощью другого инструмента через API", "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Очередь торрентов не включена в настройках qBittorrent. Включите его в qBittorrent или выберите «Последний» в качестве приоритета", - "DownloadClientDelugeValidationLabelPluginFailureDetail": "Пользователю {appName} не удалось добавить метку к клиенту {clientName}", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Чтобы использовать категории, у вас должен быть включен плагин меток в {clientName}", - "DownloadClientRTorrentProviderMessage": "rTorrent не будет приостанавливать торренты, если они соответствуют критериям раздачи. {appName} будет обрабатывать автоматическое удаление торрентов на основе текущих критериев раздачи в Настройки->Индексаторы, только если включена опция «Удаление завершенных». После импорта он также установит {importedView} в качестве представления rTorrent, которое можно использовать в сценариях rTorrent для настройки поведения", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магниты в rTorrent в остановленном состоянии. Это может привести к поломке магнет файлов", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Клиент загрузки {downloadClientName} настроен на удаление завершенных загрузок. Это может привести к удалению загрузок из вашего клиента до того, как {appName} сможет их импортировать", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Использование функции «Проверка перед загрузкой» влияет на возможность приложения {appName} отслеживать новые загрузки. Также Sabnzbd рекомендует вместо этого «Отменять задания, которые невозможно завершить», поскольку это более эффективно", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Очередь торрентов не включена в настройках qBittorrent. Включите его в qBittorrent или выберите «Последний» в качестве приоритета.", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "Пользователю {appName} не удалось добавить метку к клиенту {clientName}.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Чтобы использовать категории, у вас должен быть включен плагин меток в {clientName}.", + "DownloadClientRTorrentProviderMessage": "rTorrent не будет приостанавливать торренты, если они соответствуют критериям раздачи. {appName} будет обрабатывать автоматическое удаление торрентов на основе текущих критериев раздачи в Настройки->Индексаторы, только если включена опция «Удаление завершенных». После импорта он также установит {importedView} в качестве представления rTorrent, которое можно использовать в сценариях rTorrent для настройки поведения.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магниты в rTorrent в остановленном состоянии. Это может привести к поломке магнет файлов.", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Клиент загрузки {downloadClientName} настроен на удаление завершенных загрузок. Это может привести к удалению загрузок из вашего клиента до того, как {appName} сможет их импортировать.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Использование функции «Проверка перед загрузкой» влияет на возможность приложения {appName} отслеживать новые загрузки. Также Sabnzbd рекомендует вместо этого «Отменять задания, которые невозможно завершить», поскольку это более эффективно.", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Вы должны отключить сортировку по дате для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", "DownloadClientSettingsCategoryHelpText": "Добавление категории, специфичной для {appName}, позволяет избежать конфликтов с несвязанными загрузками, не относящимися к {appName}. Использование категории не является обязательным, но настоятельно рекомендуется.", "DownloadClientSettingsPostImportCategoryHelpText": "Категория для приложения {appName}, которую необходимо установить после импорта загрузки. {appName} не удалит торренты в этой категории, даже если раздача завершена. Оставьте пустым, чтобы сохранить ту же категорию.", @@ -763,5 +763,265 @@ "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Включите теги миниатюр изображений в <имя файла>.nfo (требуются метаданные эпизода)", "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Включить элемент руководства по эпизодам в формате JSON в tvshow.nfo (требуются «метаданные сериала»)", "MetadataSettingsSeriesSummary": "Создавать файлы метаданных при импорте эпизодов или обновлении сериалов", - "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo с полными метаданными сериала" + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo с полными метаданными сериала", + "Events": "События", + "NoHistoryFound": "История не найдена", + "UtcAirDate": "Дата выхода в эфир по UTC", + "UsenetDelayTime": "Задержка Usenet: {usenetDelay}", + "Example": "Пример", + "ExportCustomFormat": "Экспортировать пользовательский формат", + "Network": "Сеть", + "Name": "Имя", + "NotificationsPushoverSettingsRetry": "Повторить попытку", + "NotificationsSettingsUpdateLibrary": "Обновить библиотеку", + "NotificationsSettingsUpdateMapPathsTo": "Карта путей к", + "NotificationsSignalSettingsGroupIdPhoneNumber": "Идентификатор группы/номер телефона", + "NotificationsSignalSettingsSenderNumberHelpText": "Номер телефона отправителя записывается в signal-api", + "NotificationsTagsSeriesHelpText": "Отправляйте уведомления только для сериалов, у которых есть хотя бы один соответствующий тег", + "NotificationsTelegramSettingsTopicId": "Идентификатор темы", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Аутентификация с помощью Trakt", + "NotificationsTraktSettingsAuthUser": "Авторизация пользователя", + "NotificationsTraktSettingsRefreshToken": "Обновить токен", + "NotificationsTraktSettingsExpires": "Срок действия истекает", + "NotificationsValidationInvalidHttpCredentials": "Учетные данные HTTP-аутентификации недействительны: {exceptionMessage}", + "OnSeriesAdd": "При добавлении сериала", + "OpenSeries": "Открытый сериал", + "WhatsNew": "Что нового?", + "WhyCantIFindMyShow": "Почему я не могу найти свое шоу?", + "UpcomingSeriesDescription": "Сериал анонсирован, но точной даты выхода пока нет", + "Existing": "Существующий", + "ErrorLoadingPage": "Произошла ошибка при загрузке этой страницы", + "External": "Внешний", + "Never": "Никогда", + "Ok": "Хорошо", + "UpdateSonarrDirectlyLoadError": "Невозможно обновить {appName} напрямую,", + "WeekColumnHeader": "Заголовок столбца недели", + "OrganizeModalHeader": "Упорядочить и переименовать", + "MultiLanguages": "Многоязычный", + "MultiSeason": "Многосезонный", + "MoveSeriesFoldersToNewPath": "Хотите переместить файлы сериала из '{originalPath}' в '{destinationPath}'?", + "NoEpisodesFoundForSelectedSeason": "Для выбранного сезона не найдено ни одного эпизода", + "NoIssuesWithYourConfiguration": "С вашей конфигурацией нет проблем", + "NoLeaveIt": "Нет, оставить", + "OnLatestVersion": "Последняя версия {appName} уже установлена", + "OpenBrowserOnStart": "Открывать браузер при запуске", + "OrganizeLoadError": "Ошибка при загрузке предпросмотра", + "WeekColumnHeaderHelpText": "Отображается над каждым столбцом, когда неделя активна", + "Extend": "Продлить", + "OptionalName": "Опциональное имя", + "Options": "Опции", + "ExpandAll": "Развернуть Все", + "MoveFiles": "Переместить файлы", + "NoMonitoredEpisodes": "В этом сериале нет отслеживаемых эпизодов", + "NotificationTriggersHelpText": "Выберите, какие события должны вызвать это уведомление", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Конфигурационный ключ для решения постоянного хранения. Оставьте пустым, если используются URL-адреса без сохранения состояния.", + "NotificationsSignalSettingsPasswordHelpText": "Пароль, используемый для аутентификации запросов к signal-api", + "NotificationsSimplepushSettingsEventHelpText": "Настройте поведение push-уведомлений", + "XmlRpcPath": "Путь XML RPC", + "MoveSeriesFoldersToRootFolder": "Хотите переместить папки сериала в '{destinationRootFolder}'?", + "NamingSettings": "Настройки именования", + "Negate": "Отрицать", + "Monitored": "Отслеживается", + "No": "Нет", + "NoMinimumForAnyRuntime": "Нет минимума для любого времени", + "NoResultsFound": "Нет результатов", + "Yes": "Да", + "Monitor": "Монитор", + "MoreDetails": "Ещё подробности", + "EpisodesLoadError": "Невозможно загрузить эпизоды", + "ErrorRestoringBackup": "Ошибка при восстановлении данных", + "Exception": "Исключение", + "ExistingSeries": "Существующие сериалы", + "ExternalUpdater": "{appName} настроен на использование внешнего механизма обновления", + "DownloadClientRTorrentSettingsAddStopped": "Добавить остановленные", + "MonitorFutureEpisodesDescription": "Следите за эпизодами, которые еще не вышли в эфир", + "MonitorNewSeasonsHelpText": "Какие новые сезоны следует отслеживать автоматически", + "MonitorRecentEpisodesDescription": "Отслеживать эпизоды, вышедшие в эфир за последние 90 дней, а также будущие выпуски", + "MonitorSelected": "Отслеживание выбрано", + "MonitorSpecialEpisodes": "Отслеживать спец. эпизоды", + "MultiEpisodeInvalidFormat": "Мульти-эпизод: неверный формат", + "New": "Новый", + "NoChange": "Нет изменений", + "NoChanges": "Нет изменений", + "NoSeriesHaveBeenAdded": "Вы не добавили никаких сериалов, желаете начать с импорта всех или нескольких сериалов?", + "NoTagsHaveBeenAddedYet": "Теги еще не добавлены", + "NotificationsAppriseSettingsServerUrlHelpText": "Укажите URL-адрес сервера, включая http(s):// и порт, если необходимо", + "NotificationsPushcutSettingsTimeSensitive": "Чувствителен ко времени", + "NotificationsPushoverSettingsDevices": "Устройства", + "NotificationsPushoverSettingsDevicesHelpText": "Список имен устройств (оставьте пустым, чтобы отправить на все устройства)", + "NotificationsPushoverSettingsExpire": "Истекает", + "NotificationsPushoverSettingsExpireHelpText": "Максимальное время повторной попытки экстренных оповещений, максимум 86400 секунд\"", + "NotificationsPushoverSettingsRetryHelpText": "Интервал повтора экстренных оповещений, минимум 30 секунд", + "NotificationsPushoverSettingsSound": "Звук", + "NotificationsSettingsUpdateMapPathsFrom": "Карта путей от", + "NotificationsSettingsUpdateMapPathsFromHelpText": "Путь {appName}, используемый для изменения путей к сериалам, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", + "NotificationsSettingsUseSslHelpText": "Подключитесь к {serviceName} по протоколу HTTPS вместо HTTP", + "NotificationsSettingsWebhookMethod": "Метод", + "NotificationsSettingsWebhookMethodHelpText": "Какой метод HTTP использовать для отправки в веб-сервис", + "NotificationsSettingsWebhookUrl": "URL вебхука", + "NotificationsSignalSettingsSenderNumber": "Номер отправителя", + "NotificationsSignalSettingsUsernameHelpText": "Имя пользователя, используемое для аутентификации запросов к signal-api", + "NotificationsSimplepushSettingsEvent": "Событие", + "NotificationsSimplepushSettingsKey": "Ключ", + "NotificationsSlackSettingsChannel": "Канал", + "NotificationsSlackSettingsChannelHelpText": "Переопределяет канал по умолчанию для входящего вебхука (#other-channel)", + "NotificationsSlackSettingsIcon": "Значок", + "NotificationsSlackSettingsWebhookUrlHelpText": "URL-адрес вебхука канала Slack", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Вызовите Synoindex на локальном хосте, чтобы обновить файл библиотеки", + "NotificationsSynologyValidationInvalidOs": "Должно быть Synology", + "NotificationsTelegramSettingsBotToken": "Токен бота", + "NotificationsTelegramSettingsChatIdHelpText": "Для получения сообщений необходимо начать разговор с ботом или добавить его в свою группу", + "NotificationsTelegramSettingsIncludeAppName": "Включить {appName} в заголовок", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "При необходимости добавьте к заголовку сообщения префикс {appName}, чтобы отличать уведомления от разных приложений", + "NotificationsTelegramSettingsSendSilently": "Отправить молча", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Отправляет сообщение молча. Пользователи получат уведомление без звука", + "NotificationsTelegramSettingsTopicIdHelpText": "Укажите идентификатор темы, чтобы отправлять уведомления в эту тему. Оставьте пустым, чтобы использовать общую тему (только для супергрупп)", + "NotificationsTraktSettingsAccessToken": "Токен доступа", + "NotificationsTwitterSettingsAccessTokenSecret": "Секрет токена доступа", + "NotificationsTwitterSettingsDirectMessage": "Личное сообщение", + "NotificationsValidationInvalidApiKey": "Ключ API недействителен", + "NotificationsValidationInvalidApiKeyExceptionMessage": "Ключ API недействителен: {exceptionMessage}", + "NotificationsValidationInvalidUsernamePassword": "Неправильное имя пользователя или пароль", + "NotificationsValidationUnableToConnect": "Невозможно подключиться: {exceptionMessage}", + "NotificationsValidationUnableToConnectToService": "Невозможно подключиться к {serviceName}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Невозможно отправить тестовое сообщение. Ответ от API: {error}", + "OnApplicationUpdate": "При обновлении приложения", + "OnEpisodeFileDelete": "При удалении файла эпизода", + "OnEpisodeFileDeleteForUpgrade": "При удалении файла эпизода для обновления", + "OnHealthIssue": "О проблемах в системе", + "OnFileImport": "При импорте файла", + "OnSeriesDelete": "При удалении сериала", + "OneMinute": "1 минута", + "OnImportComplete": "При завершении импорта", + "OneSeason": "1 сезон", + "OnlyForBulkSeasonReleases": "Только для релизов полных сезонов", + "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления. Вы так же можете установить в Система: Обновления", + "Upcoming": "Предстоящие", + "UnselectAll": "Снять все выделения", + "UnmonitoredOnly": "Только отслеживаемые", + "UpdateSelected": "Обновление выбрано", + "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который обрабатывает остатки после процесса обновления", + "UpdateMonitoring": "Мониторинг обновлений", + "UpdateAvailableHealthCheckMessage": "Доступно новое обновление", + "UsenetBlackhole": "Usenet Черная дыра", + "YesterdayAt": "Вчера в {time}", + "WithFiles": "С файлами", + "Wiki": "Wiki", + "Week": "Неделя", + "None": "Ничто", + "NotificationTriggers": "Триггеры уведомления", + "UsenetBlackholeNzbFolder": "Nzb папка", + "YesCancel": "Да, отменить", + "MultiEpisodeStyle": "Стиль для мульти-эпизода", + "UpdateFiltered": "Фильтр обновлений", + "Year": "Год", + "MonitorLastSeason": "Последний сезон", + "MonitorAllEpisodes": "Все эпизоды", + "MonitorNoNewSeasonsDescription": "Не отслеживать новые сезоны автоматически", + "MonitoringOptions": "Опции отслеживания", + "Month": "Месяц", + "MonitorNewSeasons": "Следить за новыми сезонами", + "MoreInfo": "Ещё инфо", + "More": "Более", + "MultiEpisode": "Мульти-эпизод", + "MustContainHelpText": "Релиз должен содержать хотя бы одно из этих условий (без учета регистра)", + "MustNotContain": "Не должен содержать", + "MyComputer": "Мой компьютер", + "MoveSeriesFoldersDontMoveFiles": "Нет, я сам перенесу файлы", + "MoveSeriesFoldersMoveFiles": "Да, перенести файлы", + "NoMatchFound": "Совпадений не найдено!", + "NoSeasons": "Нет сезонов", + "NextExecution": "Следующее выполнение", + "NotificationsTelegramSettingsChatId": "Идентификатор чата", + "NotificationsSynologyValidationTestFailed": "Не Synology или Synoindex недоступен", + "NotificationsTwitterSettingsConnectToTwitter": "Подключиться к Твиттеру / X", + "NotificationsTwitterSettingsConsumerKey": "Потребительский ключ", + "NotificationsTwitterSettingsAccessToken": "Токен доступа", + "Yesterday": "Вчера", + "UnsavedChanges": "Несохраненные изменения", + "WantMoreControlAddACustomFormat": "Хотите больше контроля над тем, какие загрузки являются предпочтительными? Добавьте [Пользовательский формат](/settings/customformats)", + "NotificationsAppriseSettingsPasswordHelpText": "Пароль базовой аутентификации HTTP", + "Error": "Ошибка", + "NotificationsValidationInvalidAccessToken": "Токен доступа недействителен", + "NotificationsTwitterSettingsMentionHelpText": "Упоминать этого пользователя в отправленных твитах", + "UpdateAll": "Обновить всё", + "UpdateMechanismHelpText": "Используйте встроенную в {appName} функцию обновления или скрипт", + "OnRename": "При переименовании", + "ErrorLoadingContents": "Ошибка при загрузке контента", + "EventType": "Тип события", + "ExistingTag": "Существующий тэг", + "ErrorLoadingItem": "Произошла ошибка при загрузке этого элемента", + "MonitorAllEpisodesDescription": "Следите за всеми эпизодами, кроме специальных", + "MonitoredOnly": "Только отслеживаемые", + "Monday": "Понедельник", + "NotificationsSendGridSettingsApiKeyHelpText": "Ключ API, сгенерированный SendGrid", + "OnlyUsenet": "Только Usenet", + "NoHistory": "Нет истории", + "NotificationsValidationInvalidAuthenticationToken": "Токен аутентификации недействителен", + "NotificationsSettingsUpdateMapPathsToHelpText": "Путь {serviceName}, используемый для изменения путей к сериям, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", + "NotificationsAppriseSettingsConfigurationKey": "Применить конфигурационный ключ", + "MonitorPilotEpisode": "Пилотный эпизод", + "MonitorNoNewSeasons": "Нет новых сезонов", + "MonitoredStatus": "Отслеживаемые/Статус", + "NoLimitForAnyRuntime": "Нет ограничений для любого времени", + "NoLinks": "Нет ссылок", + "NotificationsTwitterSettingsConsumerSecret": "Секрет потребителя", + "MonitorNewItems": "Мониторинг новых объектов", + "NoSeriesFoundImportOrAdd": "Сериал не найден. Чтобы начать работу, импортируйте существующий или новый сериал.", + "NoLogFiles": "Нет файлов журнала", + "NoEventsFound": "Событий не найдено", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Включите, чтобы пометить уведомление как \"чувствительное ко времени\"", + "NotificationsPushoverSettingsUserKey": "Пользовательский ключ", + "NotificationsPushoverSettingsSoundHelpText": "Звук уведомления. Оставьте пустым, чтобы использовать звук по умолчанию", + "OnlyTorrent": "Только торрент", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Идентификатор группы/номер телефона получателя", + "NotificationsTwitterSettingsDirectMessageHelpText": "Отправьте прямое сообщение вместо публичного сообщения", + "NotificationsSignalValidationSslRequired": "Кажется, SSL требуется", + "NotificationsSlackSettingsIconHelpText": "Измените значок, используемый для сообщений, опубликованных в Slack (эмодзи или URL-адрес)", + "NotificationsSlackSettingsUsernameHelpText": "Имя пользователя для публикации в Slack как", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Секрет потребителя из приложения Twitter", + "OpenBrowserOnStartHelpText": " Открывать браузер и переходить на страницу {appName} при запуске программы.", + "MonitorFutureEpisodes": "Будущие эпизоды", + "MonitorLastSeasonDescription": "Мониторить все эпизоды последнего сезона", + "MonitorFirstSeason": "Первый сезон", + "MonitorNoEpisodes": "Ничто", + "MonitorMissingEpisodes": "Отсутствующие эпизоды", + "MonitorNoEpisodesDescription": "Эпизоды не будут отслеживаться", + "MonitorPilotEpisodeDescription": "Отслеживать только первый эпизод первого сезона", + "Negated": "Отрицательный", + "NoEpisodeHistory": "Нет истории эпизодов", + "NoEpisodeInformation": "Информации об эпизоде нет.", + "NoEpisodeOverview": "Нет обзора эпизода", + "OnGrab": "При захвате", + "AirsDateAtTimeOn": "{date} в {time} на {networkLabel}", + "AirsTbaOn": "Будет объявлено позже на {networkLabel}", + "AirsTimeOn": "{time} на {networkLabel}", + "NoEpisodesInThisSeason": "В этом сезоне нет эпизодов", + "ErrorLoadingContent": "Произошла ошибка при загрузке этого контента", + "MonitorSeries": "Отслеживать сериал", + "NotificationsTwitterSettingsMention": "Упомянуть", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Потребительский ключ из приложения Twitter", + "MonitorRecentEpisodes": "Последние эпизоды", + "MonitoredEpisodesHelpText": "Скачать отслеживаемые эпизоды этого сериала", + "Organize": "Организовать", + "Or": "или", + "OnFileUpgrade": "При обновлении файла", + "MonitorFirstSeasonDescription": "Мониторинг всех эпизодов первого сезона. Все остальные сезоны будут игнорироваться", + "MonitorMissingEpisodesDescription": "Отслеживать эпизоды, у которых нет файлов или которые еще не вышли в эфир", + "NotificationsValidationUnableToConnectToApi": "Невозможно подключиться к API {service}. Не удалось подключиться к серверу: ({responseCode}) {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessage": "Невозможно отправить тестовое сообщение: {exceptionMessage}", + "NzbgetHistoryItemMessage": "Статус PAR: {parStatus} - Статус распаковки: {unpackStatus} - Статус перемещения: {moveStatus} - Статус сценария: {scriptStatus} - Статус удаления: {deleteStatus} - Статус отметки: {markStatus}", + "MonitorSpecialEpisodesDescription": "Отслеживайте все специальные эпизоды, не меняя отслеживаемый статус других эпизодов", + "MountSeriesHealthCheckMessage": "Смонтированный путь к сериалу смонтирован в режиме только для чтения: ", + "MustContain": "Должен содержать", + "MustNotContainHelpText": "Релиз будет не принят, если он содержит один или несколько терминов (регистрозависимы)", + "NamingSettingsLoadError": "Не удалось загрузить настройки именования", + "NegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {implementationName} .", + "NextAiring": "Следующий эфир", + "NoBackupsAreAvailable": "Нет резервных копий", + "NoDelay": "Без задержки", + "NoMonitoredEpisodesSeason": "В этом сезоне нет отслеживаемых эпизодов", + "NoUpdatesAreAvailable": "Нет обновлений", + "NotSeasonPack": "Не полные сезоны", + "NotificationsAppriseSettingsServerUrl": "URL-адрес сервера приложений" } From 80ca1a6ac29f46bc3bfbe35201bad9851cfd566b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 18 Jul 2024 18:10:43 +0300 Subject: [PATCH 388/762] Fixed: Editing Quality Profiles --- .../src/Settings/Profiles/Quality/QualityProfileFormatItems.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js index 61cbefba1..3a204665b 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js @@ -21,7 +21,7 @@ function calcOrder(profileFormatItems) { return b.score - a.score; } - return a.localeCompare(b.name, undefined, { numeric: true }); + return a.name.localeCompare(b.name, undefined, { numeric: true }); }).map((x) => items[x.format]); } From f8d75d174adcdf435b70f1f82552acf4966774f6 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 23 Jul 2024 01:25:19 +0000 Subject: [PATCH 389/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dream <seth.gecko.rr@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 10 +- .../Localization/Core/pt_BR.json | 22 +- src/NzbDrone.Core/Localization/Core/ru.json | 1077 ++++++++++++++++- src/NzbDrone.Core/Localization/Core/tr.json | 5 +- 4 files changed, 1099 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 6c37b0f3f..c95808918 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2085,5 +2085,13 @@ "CustomColonReplacementFormatHint": "Caracteres válidos del sistema de archivos como dos puntos (letra)", "OnFileImport": "Al importar un archivo", "OnImportComplete": "Al completar la importación", - "OnFileUpgrade": "Al actualizar archivo" + "OnFileUpgrade": "Al actualizar archivo", + "RatingVotes": "Calificaciones", + "ShowTagsHelpText": "Muestra etiquetas debajo del póster", + "ShowTags": "Mostrar etiquetas", + "Install": "Instalar", + "InstallMajorVersionUpdate": "Instalar actualización", + "CountVotes": "{votes} votos", + "InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?", + "InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 14a1b943b..39bb7d60f 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -13,9 +13,9 @@ "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}", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com sincronização de RSS ativada, o {appName} não baixará novos lançamentos automaticamente", - "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a pesquisa automática ativada, o {appName} não fornecerá nenhum resultado de pesquisa automática", - "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa interativa habilitada, o {appName} não fornecerá nenhum resultado de pesquisa interativa", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com a sincronização RSS habilitada, {appName} não capturará novos lançamentos automaticamente", + "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa Automática habilitada, {appName} não fornecerá nenhum resultado de pesquisa automática", + "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa Interativa habilitada, {appName} não fornecerá resultados de pesquisa interativas", "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", "Language": "Idioma", @@ -897,7 +897,7 @@ "UpgradeUntilEpisodeHelpText": "Quando essa qualidade for atingida, o {appName} não fará mais download de episódios", "UpgradeUntilThisQualityIsMetOrExceeded": "Atualize até que essa qualidade seja atendida ou excedida", "UpgradesAllowed": "Atualizações Permitidas", - "UpgradesAllowedHelpText": "se as qualidades desativadas não forem atualizadas", + "UpgradesAllowedHelpText": "se as qualidades deshabilitadas não forem atualizadas", "Uppercase": "Maiuscula", "UrlBase": "URL base", "UrlBaseHelpText": "Para suporte a proxy reverso, o padrão é vazio", @@ -913,7 +913,7 @@ "WeekColumnHeaderHelpText": "Mostrado acima de cada coluna quando a semana é a exibição ativa", "RemotePathMappingLocalPathHelpText": "Caminho que o {appName} deve usar para acessar o caminho remoto localmente", "RemoveCompletedDownloadsHelpText": "Remover downloads importados do histórico do cliente de download", - "RenameEpisodesHelpText": "O {appName} usará o nome do arquivo existente se a renomeação estiver desativada", + "RenameEpisodesHelpText": "O {appName} usará o nome do arquivo existente se a renomeação estiver deshabilitada", "ReplaceWithSpaceDashSpace": "Substituir com Espaço, Traço e Espaço", "RequiredHelpText": "Esta condição {implementationName} deve corresponder para que o formato personalizado seja aplicado. Caso contrário, uma única correspondência {implementationName} é suficiente.", "SetPermissionsLinuxHelpText": "O chmod deve ser executado quando os arquivos são importados/renomeados?", @@ -1214,7 +1214,7 @@ "OpenSeries": "Abrir Séries", "OrganizeModalHeaderSeason": "Organizar & Renomear - {season}", "ParseModalErrorParsing": "Erro ao analisar, tente novamente.", - "OrganizeRenamingDisabled": "A renomeação está desativada, nada para renomear", + "OrganizeRenamingDisabled": "A renomeação está deshabilitada, nada para renomear", "ParseModalHelpText": "Insira um título de lançamento na entrada acima", "ParseModalUnableToParse": "Não foi possível analisar o título fornecido, tente novamente.", "ProgressBarProgress": "Barra de progresso em {progress}%", @@ -2085,5 +2085,13 @@ "OnImportComplete": "Ao Completar Importação", "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição aos dois pontos", "NotificationsPlexSettingsServer": "Servidor", - "OnFileUpgrade": "Ao Atualizar o Arquivo" + "OnFileUpgrade": "Ao Atualizar o Arquivo", + "CountVotes": "{votes} votos", + "InstallMajorVersionUpdateMessage": "Esta atualização instalará uma nova versão principal e pode não ser compatível com o seu sistema. Tem certeza de que deseja instalar esta atualização?", + "ShowTags": "Mostrar Etiquetas", + "ShowTagsHelpText": "Mostrar etiquetas abaixo do pôster", + "RatingVotes": "Votos de Avaliação", + "Install": "Instalar", + "InstallMajorVersionUpdate": "Instalar Atualização", + "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações." } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 02ce46079..cbdca46c2 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -94,7 +94,7 @@ "DeleteTagMessageText": "Вы уверены, что хотите удалить тэг '{label}'?", "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить Ваш API ключ?", "ResetDefinitionTitlesHelpText": "Сбросить названия определений, а также значения", - "Socks4": "Socks4", + "Socks4": "Socks4 прокси", "ManualGrab": "Ручной захват", "OverrideGrabNoLanguage": "Должен быть выбран хотя бы один язык", "OverrideGrabNoQuality": "Качество должно быть выбрано", @@ -197,7 +197,7 @@ "AlreadyInYourLibrary": "Уже в вашей библиотеке", "Always": "Всегда", "Conditions": "Условия", - "AbsoluteEpisodeNumber": "Абсолютные номера эпизодов", + "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", "AnalyticsEnabledHelpText": "Отправлять в {appName} анонимную информацию об использовании и ошибках. Анонимная статистика включает в себя информацию о браузере, какие страницы веб-интерфейса {appName} загружены, сообщения об ошибках, а также операционной системе. Мы используем эту информацию для выявления ошибок, а также для разработки нового функционала.", @@ -584,12 +584,12 @@ "AnimeEpisodeTypeFormat": "Абсолютный номер эпизода ({format})", "CalendarLegendEpisodeDownloadedTooltip": "Сериал скачан и отсортирован", "CalendarLegendEpisodeDownloadingTooltip": "Эпизод в настоящее время загружается", - "AutoRedownloadFailed": "Неудачное скачивание", + "AutoRedownloadFailed": "Повторная загрузка не удалась", "AutoRedownloadFailedFromInteractiveSearch": "Не удалось выполнить повторную загрузку из интерактивного поиска", "AutoRedownloadFailedHelpText": "Автоматически искать и пытаться скачать разные релизы", "BeforeUpdate": "До обновления", "CalendarFeed": "Лента календаря {appName}", - "AddRootFolderError": "Невозможно загрузить корневые папки", + "AddRootFolderError": "Невозможно загрузить корневую папку", "BlocklistReleases": "Релиз из черного списка", "BypassDelayIfAboveCustomFormatScore": "Пропустить, если значение больше пользовательского формата", "CalendarLegendEpisodeMissingTooltip": "Эпизод вышел в эфир и отсутствует на диске", @@ -703,7 +703,7 @@ "CreateEmptySeriesFoldersHelpText": "Создать папки для не найденных сериалов при сканировании", "DownloadClientValidationAuthenticationFailureDetail": "Пожалуйста, подтвердите свое имя пользователя и пароль. Также проверьте, не заблокирован ли хост, на котором работает {appName}, доступ к {clientName} ограничениями белого списка в конфигурации {clientName}.", "AirsTomorrowOn": "Завтра в {time} на {networkLabel}", - "AlternateTitles": "Альтернативное название", + "AlternateTitles": "Альтернативные названия", "ApplyTagsHelpTextHowToApplySeries": "Как применить теги к выбранным сериалам", "ChangeCategoryMultipleHint": "Перенести загружаемое в «Категорию после импорта» из клиента загрузки", "DownloadClientFloodSettingsRemovalInfo": "{appName} будет автоматически удалять торренты на основе текущих критериев раздачи в Настройки -> Индексаторы", @@ -1023,5 +1023,1070 @@ "NoMonitoredEpisodesSeason": "В этом сезоне нет отслеживаемых эпизодов", "NoUpdatesAreAvailable": "Нет обновлений", "NotSeasonPack": "Не полные сезоны", - "NotificationsAppriseSettingsServerUrl": "URL-адрес сервера приложений" + "NotificationsAppriseSettingsServerUrl": "URL-адрес сервера приложений", + "SetPermissionsLinuxHelpText": "Следует ли запускать chmod при импорте/переименовании файлов?", + "SupportedIndexersMoreInfo": "Для получения дополнительной информации об отдельных индексаторах нажмите кнопку «Дополнительная информация».", + "Tasks": "Задачи", + "TotalSpace": "Общее сводное место", + "Rejections": "Отказы", + "Repack": "Репак (Repack)", + "SceneNumbering": "Нумерация сцен", + "SearchForQuery": "Искать {query}", + "SeriesFinale": "Финал сериала", + "Shutdown": "Выключить", + "ResetQualityDefinitions": "Сбросить определения качества", + "RestartLater": "Перезапущу позднее", + "MinutesThirty": "30 минут: {thirty}", + "FilterInLast": "напоследок", + "IncludeHealthWarnings": "Включить предупреждения о здоровье", + "IndexerSettingsAnimeCategories": "Категории аниме", + "IndexerSettingsRssUrl": "URL-адрес RSS-канала", + "IndexerValidationRequestLimitReached": "Достигнут лимит запросов: {exceptionMessage}", + "Mechanism": "Механизм", + "MegabytesPerMinute": "Мегабайт в минуту", + "NotificationsCustomScriptSettingsName": "Пользовательский скрипт", + "NotificationsDiscordSettingsWebhookUrlHelpText": "URL вебхука канала Discord", + "NotificationsEmailSettingsCcAddressHelpText": "Список получателей копий электронной почты, разделенный запятыми", + "NotificationsEmailSettingsRecipientAddress": "Адрес(а) получателя", + "NotificationsEmbySettingsSendNotifications": "Отправить уведомления", + "NotificationsGotifySettingIncludeSeriesPoster": "Включая постер сериала", + "NotificationsKodiSettingsCleanLibrary": "Очистить библиотеку", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Ваш API-ключ из вашего профиля", + "NotificationsNtfySettingsTopics": "Темы", + "NotificationsPlexSettingsAuthToken": "Токен авторизации", + "NotificationsPushBulletSettingsAccessToken": "Токен доступа", + "PasswordConfirmation": "Подтверждение пароля", + "Paused": "Приостановлено", + "ProfilesSettingsSummary": "Профили качества, языковой задержки и релиза", + "PreviouslyInstalled": "Ранее установленный", + "ProgressBarProgress": "Индикатор выполнения: {progress}%", + "QualityProfile": "Профиль качества", + "RecyclingBinCleanupHelpText": "Установите значение 0, чтобы отключить автоматическую очистку", + "RefreshAndScan": "Обновить и сканировать", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Локальный загрузочный клиент {downloadClientName} сообщил о файлах в {path}, но это недопустимый путь {osName}. Проверьте настройки загрузочного клиента.", + "RemotePathMappingRemotePathHelpText": "Корневой путь к каталогу, к которому имеет доступ загрузочный клиент", + "RemoveRootFolder": "Удалить корневой каталог", + "RemoveDownloadsAlert": "Параметры удаления были перенесены в отдельные настройки загрузочного клиента выше таблицы.", + "Restart": "Перезапустить", + "Restore": "Восстановить", + "RootFoldersLoadError": "Невозможно загрузить корневые папки", + "SceneInfo": "Информация о сцене", + "SceneInformation": "Информация о сцене", + "SearchForAllMissingEpisodesConfirmationCount": "Вы уверены, что хотите найти все ({totalRecords}) недостающие эпизоды ?", + "SelectAll": "Выбрать все", + "SecretToken": "Секретный токен", + "SelectQuality": "Выбрать качество", + "SeriesEditor": "Редактор сериала", + "Small": "Маленький", + "SslCertPath": "Путь к SSL сертификату", + "Sunday": "Воскресенье", + "TorrentDelay": "Задержка торрента", + "Umask750Description": "{octal} - Владелец (запись), Группы (чтение)", + "UnmonitorSpecialsEpisodesDescription": "Отслеживайте все специальные эпизоды, не меняя отслеживаемый статус других эпизодов", + "Theme": "Тема", + "SeasonPack": "Сезонный пак", + "TorrentsDisabled": "Торренты выключены", + "Torrents": "Торренты", + "TotalFileSize": "Общий объем файла", + "Medium": "Средний", + "FailedToLoadTranslationsFromApi": "Не удалось загрузить переводы из API", + "FilterEqual": "равно", + "FilterInNext": "в следующий", + "FilterNotEqual": "не равно", + "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatAgeMinutes": "минуты", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} файлов эпизодов", + "SeriesMatchType": "Тип соответствия сериала", + "Languages": "Языки", + "Imported": "Импортировано", + "ShownClickToHide": "Показано, нажмите, чтобы скрыть", + "SizeLimit": "Ограничение по размеру", + "IndexerOptionsLoadError": "Невозможно загрузить параметры индексатора", + "MaximumSize": "Максимальный размер", + "Trace": "След", + "Size": "Размер", + "LocalAirDate": "Локальная дата эфира", + "LongDateFormat": "Длинный формат даты", + "RemoveFilter": "Удалить фильтр", + "Password": "Пароль", + "ReleaseSceneIndicatorAssumingTvdb": "Предполагается нумерация TVDB.", + "ExtraFileExtensionsHelpText": "Список дополнительных файлов для импорта, разделенных запятыми (.nfo будет импортирован как .nfo-orig)", + "Progress": "Прогресс", + "ICalShowAsAllDayEvents": "Показать как события на весь день", + "Season": "Сезон", + "SendAnonymousUsageData": "Отправка анонимных данных об использовании", + "HistorySeason": "Посмотреть историю этого сезона", + "SeriesEditRootFolderHelpText": "Перемещение сериала в ту же корневую папку можно использовать для переименования папок сериала в соответствии с обновленным заголовком или форматом именования", + "SeriesFolderFormatHelpText": "Используется при добавлении или перемещении новых сериалов через редактор", + "SeriesIndexFooterDownloading": "Загрузка (один или несколько эпизодов)", + "SeriesIndexFooterContinuing": "Продолжается (все эпизоды скачаны)", + "SeriesIndexFooterMissingUnmonitored": "Отсутствующие эпизоды (сериал не отслеживается)", + "SeriesTypesHelpText": "Тип сериала используется для переименования, анализа и поиска", + "ShowEpisodeInformation": "Показать информацию об эпизоде", + "SmartReplaceHint": "Тире или пробел в зависимости от имени", + "Sort": "Сортировка", + "SourceRelativePath": "Относительный путь источника", + "Source": "Источник", + "SourceTitle": "Название источника", + "SupportedDownloadClients": "{appName} поддерживает многие популярные торрент и usenet-клиенты для скачивания.", + "SupportedImportListsMoreInfo": "Для дополнительной информации по спискам импорта нажмите эту кнопку.", + "SingleEpisodeInvalidFormat": "Одиночный эпизод: неверный формат", + "QualitySettings": "Настройки качества", + "Queued": "В очереди", + "IndexerIPTorrentsSettingsFeedUrl": "URL-адрес фида", + "IndexerSettingsAdditionalParametersNyaa": "Дополнительные параметры", + "IndexerSettingsCategories": "Категории", + "IndexerValidationSearchParametersNotSupported": "Индексатор не поддерживает необходимые параметры поиска", + "Refresh": "Обновить", + "InteractiveSearch": "Интерактивный поиск", + "LibraryImport": "Импорт библиотеки", + "TestAll": "Тестировать все", + "TagsLoadError": "Невозможно загрузить теги", + "NotificationsLoadError": "Невозможно загрузить уведомления", + "NotificationsNtfySettingsPasswordHelpText": "Дополнительный пароль", + "ListsLoadError": "Невозможно загрузить списки", + "LogFiles": "Файлы журнала", + "ToggleMonitoredToUnmonitored": "Отслеживается, нажмите, чтобы отключить отслеживание", + "TheTvdb": "TheTVDB (The TV Database)", + "ToggleUnmonitoredToMonitored": "Не отслеживается, нажмите, чтобы отслеживать", + "MatchedToSeason": "Соответствует сезону", + "MaximumSizeHelpText": "Максимальный размер загружаемого релиза в МБ. Установите 0, чтобы снять все ограничения", + "LogLevelTraceHelpTextWarning": "Отслеживание журнала следует включать только временно", + "MediaManagementSettingsSummary": "Именование, настройки управления файлами и корневыми папками", + "MinimumFreeSpace": "Минимальное свободное место", + "Mode": "Режим", + "MissingLoadError": "Ошибка загрузки отсутствующих элементов", + "OrganizeNothingToRename": "Успешно! Моя работа завершена, файлов для переименования нет.", + "PartialSeason": "Частичный сезон", + "OverrideGrabNoEpisode": "Необходимо выбрать хотя бы один эпизод", + "Pending": "В ожидании", + "PreferTorrent": "Предпочитать торрент", + "PreferredSize": "Предпочтительный размер", + "Preferred": "Предпочтительный", + "Priority": "Приоритет", + "Profiles": "Профили", + "PublishedDate": "Дата публикации", + "Qualities": "Качества", + "RegularExpressionsTutorialLink": "Более подробную информацию о регулярных выражениях можно найти [здесь]({url}).", + "RelativePath": "Относительный путь", + "ReleaseGroups": "Релиз группы", + "ReleaseProfile": "Профиль релиза", + "RemoveCompleted": "Удаление завершено", + "Runtime": "Продолжительность", + "SearchByTvdbId": "Вы также можете выполнить поиск, используя идентификатор сериала в TVDB. например: tvdb:71663", + "Script": "Скрипт", + "SearchSelected": "Искать выделенные", + "SetTags": "Установить теги", + "SetReleaseGroup": "Установить релиз-группу", + "ShowMonitored": "Показать отслеживаемые", + "ShowEpisodes": "Показать эпизоды", + "Started": "Запущено", + "SslCertPassword": "Пароль SSL сертификата", + "TagCannotBeDeletedWhileInUse": "Невозможно удалить во время использования", + "Type": "Тип", + "Dash": "Тире", + "Folder": "Папка", + "Fixed": "Исправлено", + "FullSeason": "Полный сезон", + "From": "Из", + "IRC": "IRC", + "IndexerSettings": "Настройки индексатора", + "Info": "Информация", + "ListRootFolderHelpText": "Элементы списка корневых папок будут добавлены в", + "ListTagsHelpText": "Теги, которые будут добавлены при импорте из этого списка", + "Logs": "Журналы", + "ProxyType": "Тип прокси", + "QualityDefinitions": "Определение качества", + "Rating": "Рейтинг", + "RemoveFailed": "Удаление не удалось", + "Series": "Сериалы", + "Indexers": "Индексаторы", + "IncludeUnmonitored": "Включить неотслеживаемые", + "Filename": "Имя файла", + "FormatShortTimeSpanMinutes": "{minutes} минут(ы)", + "FormatShortTimeSpanSeconds": "{seconds} секунд(ы)", + "General": "Основное", + "GeneralSettings": "Основные настройки", + "GeneralSettingsLoadError": "Невозможно загрузить основные настройки", + "GrabSelected": "Захватить выбранные", + "HasMissingSeason": "Отсутствует сезон", + "Group": "Группа", + "Grabbed": "Захвачено", + "IconForSpecialsHelpText": "Показать значок для спец. эпизодов (сезон 0)", + "ImportList": "Импортировать список", + "ImportListsAniListSettingsImportFinished": "Импорт завершен", + "ImportListsAniListSettingsImportDroppedHelpText": "Список: исключено", + "ImportListsAniListSettingsImportHiatusHelpText": "Медиа: Сериал на перерыве", + "ImportListsAniListSettingsImportPausedHelpText": "Список: На удержании", + "ImportListsAniListSettingsImportRepeatingHelpText": "Список: Сейчас пересматриваю", + "ImportListsAniListSettingsImportPlanning": "Импорт запланированного", + "IndexerValidationInvalidApiKey": "Неверный ключ API", + "ListSyncTag": "Список синхронизации тегов", + "MetadataSettingsSeriesMetadata": "Метаданные сериала", + "MediaManagementSettings": "Настройки управления медиа", + "MinutesFortyFive": "45 минут: {fortyFive}", + "MonitorAllSeasonsDescription": "Автоматически отслеживайте все новые сезоны", + "Mixed": "Смешанный", + "NotificationsCustomScriptSettingsArguments": "Аргументы", + "NotificationsEmailSettingsServer": "Сервер", + "NotificationsGotifySettingsAppToken": "Токен приложения", + "NotificationsJoinSettingsNotificationPriority": "Приоритет уведомления", + "RecentChanges": "Последние изменения", + "RegularExpression": "Регулярное выражение", + "SeriesLoadError": "Не удалось загрузить сериал", + "ShowBanners": "Показывать баннеры", + "Settings": "Настройки", + "StandardEpisodeFormat": "Стандартный формат эпизода", + "StartImport": "Начать импорт", + "StartupDirectory": "Каталог автозагрузки", + "System": "Система", + "TorrentBlackholeTorrentFolder": "Папка торрента", + "Unavailable": "Недоступно", + "Unlimited": "Неограниченно", + "IconForFinalesHelpText": "Показывать значок финала сериала/сезона на основе доступной информации об эпизоде", + "InfoUrl": "URL-адрес информации", + "InvalidFormat": "Неправильный формат", + "MarkAsFailed": "Пометить как неудачный", + "MediaManagement": "Управление медиа", + "ProcessingFolders": "Обработка папок", + "SupportedDownloadClientsMoreInfo": "Для получения дополнительной информации о каждом из клиентов загрузки нажмите на кнопки с дополнительной информацией.", + "NotificationsPushcutSettingsNotificationName": "Название уведомления", + "LastWriteTime": "Последнее время записи", + "LastUsed": "Использовано последний раз", + "IndexerSettingsSeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто: используется значение по умолчанию клиента загрузки", + "KeyboardShortcutsOpenModal": "Открыть это модальное окно", + "KeyboardShortcutsConfirmModal": "Окно подтверждения", + "KeyboardShortcutsCloseModal": "Закрыть текущее окно", + "InteractiveImportNoLanguage": "Язык должен быть выбран для каждого выбранного файла", + "InteractiveImport": "Интерактивный импорт", + "InstallMajorVersionUpdateMessageLink": "Пожалуйста, проверьте [{domain}]({url}) для получения дополнительной информации.", + "InstallLatest": "Установить последнюю версию", + "IndexersSettingsSummary": "Индексаторы и параметры индексатора", + "IndexerValidationNoResultsInConfiguredCategories": "Запрос выполнен успешно, но индексатор не вернул результатов в настроенных категориях. Это может быть проблема с индексатором или настройками категории индексатора.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Если торрент заблокирован хешем, он может не быть должным образом отклонен во время RSS/поиска для некоторых индексаторов. Включение этого параметра позволит отклонить его после захвата торрента, но до его отправки клиенту.", + "ImportListsAniListSettingsUsernameHelpText": "Имя пользователя для списка, из которого нужно импортировать", + "ImportListsAniListSettingsImportCompleted": "Импорт завершен", + "ImportListSettings": "Импортировать настройки списка", + "ImportListSearchForMissingEpisodesHelpText": "После добавления сериала в {appName} автоматически выполняется поиск недостающих эпизодов", + "ImportListExclusionsLoadError": "Невозможно загрузить исключения из списка импорта", + "ImportListExclusions": "Исключения из списка импорта", + "Images": "Изображения", + "IconForCutoffUnmetHelpText": "Показывать значок для файлов, если ограничение не достигнуто", + "ICalShowAsAllDayEventsHelpText": "События будут отображаться в Вашем календаре как события на весь день", + "ICalSeasonPremieresOnlyHelpText": "В ленте будет только первый эпизод сезона", + "ICalLink": "iCal ссылка", + "FailedToLoadSeriesFromApi": "Не удалось загрузить сериалы из API", + "FileNames": "Имена файлов", + "FailedToLoadSonarr": "Не удалось загрузить {appName}", + "FailedToUpdateSettings": "Не удалось обновить настройки", + "FilterGreaterThanOrEqual": "больше или равно", + "Filter": "Фильтр", + "FilterIs": "является", + "FilterIsAfter": "после", + "FilterLessThan": "меньше, чем", + "FilterLessThanOrEqual": "меньше или равно", + "FilterNotInLast": "не в последнем", + "FirstDayOfWeek": "Первый день недели", + "Forecast": "Прогноз", + "FormatAgeDays": "дни", + "FormatAgeHour": "час", + "FormatAgeHours": "часы", + "FormatAgeMinute": "минута", + "FormatRuntimeMinutes": "{minutes}мин", + "FullColorEvents": "Полноцветные события", + "FormatRuntimeHours": "{hours}ч", + "GeneralSettingsSummary": "Порт, SSL, имя пользователя/пароль, прокси, аналитика и обновления", + "Genres": "Жанры", + "Global": "Глобальный", + "GrabId": "Захватить ID", + "HideEpisodes": "Скрыть эпизоды", + "HasUnmonitoredSeason": "Имеет неотслеживаемый сезон", + "History": "История", + "HourShorthand": "ч", + "HistoryLoadError": "Не удалось загрузить историю", + "ICalTagsSeriesHelpText": "Фид будет содержать только сериалы, имеющие хотя бы один соответствующий тег", + "IgnoreDownloads": "Игнорировать загрузки", + "IgnoredAddresses": "Игнорируемые адреса", + "Ignored": "Проигнорировано", + "ImportCountSeries": "Импортировать сериалы- {selectedCount}", + "ImportLists": "Импорт списков", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Все списки требуют ручного взаимодействия из-за возможной частичной выборки", + "ImportListSearchForMissingEpisodes": "Поиск недостающих эпизодов", + "ImportListsAniListSettingsImportCompletedHelpText": "Список: Просмотр завершен", + "ImportListsAniListSettingsImportDropped": "Импорт прекращен", + "ImportListsAniListSettingsImportFinishedHelpText": "Медиа: Все эпизоды вышли в эфир", + "ImportListsAniListSettingsImportHiatus": "Импортировать на перерыве", + "ImportListsAniListSettingsImportReleasing": "Импорт выпущенного", + "ImportListsAniListSettingsImportWatching": "Импортировать просматриваемое", + "ImportListsCustomListSettingsName": "Пользовательский список", + "ImportListsSonarrSettingsRootFoldersHelpText": "Корневые папки исходного экземпляра для импорта", + "ImportListsSonarrSettingsTagsHelpText": "Теги из исходного экземпляра для импорта из", + "ImportListsTraktSettingsAdditionalParameters": "Дополнительные параметры", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Дополнительные параметры Trakt API", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} не удалось определить, к какому сериалу и эпизоду относится этот релиз. {appName}, возможно, не сможет автоматически импортировать этот релиз. Вы хотите захватить «{title}»?", + "FullColorEventsHelpText": "Изменен стиль, чтобы раскрасить все событие цветом статуса, а не только левый край. Не относится к повестке дня", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Аутентификация с помощью Trakt", + "ImportListsTraktSettingsGenres": "Жанры", + "ImportListsTraktSettingsGenresHelpText": "Фильтр сериалов по Trakt Genre Slug (через запятую) Только для популярных списков", + "FilterIsNot": "не является", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Самые просматриваемые шоу за год", + "ImportListsTraktSettingsUserListTypeWatched": "Список просмотренного пользователем", + "IndexerDownloadClientHelpText": "Укажите, какой клиент загрузки используется для получения данных из этого индексатора", + "FilterIsBefore": "до", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "В тренде", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Если тип списка просматривается, выберите тип сериала, который вы хотите импортировать", + "IndexerHDBitsSettingsMediums": "Средний", + "FilterDoesNotStartWith": "не начинается с", + "ImportListsTraktSettingsUsernameHelpText": "Имя пользователя для списка, из которого нужно импортировать", + "ImportListsValidationInvalidApiKey": "Ключ API недействителен", + "ImportListsValidationTestFailed": "Тест прерван из-за ошибки: {exceptionMessage}", + "FilterDoesNotContain": "не содержит", + "LibraryImportTips": "Несколько советов, чтобы импорт прошел без проблем:", + "ImportListsTraktSettingsWatchedListTypeAll": "Все", + "FailedToLoadSystemStatusFromApi": "Не удалось загрузить статус системы из API", + "FailedToFetchUpdates": "Не удалось получить обновления", + "LibraryImportTipsDontUseDownloadsFolder": "Не используйте для импорта загрузки из вашего клиента загрузки, это предназначено только для существующих организованных библиотек, а не для неотсортированных файлов.", + "IncludeCustomFormatWhenRenaming": "Включить пользовательский формат при переименовании", + "IndexerHDBitsSettingsCategories": "Категории", + "ListSyncTagHelpText": "Этот тег будет добавлен, когда сериал исчезнет или больше не будет в вашем списке(ах)", + "CustomColonReplacement": "Пользовательская замена двоеточия", + "Importing": "Импортирование", + "ImportListsCustomListSettingsUrlHelpText": "URL-адрес списка сериалов", + "IndexerHDBitsSettingsCodecsHelpText": "Если не указано, используются все параметры.", + "IndexerHDBitsSettingsCategoriesHelpText": "Если не указано, используются все параметры.", + "IndexerPriorityHelpText": "Приоритет индексатора от 1 (самый высокий) до 50 (самый низкий). По умолчанию: 25. Используется, если при захвате существуют одинаковые релизы. {appName} по-прежнему будет использовать все включенные индексаторы для синхронизации RSS и поиска", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Синхронизировать отслеживаемый сезон из экземпляра {appName}. Если включено, «Отслеживание» будет игнорироваться", + "ImportListsSonarrValidationInvalidUrl": "URL-адрес {appName} недействителен. Вам не хватает базового URL-адреса?", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Рекомендуемые шоу за неделю", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Обратите внимание: если вы измените категорию, вам придется добавить обязательные/ограниченные правила для подгрупп, чтобы избежать релизов на иностранных языках.", + "IndexerSettingsAdditionalParameters": "Дополнительные параметры", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Также ищите аниме, используя стандартную нумерацию", + "IndexerSettingsAllowZeroSizeHelpText": "Включение этого параметра позволит вам использовать каналы, в которых не указан размер релиза, но будьте осторожны: проверки размера не будут выполняться.", + "IndexerSettingsAnimeStandardFormatSearch": "Поиск аниме в стандартном формате", + "Presets": "Пресеты", + "IndexerSettingsCookie": "Cookie", + "PreviousAiring": "Предыдущий выход в эфир", + "IndexerSettingsMinimumSeeders": "Минимум сидеров (раздающих)", + "NotificationsJoinSettingsDeviceNamesHelpText": "Разделенный запятыми список полных или частичных имен устройств, на которые вы хотите отправлять уведомления. Если параметр не установлен, все устройства будут получать уведомления.", + "IndexerSettingsMultiLanguageReleaseHelpText": "Какие языки обычно используются в релизах этого индексатора?", + "IndexerSettingsPasskey": "Пасскей", + "Period": "Период", + "Permissions": "Разрешения", + "HardlinkCopyFiles": "Жесткая ссылка/Копирование файлов", + "Here": "здесь", + "Health": "Здоровье", + "Hostname": "Имя хоста", + "Host": "Хост", + "IndexerSettingsRssUrlHelpText": "Введите URL-адрес RSS-канала, совместимого с {indexer}", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Время, когда торрент сезонного пакета должен быть на раздаче перед остановкой, при пустом значении используется значение по умолчанию клиента загрузки", + "IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пустой использует значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов", + "IndexerHDBitsSettingsMediumsHelpText": "Если не указано, используются все параметры.", + "IndexerSettingsApiUrlHelpText": "Не меняйте это, если вы не знаете, что делаете. Поскольку ваш ключ API будет отправлен на этот хост.", + "IndexerSettingsCategoriesHelpText": "Раскрывающийся список. Оставьте пустым, чтобы отключить стандартные/ежедневные шоу", + "ProxyResolveIpHealthCheckMessage": "Не удалось определить IP-адрес настроенного прокси-хоста {proxyHostName}", + "IndexerValidationNoRssFeedQueryAvailable": "Запрос RSS-канала недоступен. Это может быть проблема с индексатором или настройками категории индексатора.", + "IndexerValidationCloudFlareCaptchaExpired": "Срок действия токена CloudFlare CAPTCHA истек, обновите его.", + "RecycleBinUnableToWriteHealthCheckMessage": "Невозможно выполнить запись в настроенную папку корзины: {path}. Убедитесь, что этот путь существует и доступен для записи пользователю, запускающего {appName}", + "Links": "Ссылки", + "IndexerValidationUnableToConnectInvalidCredentials": "Невозможно подключиться к индексатору, неверные учетные данные. {exceptionMessage}.", + "IndexerValidationTestAbortedDueToError": "Тест прерван из-за ошибки: {exceptionMessage}", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Индексатор не поддерживает текущий запрос. Проверьте, поддерживаются ли категории и поиск сезонов/эпизодов. Проверьте журнал для получения более подробной информации.", + "RegularExpressionsCanBeTested": "Регулярные выражения можно протестировать [здесь]({url}).", + "InteractiveImportNoSeason": "Сезон необходимо выбрать для каждого выбранного файла", + "InteractiveImportNoSeries": "Сериал необходимо выбирать для каждого выбранного файла", + "LabelIsRequired": "Требуется метка", + "LastExecution": "Последнее выполнение", + "LibraryImportTipsQualityInEpisodeFilename": "Убедитесь, что в именах файлов указано качество. Например. `episode.s02e15.bluray.mkv`", + "ListOptionsLoadError": "Не удалось загрузить параметры списка", + "MissingEpisodes": "Отсутствующие эпизоды", + "ListSyncLevelHelpText": "Сериалы в библиотеке будут обрабатываться на основе вашего выбора, если они выпадают или не отображаются в ваших списках", + "ListQualityProfileHelpText": "Элементы списка профиля качества будут добавлены с помощью", + "ListWillRefreshEveryInterval": "Список будет обновляться каждые {refreshInterval}", + "LocalPath": "Локальный путь", + "LocalStorageIsNotSupported": "Локальное хранилище не поддерживается или отключено. Плагин или приватный просмотр могли отключить его.", + "LogFilesLocation": "Файлы журнала расположены по адресу: {location}", + "LogLevel": "Уровень журнала", + "LogOnly": "Только журнал", + "Logging": "Ведение журнала", + "Logout": "Выйти", + "Lowercase": "Нижний регистр", + "ManageEpisodes": "Управление эпизодами", + "ManageEpisodesSeason": "Управление файлами эпизодов в этом сезоне", + "Manual": "Ручной", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Вы используете Docker; Клиент загрузки {downloadClientName} помещает загрузки в {path}, но это недопустимый путь {osName}. Проверьте правильность указанного пути и настройки клиента загрузки.", + "ManualImport": "Ручной импорт", + "Mapping": "Сопоставление", + "MappedNetworkDrivesWindowsService": "Подключенные сетевые диски недоступны при работе в качестве службы Windows. Дополнительную информацию см. в [FAQ]({url}).", + "MassSearchCancelWarning": "Это нельзя отменить после запуска без перезапуска {appName} или отключения всех индексаторов.", + "RemotePathMappingFileRemovedHealthCheckMessage": "Файл {path} был удален во время обработки.", + "MaximumSingleEpisodeAge": "Максимальный возраст одного эпизода", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Локальный загрузочный клиент {downloadClientName} размещает загрузки в {path}, но это недопустимый путь {osName}. Проверьте настройки загрузочного клиента.", + "MetadataPlexSettingsSeriesPlexMatchFile": "Файл соответствия Plex для сериала", + "Message": "Сообщение", + "RemotePathMappingsLoadError": "Не удалось загрузить сопоставления удаленного пути", + "MetadataSettingsEpisodeMetadataImageThumbs": "Превью изображений метаданных эпизода", + "MetadataSettingsEpisodeMetadata": "Метаданные эпизода", + "RemovedSeriesSingleRemovedHealthCheckMessage": "Сериал {series} был удален из TheTVDB", + "RescanAfterRefreshHelpTextWarning": "{appName} не будет автоматически обнаруживать изменения в файлах, если не установлено значение «Всегда»", + "MinimumFreeSpaceHelpText": "Не импортировать, если останется меньше указанного места на диске", + "MinimumAgeHelpText": "Только для Usenet: минимальный возраст NZB в минутах до их захвата. Используйте это, чтобы дать новым релизам время распространиться среди вашего провайдера Usenet.", + "MinimumCustomFormatScoreHelpText": "Минимальная оценка пользовательского формата, разрешенная для загрузки", + "MissingNoItems": "Нет отсутствующих элементов", + "MinutesSixty": "60 минут: {sixty}", + "MonitorExistingEpisodesDescription": "Отслеживайте эпизоды, у которых есть файлы или которые еще не вышли в эфир", + "Formats": "Форматы", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Один или несколько URL-адресов, разделенных запятыми, указывающих, куда следует отправить уведомление. Оставьте пустым, если используется постоянное хранилище.", + "NotificationsAppriseSettingsTagsHelpText": "При желании уведомите только тех, кто отмечен соответствующим образом.", + "NotificationsAppriseSettingsUsernameHelpText": "Имя пользователя базовой аутентификации HTTP", + "NotificationsDiscordSettingsAvatar": "Аватара", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Измените поля, которые передаются для этого уведомления «при импорте»", + "NotificationsDiscordSettingsOnManualInteractionFields": "О полях ручного взаимодействия", + "GrabRelease": "Захватить релиз", + "FreeSpace": "Свободное место", + "Grab": "Захватить", + "ICalFeed": "Лента iCal", + "HttpHttps": "HTTP(S)", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Измените поля, которые передаются для этого уведомления «при ручном взаимодействии»", + "NotificationsEmailSettingsBccAddressHelpText": "Список получателей скрытой копии электронной почты, разделенный запятыми", + "NotificationsEmailSettingsName": "Почта", + "NotificationsEmailSettingsServerHelpText": "Имя хоста или IP-адрес почтового сервера", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Включить постер сериала в сообщение", + "NotificationsGotifySettingsAppTokenHelpText": "Токен приложения, сгенерированный Gotify", + "NotificationsGotifySettingsServer": "Сервер Gotify", + "NotificationsJoinSettingsDeviceIds": "ID устройств", + "NotificationsGotifySettingsPriorityHelpText": "Приоритет уведомления", + "NotificationsJoinSettingsDeviceIdsHelpText": "Устарело, вместо этого используйте имена устройств. Список идентификаторов устройств, разделенных запятыми, на которые вы хотите отправлять уведомления. Если параметр не установлен, все устройства будут получать уведомления.", + "NotificationsJoinSettingsDeviceNames": "Названия устройств", + "NotificationsJoinValidationInvalidDeviceId": "ID устройств недействительны.", + "NotificationsKodiSettingAlwaysUpdate": "Всегда обновлять", + "NotificationsKodiSettingAlwaysUpdateHelpText": "Обновлять библиотеку даже во время воспроизведения видео?", + "NotificationsKodiSettingsDisplayTimeHelpText": "Как долго будет отображаться уведомление (в секундах)", + "NotificationsKodiSettingsCleanLibraryHelpText": "Очистить библиотеку после обновления", + "NotificationsMailgunSettingsApiKeyHelpText": "Ключ API, сгенерированный из MailGun", + "NotificationsMailgunSettingsSenderDomain": "Домен отправителя", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Включите использование конечной точки EU MailGun", + "NotificationsMailgunSettingsUseEuEndpoint": "Использовать конечную точку ЕС", + "NotificationsNtfySettingsAccessToken": "Токен доступа", + "NotificationsNtfySettingsAccessTokenHelpText": "Дополнительная авторизация на основе токена. Имеет приоритет над именем пользователя/паролем", + "NotificationsNtfySettingsServerUrlHelpText": "Оставьте поле пустым, чтобы использовать общедоступный сервер ({url})", + "NotificationsNtfySettingsTagsEmojisHelpText": "Опциональный список тегов или эмодзи для использования", + "NotificationsNtfySettingsTopicsHelpText": "Список тем для отправки уведомлений", + "NotificationsNtfySettingsUsernameHelpText": "Опциональное имя пользователя", + "NotificationsPlexSettingsServerHelpText": "Выберите сервер из учетной записи plex.tv после аутентификации", + "NotificationsNtfyValidationAuthorizationRequired": "Требуется авторизация", + "NotificationsPushBulletSettingSenderId": "ID отправителя", + "NotificationsPlexValidationNoTvLibraryFound": "Требуется хотя бы одна библиотека c сериалами", + "NotificationsPushcutSettingsApiKeyHelpText": "Ключами API можно управлять в разделе «Учетная запись» приложения Pushcut", + "NotificationsPushBulletSettingsChannelTags": "Теги канала", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Список идентификаторов устройств (оставьте пустым, чтобы отправить на все устройства)", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Список тегов канала для отправки уведомлений", + "NotificationsPushcutSettingsNotificationNameHelpText": "Название уведомления на вкладке «Уведомления» приложения Pushcut", + "PendingChangesDiscardChanges": "Не применять изменения и выйти", + "PortNumber": "Номер порта", + "PostImportCategory": "Категория после импорта", + "PreferUsenet": "Предпочитать Usenet", + "PreferredProtocol": "Предпочтительный протокол", + "PreviewRename": "Предварительный просмотр переименования", + "PreviousAiringDate": "Предыдущий выход в эфир: {date}", + "Proper": "Пропер (Proper)", + "Protocol": "Протокол", + "ProxyBadRequestHealthCheckMessage": "Не удалось проверить прокси. Код состояния: {statusCode}", + "ProtocolHelpText": "Выберите, какой протокол(ы) использовать и какой из них предпочтительнее при выборе между одинаковыми в остальном релизами", + "ProxyFailedToTestHealthCheckMessage": "Не удалось проверить прокси: {url}", + "QualityDefinitionsLoadError": "Невозможно загрузить определение качества", + "QualitiesHelpText": "Качества, стоящие выше в списке, являются более предпочтительными. Качества внутри одной группы равны. Требуются только проверенные качества", + "QualityLimitsSeriesRuntimeHelpText": "Ограничения автоматически корректируются в зависимости от продолжительности сериала и количества эпизодов в файле.", + "QualityProfileInUseSeriesListCollection": "Невозможно удалить профиль качества, прикрепленный к сериалу, списку или коллекции", + "QueueFilterHasNoItems": "В выбранном фильтре очереди нет элементов", + "QueueIsEmpty": "Очередь пуста", + "QueueLoadError": "Не удалось загрузить очередь", + "QuickSearch": "Быстрый поиск", + "Range": "Диапазон", + "Real": "Настоящий", + "Reason": "Причина", + "RecyclingBinCleanupHelpTextWarning": "Файлы в корзине старше выбранного количества дней будут очищены автоматически", + "RejectionCount": "Количество отказов", + "Release": "Релиз", + "ReleaseGroup": "Релиз группа", + "ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).`).", + "ReleaseProfileIndexerHelpText": "Укажите, к какому индексатору применяется профиль", + "ReleaseProfileIndexerHelpTextWarning": "Установка определенного индексатора в профиле релиза приведет к тому, что этот профиль будет применяться только к релизам из этого индексатора.", + "ReleaseProfiles": "Профили релизов", + "ReleaseSceneIndicatorUnknownMessage": "Нумерация этого эпизода различается, и релиз не соответствует ни одному из известных сопоставлений.", + "ReleaseProfilesLoadError": "Невозможно загрузить профили релиза", + "ReleaseRejected": "Релиз отклонен", + "ReleaseSceneIndicatorSourceMessage": "Релизы {message} существуют с неоднозначной нумерацией, из-за которой невозможно надежно идентифицировать эпизод.", + "ReleaseSceneIndicatorUnknownSeries": "Неизвестный эпизод или сериал.", + "ReleaseTitle": "Название релиза", + "ReleaseType": "Тип релиза", + "Reload": "Перезагрузить", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "Приложение {appName} видит, но не имеет доступа к загруженному эпизоду {path}. Вероятно ошибка в правах доступа.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Загрузочный клиент {downloadClientName} размещает загрузки в {path}, но {appName} не видит этот каталог. Вероятно, Вам необходимо настроить права доступа к данной директории.", + "RemotePathMappingLocalPathHelpText": "Путь, который {appName} должен использовать для локального доступа к удаленному пути", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Удаленный загрузочный клиент {downloadClientName} сообщил о файлах в {path}, но этот каталог, похоже, не существует. Вероятно, отсутствует сопоставление удаленных путей.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Удаленный загрузочный клиент {downloadClientName} размещает загрузки в {path}, но это недопустимый путь {osName}. Проверьте соответствие удаленных путей, а также настройки загрузочного клиента.", + "Remove": "Удалить", + "RemotePathMappingsInfo": "Сопоставление удаленного пути требуются очень редко. Если {appName} и Ваш загрузочный клиент находятся в одной системе, лучше сопоставить ваши пути. Для получения дополнительной информации см. [вики]({wikiLink})", + "RemoveCompletedDownloadsHelpText": "Удалить импортированные загрузки из истории загрузочного клиента", + "RemoveFromDownloadClientHint": "Удаляет загрузку и файлы из загрузочного клиента", + "RemoveFailedDownloadsHelpText": "Удалить неудачные загрузки из истории загрузочного клиента", + "RemoveFromQueue": "Удалить из очереди", + "RemoveQueueItemRemovalMethod": "Метод удаления", + "RemoveSelectedItem": "Удалить выбранный элемент", + "RemoveSelectedItems": "Удалить выбранные элементы", + "RemoveQueueItemRemovalMethodHelpTextWarning": "«Удаление из загрузочного клиента» удалит загрузку и файлы из загрузочного клиента.", + "RemovedFromTaskQueue": "Удалено из очереди задач", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Сериал {series} удален из TheTVDB", + "RemovingTag": "Удаление тега", + "RenameEpisodes": "Переименовать эпизоды", + "Reorder": "Изменение порядка", + "RenameFiles": "Переименовать файлы", + "Repeat": "Повторить", + "Replace": "Заменить", + "ReplaceIllegalCharacters": "Заменить недопустимые символы", + "ReplaceIllegalCharactersHelpText": "Заменить недопустимые символы. Если флажок снят, {appName} удалит их", + "Result": "Результат", + "RestrictionsLoadError": "Невозможно загрузить ограничения", + "ResetDefinitions": "Сбросить определения", + "RestartRequiredToApplyChanges": "Для применения изменений {appName} требуется перезагрузка. Перезагрузить сейчас?", + "RestartRequiredHelpTextWarning": "Для применения изменений, требуется перезапуск", + "RootFolder": "Корневой каталог", + "RetryingDownloadOn": "Повторная попытка загрузки {date} в {time}", + "RetentionHelpText": "Только Usenet: установите нулевое значение для неограниченного хранения", + "Rss": "RSS", + "RootFolders": "Корневые папки", + "RootFolderMultipleMissingHealthCheckMessage": "Отсутствуют несколько корневых папок: {rootFolderPaths}", + "RssSyncIntervalHelpText": "Интервал в минутах. Установите 0, чтобы отключить (это остановит все автоматические захваты релизов)", + "RssSyncInterval": "Интервал синхронизации RSS", + "RssSync": "Синхронизация RSS", + "RssSyncIntervalHelpTextWarning": "Это будет применяться ко всем индексаторам, пожалуйста, следуйте установленным ими правилам", + "Save": "Сохранить", + "SeasonFinale": "Финал сезона", + "SearchMonitored": "Искать отслеживаемое", + "SearchForMonitoredEpisodes": "Поиск отслеживаемых эпизодов", + "SearchForCutoffUnmetEpisodes": "Искать все эпизоды не достигшие указанного качества", + "SearchFailedError": "Не удалось выполнить поиск, повторите попытку позже.", + "SeasonFolderFormat": "Формат папки сезона", + "SeasonFolder": "Папка сезона", + "SeasonInformation": "Информация о сезоне", + "SeasonPassTruncated": "Показаны только последние 25 сезонов. Чтобы просмотреть все сезоны, перейдите к подробной информации", + "SelectFolder": "Выбрать папку", + "SeriesDetailsNoEpisodeFiles": "Нет файлов эпизода", + "SeriesDetailsGoTo": "Перейти к {title}", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Информация о сериалах и эпизодах предоставлена TheTVDB.com. [Пожалуйста, рассмотрите возможность их поддержки]({url}) .", + "SeriesFolderImportedTooltip": "Эпизод импортирован из папки сериала", + "SeriesFolderFormat": "Формат папки сериала", + "SeriesDetailsOneEpisodeFile": "1 файл эпизода", + "SeriesIsUnmonitored": "Сериал не отслеживается", + "SeriesIndexFooterEnded": "Завершено (Все эпизоды скачаны)", + "SeriesID": "Идентификатор сериала", + "SeriesType": "Тип сериала", + "SeriesMonitoring": "Мониторинг сериала", + "SeriesTitleToExcludeHelpText": "Название сериала, который нужно исключить", + "SeriesTitle": "Название сериала", + "SetIndexerFlags": "Установить флаги индексатора", + "ShowBannersHelpText": "Показывать баннеры вместо заголовков", + "ShowPath": "Показать путь", + "ShowRelativeDatesHelpText": "Показывать относительные (сегодня / вчера / и т. д.) или абсолютные даты", + "ShowQualityProfileHelpText": "Показать профиль качества под постером", + "ShowUnknownSeriesItemsHelpText": "Показывать элементы без сериалов в очереди, это могут быть удаленные сериалы, фильмы или что-либо еще в категории {appName}", + "ShowUnknownSeriesItems": "Показать элементы неизвестного сериала", + "ShowTitle": "Показать название", + "SkipFreeSpaceCheck": "Пропустить проверку свободного места", + "ShowSizeOnDisk": "Показать размер на диске", + "ShowSearchHelpText": "Показать копку поиска по наведению", + "SkipFreeSpaceCheckWhenImportingHelpText": "Используйте, когда {appName} не может обнаружить свободное место в вашей корневой папке во время импорта файлов", + "SpecialsFolderFormat": "Формат папки спец. эпизодов", + "SmartReplace": "Умная замена", + "SkipRedownloadHelpText": "Предотвращает попытку {appName} загрузить альтернативную версию для этого элемента", + "SonarrTags": "Теги {appName}-а", + "SupportedListsMoreInfo": "Для дополнительной информации по спискам импорта нажмите эту кнопку.", + "Style": "Стиль", + "SupportedIndexers": "{appName} поддерживает любой индексатор, использующий стандарт Newznab, а также другие индексаторы, перечисленные ниже.", + "Table": "Таблица", + "TableColumnsHelpText": "Выберите, какие столбцы отображаются и в каком порядке", + "TestAllClients": "Тестировать всех клиентов", + "TablePageSizeMaximum": "Размер страницы не должен превышать {maximumValue}", + "TaskUserAgentTooltip": "User-Agent, представленный приложением, который вызывает API", + "TorrentDelayTime": "Задержка торрента: {torrentDelay}", + "ThemeHelpText": "Измените тему пользовательского интерфейса приложения, тема «Авто» будет использовать тему вашей ОС для установки светлого или темного режима. Вдохновлено Theme.Park", + "TorrentDelayHelpText": "Задержка в минутах перед скачиванием торрента", + "TorrentBlackhole": "Blackhole торрент", + "Total": "Общий", + "TotalRecords": "Всего записей: {totalRecords}", + "TvdbId": "Идентификатор TVDB", + "Umask755Description": "{octal} - Владелец (запись), остальные (чтение)", + "Underscore": "Нижнее подчеркивание", + "UnableToLoadBackups": "Невозможно загрузить резервные копии", + "UnmappedFilesOnly": "Только несопоставленные файлы", + "Unknown": "Неизвестный", + "UnknownEventTooltip": "Неизвестное событие", + "UnmonitorSelected": "Неотслеживаемые выбраны", + "UnmonitorDeletedEpisodes": "Не отслеживать удаленные эпизоды", + "Unmonitored": "Не отслеживается", + "SaveSettings": "Сохранить настройки", + "IndexerPriority": "Приоритет индексатора", + "Files": "Файлы", + "ImportErrors": "Ошибки при импорте", + "MatchedToSeries": "Соответствует сериалу", + "Renamed": "Переименовано", + "FileBrowserPlaceholderText": "Начните вводить или выберите путь ниже", + "IndexerStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок", + "Other": "Другой", + "OverviewOptions": "Опции обзора", + "RefreshAndScanTooltip": "Обновить информацию и просканировать диск", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "Приложение {appName} видит, но не имеет доступа к каталогу загрузки {downloadPath}. Вероятно, ошибка связана с правами доступа.", + "ScriptPath": "Путь к скрипту", + "SeasonDetails": "Детали сезона", + "ShowNetwork": "Показать сеть", + "RemotePath": "Удаленный путь", + "Twitter": "Twitter (X)", + "IndexerValidationCloudFlareCaptchaRequired": "Сайт защищен CloudFlare CAPTCHA. Требуется действительный токен CAPTCHA.", + "IndexerValidationFeedNotSupported": "Фид индексатора не поддерживается: {exceptionMessage}", + "UiLanguageHelpText": "Язык, который {appName} будет использовать для пользовательского интерфейса", + "Today": "Сегодня", + "UnknownDownloadState": "Неизвестное состояние загрузки: {state}", + "Ungroup": "Разгруппировать", + "UnmappedFolders": "Несопоставленные папки", + "CountVotes": "{votes} голосов", + "RemoveFromDownloadClient": "Удалить из загрузочного клиента", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Вы используете Docker. Загрузочный клиент {downloadClientName} сообщает что путь {path} некорректен {osName}. Проверьте правильность настройки загрузочного клиента.", + "Required": "Необходимо", + "RemoveSelected": "Удалить выбранное", + "RemoveSelectedBlocklistMessageText": "Вы уверены, что хотите удалить выбранные элементы из черного списка?", + "SelectLanguage": "Выбрать язык", + "SearchForMissing": "Поиск отсутствующих", + "SetPermissionsLinuxHelpTextWarning": "Если вы не уверены, что делают эти настройки, не меняйте их.", + "SeriesIsMonitored": "Сериал отслеживается", + "SetPermissions": "Установить разрешения", + "ShowRelativeDates": "Показать относительные даты", + "ShowPreviousAiring": "Показать предыдущий эфир", + "ShowMonitoredHelpText": "Показывать статус отслеживания под постером", + "SingleEpisode": "Одиночный эпизод", + "SizeOnDisk": "Размер на диске", + "ShowSearch": "Показать поиск", + "SupportedListsSeries": "{appName} поддерживает несколько списков для импорта сериалов в базу данных.", + "StopSelecting": "Прекратить выбор", + "Status": "Статус", + "SupportedCustomConditions": "{appName} поддерживает настраиваемые условия в соответствии со свойствами релиза, указанными ниже.", + "ToggleMonitoredSeriesUnmonitored ": "Невозможно переключить отслеживаемое состояние, если сериал не отслеживается", + "Failed": "Неудачно", + "FilterSeriesPlaceholder": "Фильтр сериалов", + "OrganizeSelectedSeriesModalAlert": "Совет: Чтобы просмотреть переименование, выберите «Отмена», затем выберите любой заголовок эпизода и используйте этот значок:", + "Original": "Оригинал", + "Level": "Уровень", + "FilterEpisodesPlaceholder": "Фильтровать эпизоды по названию или номеру", + "FilterGreaterThan": "больше чем", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "IRCLinkText": "#sonarr на Либере", + "ICalFeedHelpText": "Скопируйте этот URL-адрес своим клиентам или нажмите, чтобы подписаться, если ваш браузер поддерживает Webcal", + "ImportListsLoadError": "Невозможно загрузить списки импорта", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Медиа: Выход в эфир еще не начался", + "ImportListsAniListSettingsImportNotYetReleased": "Импорт еще не выпущенного", + "ImportListsSettingsAccessToken": "Токен доступа", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Рекомендуемые шоу за год", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Самые просматриваемые шоу всех времен", + "ImportListsTraktSettingsPopularName": "Список популярного от Trakt", + "ImportListsPlexSettingsWatchlistRSSName": "Список наблюдения Plex RSS", + "IndexerSettingsAnimeCategoriesHelpText": "Выпадающий список, оставьте пустым, чтобы отключить аниме", + "IndexerSettingsMultiLanguageRelease": "Несколько языков", + "IndexerSettingsSeasonPackSeedTime": "Время сидирования сезон-пака", + "IndexerValidationUnableToConnectResolutionFailure": "Невозможно подключиться к индексатору. Проверьте подключение к серверу индексатора и DNS. {exceptionMessage}.", + "IndexerValidationUnableToConnectHttpError": "Невозможно подключиться к индексатору. Проверьте настройки DNS и убедитесь, что IPv6 работает или отключен. {exceptionMessage}.", + "IndexerTagSeriesHelpText": "Используйте этот индексатор только для сериалов, имеющих хотя бы один соответствующий тег. Оставьте поле пустым, чтобы использовать его для всех.", + "InteractiveSearchResultsSeriesFailedErrorMessage": "Поиск не удался, поскольку это {message}. Попробуйте обновить информацию о сериале и убедитесь, что необходимая информация присутствует, прежде чем продолжить поиск.", + "LatestSeason": "Последний сезон", + "KeyboardShortcuts": "Горячие клавиши", + "LiberaWebchat": "Libera Webchat", + "LibraryImportSeriesHeader": "Импортируйте Ваши сериалы", + "Location": "Расположение", + "ManualImportItemsLoadError": "Невозможно загрузить элементы импорта вручную", + "MarkAsFailedConfirmation": "Вы уверены, что хотите пометить '{sourceTitle}' как неудачный?", + "MetadataSettings": "Настройки метаданных", + "NotificationsAppriseSettingsNotificationType": "Тип информирования об уведомлении", + "NotificationsEmailSettingsFromAddress": "С адреса", + "NotificationsEmailSettingsUseEncryptionHelpText": "Предпочитать использовать шифрование, если оно настроено на сервере, всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт) или никогда не использовать шифрование", + "NotificationsCustomScriptSettingsProviderMessage": "При тестировании будет выполняться сценарий с типом события, установленным на {eventTypeTest}. Убедитесь, что ваш сценарий обрабатывает это правильно", + "NotificationsJoinSettingsApiKeyHelpText": "Ключ API из настроек вашей учетной записи присоединения (нажмите кнопку «Присоединиться к API»).", + "NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо", + "NotificationsNtfySettingsTagsEmojis": "Ntfy-теги и эмодзи", + "NotificationsKodiSettingsDisplayTime": "Отображать время", + "NotificationsNtfySettingsClickUrlHelpText": "Опциональная ссылка, когда пользователь нажимает уведомление", + "PendingDownloadClientUnavailable": "Ожидание – Клиент для загрузки недоступен", + "Port": "Порт", + "ProxyBypassFilterHelpText": "Используйте ',' в качестве разделителя и '*.' как подстановочный знак для субдоменов", + "RefreshSeries": "Обновить сериал", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} не удалось импортировать эпизоды. Проверьте логи для детальной информации.", + "SelectReleaseType": "Выберите тип релиза", + "Security": "Безопасность", + "SeriesFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Series Title:30}`), так и с начала (например, `{Series Title:-30}`).", + "SeriesCannotBeFound": "К сожалению, невозможно найти этот сериал.", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Всего: {totalEpisodeCount}, Загружается: {downloadingCount})", + "SetIndexerFlagsModalTitle": "{modalTitle} — установка флагов индексатора", + "Socks5": "Socks5 (Поддержка TOR)", + "SomeResultsAreHiddenByTheAppliedFilter": "Некоторые результаты скрыты примененным фильтром", + "TagIsNotUsedAndCanBeDeleted": "Тег не используется и может быть удален", + "TableOptions": "Опции таблицы", + "TablePageSizeHelpText": "Количество элементов, отображаемых на каждой странице", + "TablePageSizeMinimum": "Размер страницы должен быть не менее {minimumValue}", + "UnmonitorSpecialEpisodes": "Не отслеживать спец. эпизоды", + "UiSettingsLoadError": "Не удалось загрузить настройки пользовательского интерфейса", + "Umask": "Маска режима создания пользовательских файлов (Umask)", + "ImportListsSettingsAuthUser": "Авторизация пользователя", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Аутентификация с помощью MyAnimeList", + "ImportListsMyAnimeListSettingsListStatus": "Статус списка", + "ImportListsSimklSettingsShowType": "Показать тип", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Планирую посмотреть", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Тип списка, из которого вы хотите импортировать, установите для всех списков значение «Все»", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Рекомендуемые шоу за месяц", + "InteractiveSearchSeason": "Интерактивный поиск всех эпизодов этого сезона", + "Label": "Метка", + "MetadataSettingsEpisodeImages": "Изображения эпизодов", + "ListExclusionsLoadError": "Невозможно загрузить список исключений", + "LibraryImportTipsSeriesUseRootFolder": "Укажите в приложении {appName} папку, содержащую все ваши телешоу, а не какое-то конкретное. например. «`{goodFolderExample}`», а не «`{badFolderExample}`». Кроме того, каждый сериал должен находиться в отдельной папке в корневом каталоге библиотеки.", + "MinimumCustomFormatScore": "Минимальная оценка пользовательского формата", + "MinimumLimits": "Минимальные ограничения", + "Missing": "Отсутствующий", + "NotificationsAppriseSettingsStatelessUrls": "Утвердить URL-адреса без сохранения состояния", + "NotificationsAppriseSettingsTags": "Информирующие теги", + "NotificationsEmailSettingsCcAddress": "Адрес(а) CC", + "NotificationsEmailSettingsRecipientAddressHelpText": "Список получателей электронной почты, разделенный запятыми", + "NotificationsEmbySettingsSendNotificationsHelpText": "Заставить MediaBrowser отправлять уведомления настроенным поставщикам", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Загрузочный клиент {downloadClientName} сообщает о наличии файлах по пути {path}, но {appName} не видит этот каталог. Возможно, вам придется настроить права доступа к папке.", + "RemotePathMappings": "Сопоставления удаленного пути", + "SearchIsNotSupportedWithThisIndexer": "Поиск не поддерживается этим индексатором", + "SearchForMonitoredEpisodesSeason": "Поиск отслеживаемых эпизодов в этом сезоне", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Вы уверены, что хотите найти все {totalRecords}, не достигшие указанного качества эпизоды ?", + "SeasonPremiere": "Премьера сезона", + "SeasonPassEpisodesDownloaded": "Скачано эпизодов: {episodeFileCount}/{totalEpisodeCount}", + "SeasonNumberToken": "Сезон {seasonNumber}", + "SeasonNumber": "Номер сезона", + "Seeders": "Сиды", + "SeasonPremieresOnly": "Только премьеры сезона", + "FailedToLoadUiSettingsFromApi": "Не удалось загрузить настройки пользовательского интерфейса из API", + "FailedToLoadTagsFromApi": "Не удалось загрузить теги из API", + "FeatureRequests": "Будущие запросы", + "FileManagement": "Управление файлами", + "HomePage": "Домашняя страница", + "HistoryModalHeaderSeason": "История {season}", + "ImportCustomFormat": "Импортировать пользовательский формат", + "IconForFinales": "Значок для финала", + "ImportListsAniListSettingsImportCancelled": "Импорт отменен", + "ImportListsAniListSettingsAuthenticateWithAniList": "Аутентификация с помощью AniList", + "ImportListsAniListSettingsImportPlanningHelpText": "Список: Планируем посмотреть", + "ImportListsCustomListValidationConnectionError": "Невозможно выполнить запрос по этому URL-адресу. Код статуса: {exceptionStatusCode}", + "ImportListsImdbSettingsListIdHelpText": "ID списка IMDB (пример:ls12345678)", + "ImportListsImdbSettingsListId": "ID списка", + "ImportListsPlexSettingsWatchlistName": "Список наблюдения Plex", + "ImportListsPlexSettingsAuthenticateWithPlex": "Аутентификация с помощью Plex.tv", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Аутентификация с помощью Simkl", + "ImportListsSettingsSummary": "Импортируйте из другого экземпляра {appName} или списков Trakt и управляйте исключениями из списков", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Профили качества из исходного экземпляра для импорта из", + "ImportListsSonarrSettingsFullUrlHelpText": "URL-адрес экземпляра {appName}, включая порт, из которого необходимо импортировать", + "ImportListsSonarrSettingsFullUrl": "Полный URL-адрес", + "ImportListsTraktSettingsListType": "Тип списка", + "ImportListsTraktSettingsListTypeHelpText": "Тип списка, из которого вы хотите импортировать", + "ImportListsTraktSettingsListNameHelpText": "Имя списка для импорта. Список должен быть общедоступным или у вас должен быть доступ к списку", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Ожидаемые шоу", + "ImportListsTraktSettingsListName": "Имя списка", + "ImportListsTraktSettingsUserListName": "Пользователь Trakt", + "ImportListsTraktSettingsUserListTypeCollection": "Список коллекций пользователей", + "ImportListsTraktSettingsUserListTypeWatch": "Список наблюдения пользователя", + "ImportListsTraktSettingsYears": "Годы", + "ImportListsTraktSettingsYearsHelpText": "Фильтрация сериалов по годам или диапазону лет", + "ImportListsTraktSettingsWatchedListTypeInProgress": "В процессе", + "Indexer": "Индексатор", + "IndexerSettingsWebsiteUrl": "URL веб-сайта", + "InteractiveImportNoEpisode": "Для каждого выбранного файла необходимо выбрать один или несколько эпизодов", + "IndexersLoadError": "Не удалось загрузить индексаторы", + "MediaInfo": "Медиа данные", + "Metadata": "Метаданные", + "MediaManagementSettingsLoadError": "Не удалось загрузить настройки управления медиа", + "NotificationsDiscordSettingsOnImportFields": "Поля импорта", + "NotificationsDiscordSettingsUsernameHelpText": "Имя пользователя для публикации по умолчанию — Discord webhook по умолчанию", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Обновить библиотеку при импорте, переименовании или удалении?", + "Posters": "Постеры", + "PendingChangesMessage": "У вас есть несохраненные изменения. Вы уверены, что хотите покинуть эту страницу?", + "Path": "Путь", + "RecyclingBinHelpText": "Файлы попадут сюда после удаления, а не будут удалены безвозвратно", + "ReadTheWikiForMoreInformation": "Прочтите Wiki для получения дополнительной информации", + "ReleaseProfileTagSeriesHelpText": "Профили релиза будут применяться к сериалам, имеющим хотя бы один соответствующий тег. Оставьте поле пустым, чтобы применить ко всем", + "ReleaseSceneIndicatorMappedNotRequested": "Сопоставленный эпизод не был запрошен в этом поиске.", + "RemotePathMappingHostHelpText": "Тот же хост, который вы указали для удаленного загрузочного клиента", + "ICalIncludeUnmonitoredEpisodesHelpText": "Включение неотслеживаемых эпизодов в ленту iCal", + "ResetAPIKey": "Сбросить API ключ", + "RootFolderSelectFreeSpace": "{freeSpace} свободно", + "RssIsNotSupportedWithThisIndexer": "RSS не поддерживается этим индексатором", + "Seasons": "Сезоны", + "SearchAll": "Искать все", + "SaveChanges": "Сохранить изменения", + "ShowDateAdded": "Показать дату добавления", + "ShortDateFormat": "Короткий формат даты", + "ShowAdvanced": "Показать расширенные", + "FinaleTooltip": "Финал сериала или сезона", + "Interval": "Интервал", + "IndexerValidationJackettAllNotSupportedHelpText": "Все конечные точки Jackett не поддерживаются, добавьте индексаторы по отдельности", + "Overview": "Обзор", + "OutputPath": "Выходной путь", + "PreferAndUpgrade": "Предпочитать и улучшать", + "PreferProtocol": "Предпочитать {preferredProtocol}", + "ProxyUsernameHelpText": "Вам нужно только ввести имя пользователя и пароль, если они необходимы. В противном случае оставьте их пустыми.", + "RemoveMultipleFromDownloadClientHint": "Удаляет загрузки и файлы из загрузочного клиента", + "FilterEndsWith": "заканчивается", + "ImdbId": "Идентификатор IMDB", + "FilterDoesNotEndWith": "не заканчивается", + "IndexerSettingsAllowZeroSize": "Разрешить нулевой размер", + "IndexerSearchNoInteractiveHealthCheckMessage": "При включенном интерактивном поиске индексаторы недоступны. Приложение {appName} не будет предоставлять результаты интерактивного поиска", + "IndexerSettingsApiPath": "Путь API", + "InvalidUILanguage": "В вашем пользовательском интерфейсе установлен недопустимый язык. Исправьте его и сохраните настройки", + "Language": "Язык", + "KeyboardShortcutsSaveSettings": "Сохранить настройки", + "KeyboardShortcutsFocusSearchBox": "Поле поиска в фокусе", + "MonitorAllSeasons": "Все сезоны", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Аргументы для передачи в скрипт", + "NotificationsDiscordSettingsAuthor": "Автор", + "PackageVersionInfo": "{packageVersion} от {packageAuthor}", + "Peers": "Пиры", + "PreviewRenameSeason": "Предварительный просмотр переименования этого сезона", + "Proxy": "Прокси", + "RenameEpisodesHelpText": "{appName} будет использовать существующее имя файла, если переименование отключено", + "QualityProfiles": "Профили качества", + "SupportedAutoTaggingProperties": "{appName} поддерживает следующие свойства для правил автоматических тегов", + "TablePageSize": "Размер страницы", + "TypeOfList": "{typeOfList} список", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Только чтение", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Вы используете Docker; Клиент загрузки {downloadClientName} помещает загрузки в {path}, но этот каталог не существует внутри контейнера. Просмотрите сопоставления удаленных путей и настройки тома контейнера.", + "Import": "Импортировать", + "ImportExtraFiles": "Импортировать дополнительные файлы", + "ImportExtraFilesEpisodeHelpText": "Импортируйте соответствующие дополнительные файлы (субтитры, nfo файлы и т. д.) после добавления файла эпизода", + "ImportListsAniListSettingsImportWatchingHelpText": "Список: Сейчас смотрят", + "ImportFailed": "Не удалось импортировать: {sourceTitle}", + "ImportListsTraktSettingsUserListUsernameHelpText": "Имя пользователя для импорта списка (оставьте пустым, чтобы использовать пользователя с аутентификацией)", + "ImportSeries": "Импортировать сериалы", + "InteractiveSearchModalHeaderSeason": "Интерактивный поиск – {season}", + "InteractiveSearchModalHeader": "Интерактивный поиск", + "MonitorExistingEpisodes": "Существующие эпизоды", + "NotificationsDiscordSettingsAuthorHelpText": "Заменить стандартного автора, который отображается для этого уведомления. Пусто — имя экземпляра", + "NotificationsCustomScriptValidationFileDoesNotExist": "Файл не существует", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Аутентификация с помощью Plex.tv", + "PrefixedRange": "Префиксный диапазон", + "ProxyPasswordHelpText": "Вам нужно только ввести имя пользователя и пароль, если они необходимы. В противном случае оставьте их пустыми.", + "Quality": "Качество", + "QualityProfilesLoadError": "Невозможно загрузить профили качества", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Удаленный загрузочный клиент {downloadClientName} размещает загрузки в {path}, но этот каталог не существует. Вероятно, отсутствует или неправильно указан удаленный путь.", + "RestartNow": "Перезапустить сейчас", + "ResetTitles": "Сбросить заголовки", + "RestoreBackup": "Восстановить из резервной копии", + "RestartSonarr": "Перезапустить {appName}", + "RestartRequiredWindowsService": "В зависимости от того, какой пользователь запускает службу {appName}, вам может потребоваться один раз перезапустить {appName} от имени администратора, прежде чем служба запустится автоматически.", + "ShowEpisodeInformationHelpText": "Показать название и номер эпизода", + "TvdbIdExcludeHelpText": "Идентификатор TVDB сериала, который нужно исключить", + "Specials": "Спец. эпизоды", + "PosterSize": "Размер постера", + "NotificationsNtfySettingsServerUrl": "URL-адрес сервера", + "SearchForAllMissingEpisodes": "Искать все недостающие эпизоды", + "SeriesIndexFooterMissingMonitored": "Отсутствующие эпизоды (сериал отслеживается)", + "Scheduled": "Запланировано", + "FilterNotInNext": "не в следующем", + "Folders": "Папки", + "FilterStartsWith": "начинается с", + "ImportedTo": "Импортировано в", + "ImportListsTraktSettingsWatchedListSorting": "Сортировка списка просмотра", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Если тип списка — «Просматривается», выберите порядок сортировки списка", + "IndexerSettingsApiUrl": "URL-адрес API", + "Scene": "Сцена", + "SeriesPremiere": "Премьера сериала", + "SelectSeasonModalTitle": "{modalTitle} ‐ выбор сезона", + "SelectSeries": "Выберите сериал", + "SelectSeason": "Выберите сезон", + "Time": "Время", + "SelectReleaseGroup": "Выберите релиз-группу", + "TestAllLists": "Тестировать все листы", + "Test": "Тест", + "TagsSettingsSummary": "Посмотрите все теги и способы их использования. Неиспользуемые теги можно удалить", + "MetadataSettingsSeasonImages": "Изображения сезона", + "MetadataSettingsSeriesImages": "Изображения сериала", + "NotificationsEmailSettingsUseEncryption": "Использовать шифрование", + "Monitoring": "Мониторинг", + "PosterOptions": "Опции постера", + "FilterContains": "содержит", + "SceneNumberNotVerified": "Номер сцены еще не проверен", + "RemoveFromBlocklist": "Удалить из черного списка", + "ImportListsSettingsExpires": "Срок действия истекает", + "FailedToLoadCustomFiltersFromApi": "Не удалось загрузить пользовательские фильтры из API", + "ExtraFileExtensionsHelpTextsExamples": "Например: '.sub, .nfo' или 'sub,nfo'", + "IconForSpecials": "Значок для спец. эпизодов", + "FormatShortTimeSpanHours": "{hours} час(ов)", + "Filters": "Фильтры", + "FileNameTokens": "Токены имени файла", + "FileBrowser": "Файловый браузер", + "FailedToLoadQualityProfilesFromApi": "Не удалось загрузить профили качества из API", + "ImportListsAniListSettingsImportRepeating": "Импортировать повторы", + "ImportListsAniListSettingsImportReleasingHelpText": "Медиа: Сейчас выходят в эфир новые эпизоды", + "ImportListsAniListSettingsImportPaused": "Импорт приостановлен", + "IgnoreDownloadsHint": "Не позволяет приложению {appName} обрабатывать эти загрузки", + "ImportListsCustomListSettingsUrl": "Список URL", + "ImportListsSettingsRefreshToken": "Обновить токен", + "ImportListsSettingsRssUrl": "URL-адрес RSS-канала", + "ImportListsSimklSettingsName": "Список наблюдения пользователя Simkl", + "ImportListsValidationUnableToConnectException": "Невозможно подключиться для импорта списка: {exceptionMessage}. Подробности см. в журнале этой ошибки.", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Если возможно, включите обработку завершенной загрузки (несколько компьютеров не поддерживается)", + "ImportUsingScriptHelpText": "Копирование файлов для импорта с помощью скрипта (например, для перекодирования)", + "IndexerValidationUnableToConnect": "Невозможно подключиться к индексатору: {exceptionMessage}. Подробности см. в журнале этой ошибки", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Отклонять хэши торрентов из черного списка при захвате", + "MatchedToEpisodes": "Соответствует эпизодам", + "Max": "Максимально", + "NotificationsDiscordSettingsOnGrabFields": "При захвате полей", + "NotificationsNtfySettingsClickUrl": "Нажмите URL-адрес", + "OrganizeNamingPattern": "Шаблон именования: `{episodeFormat}`", + "OrganizeSelectedSeriesModalHeader": "Упорядочить выбранные сериалы", + "OrganizeModalHeaderSeason": "Упорядочить и переименовать - {season}", + "OrganizeSelectedSeriesModalConfirmation": "Вы уверены, что хотите упорядочить все файлы в выбранном сериале: {count}?", + "OrganizeRenamingDisabled": "Переименование отключено, переименовывать нечего", + "OrganizeRelativePaths": "Все пути указаны относительно: `{path}`", + "ImportListsSimklSettingsShowTypeHelpText": "Тип шоу, из которого вы хотите импортировать", + "ImportListsSimklSettingsUserListTypeCompleted": "Завершенный", + "ImportListsSimklSettingsUserListTypeWatching": "Смотрю", + "ImportListsSonarrSettingsApiKeyHelpText": "Ключ API экземпляра {appName}, из которого нужно импортировать", + "ImportListsTraktSettingsLimitHelpText": "Ограничение количества сериалов для получения", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Рекомендуемые шоу всех времен", + "ImportListsTraktSettingsRating": "Рейтинг", + "ImportListsTraktSettingsRatingHelpText": "Фильтровать сериалы по диапазону рейтингов (0–100)", + "NotificationsDiscordSettingsAvatarHelpText": "Измените аватар, который используется для сообщений из этой интеграции", + "IndexerSettingsSeedRatio": "Рейтинг", + "ImportListsTraktSettingsWatchedListFilter": "Фильтр списка просмотра", + "IndexerHDBitsSettingsCodecs": "Кодеки", + "IndexerStatusUnavailableHealthCheckMessage": "Индексаторы недоступны из-за ошибок: {indexerNames}", + "IndexerSettingsSeedTime": "Время сидирования", + "Local": "Локальный", + "Score": "Счет", + "Forums": "Форумы", + "IgnoreDownloadHint": "Не позволяет приложению {appName} продолжать обработку этой загрузки", + "FormatAgeDay": "день", + "NotificationsKodiSettingsGuiNotification": "Уведомление(GUI)", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Обновить библиотеку при импорте и переименовании?", + "Large": "Большой", + "MaximumLimits": "Максимальные ограничения", + "OriginalLanguage": "Язык оригинала", + "SeriesDetailsRuntime": "{runtime} минут", + "Tags": "Теги", + "ImportListsTraktSettingsWatchedListTypeCompleted": "Просмотрено на 100%", + "ImportScriptPathHelpText": "Путь к скрипту, который будет использоваться для импорта", + "IncludeCustomFormatWhenRenamingHelpText": "Включить в {Custom Formats} формат переименования", + "IndexerFlags": "Флаги индексатора", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "Полный URL-адрес RSS-канала, созданный IPTorrents с использованием только выбранных вами категорий (HD, SD, x264 и т. д.)", + "ReleaseSceneIndicatorAssumingScene": "Предполагается нумерация сцен.", + "SeasonCount": "Количество сезонов", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Синхронизация мониторинга сезона", + "KeepAndUnmonitorSeries": "Сохранить сериал и не отслеживать", + "KeepAndTagSeries": "Сохранить и пометить тегом сериал", + "IndexerValidationUnableToConnectServerUnavailable": "Невозможно подключиться к индексатору, сервер индексатора недоступен. Попробуйте позже. {exceptionMessage}.", + "Min": "Минимум", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "«Удаление из загрузочного клиента» удалит загрузки и файлы из загрузочного клиента.", + "PackageVersion": "Версия пакета", + "ImportListsAniListSettingsImportCancelledHelpText": "Медиа: Сериал отменен", + "ImportExistingSeries": "Импортировать существующие сериалы", + "IgnoreDownload": "Игнорировать загрузку", + "IconForCutoffUnmet": "Значок для невыполненного порога", + "FormatTimeSpanDays": "{days}d {time}", + "ImportListsCustomListValidationAuthenticationFailure": "Ошибка аутентификации", + "ImportListsSimklSettingsListType": "Тип списка", + "ImportListsSimklSettingsListTypeHelpText": "Тип списка, из которого вы хотите импортировать", + "ImportListsTraktSettingsLimit": "Лимит", + "InstanceName": "Имя экземпляра", + "IndexerValidationUnableToConnectTimeout": "Невозможно подключиться к индексатору, возможно, из-за тайм-аута. Попробуйте еще раз или проверьте настройки сети. {exceptionMessage}.", + "IndexerValidationJackettAllNotSupported": "Все конечные точки Jackett не поддерживаются, добавьте индексаторы по отдельности", + "IndexerSettingsMinimumSeedersHelpText": "Необходимое минимальное количество сидеров (раздающих).", + "IndexerSettingsCookieHelpText": "Если вашему сайту требуется файл cookie для входа в систему для доступа к RSS, вам придется получить его через браузер.", + "IndexerSettingsApiPathHelpText": "Путь к API, обычно {url}", + "ImportListsTraktSettingsPopularListTypePopularShows": "Популярные шоу", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Самые просматриваемые шоу за месяц", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Самые просматриваемые шоу за неделю", + "UnableToUpdateSonarrDirectly": "Невозможно обновить {appName} напрямую,", + "UnableToImportAutomatically": "Невозможно импортировать автоматически", + "MetadataProvidedBy": "Метаданные предоставлены {provider}", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Включите URL-адрес сериала TheTVDB в tvshow.nfo (можно комбинировать с «Метаданными сериала»)", + "MidseasonFinale": "Финал середины сезона", + "MinimumAge": "Минимальный возраст", + "StandardEpisodeTypeFormat": "Номера сезонов и серий ({format})", + "StartProcessing": "Начать обработку", + "TableColumns": "Столбцы", + "TodayAt": "Сегодня в{time}", + "Titles": "Названия", + "Title": "Название", + "TimeLeft": "Оставшееся время", + "TimeFormat": "Формат времени", + "TestAllIndexers": "Тестировать все индексаторы", + "Tba": "Будет объявлено позже", + "LastDuration": "Последняя длительность", + "PendingChangesStayReview": "Оставайтесь и просмотрите изменения", + "QualitySettingsSummary": "Размеры и название качества", + "Queue": "Очередь", + "RatingVotes": "Рейтинг голосов", + "NotificationsPlexSettingsServer": "Сервер", + "Search": "Поиск", + "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит пользовательский интерфейс во время процесса восстановления.", + "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, щелкнув вики-ссылку (значок книги) в конце строки или проверив свои [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", + "MaintenanceRelease": "Техническая версия: исправлены ошибки и другие улучшения. Дополнительную информацию см. в истории коммитов Github", + "Space": "Пробел", + "SslCertPasswordHelpText": "Пароль для файла pfx", + "SpecialEpisode": "Спец. эпизод", + "ShowTagsHelpText": "Показать теги под постером", + "StandardEpisodeTypeDescription": "Эпизоды выпущены с шаблоном SxxEyy", + "Standard": "Стандартный", + "SslPort": "SSL порт", + "SslCertPathHelpText": "Путь к pfx файлу", + "ShowTags": "Показать теги", + "TorrentBlackholeSaveMagnetFiles": "Сохранить магнет файлы", + "TomorrowAt": "Завтра в {time}", + "Tomorrow": "Завтра", + "Special": "Спец. эпизод", + "SourcePath": "Исходный путь", + "InstanceNameHelpText": "Имя экземпляра на вкладке и имя приложения системного журнала", + "InstallMajorVersionUpdateMessage": "Это обновление установит новую основную версию и может быть несовместимо с вашей системой. Вы уверены, что хотите установить это обновление?", + "InstallMajorVersionUpdate": "Установить обновление", + "Install": "Установить", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Измените поля, которые передаются для этого уведомления «при получении»", + "NotificationsEmailSettingsBccAddress": "Адрес(а) BCC", + "NotificationsPushBulletSettingsDeviceIds": "Идентификаторы устройств", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Создавать файл .plexmatch в папке сериала", + "MetadataLoadError": "Невозможно загрузить метаданные", + "RecyclingBin": "Корзина", + "RecyclingBinCleanup": "Очистка корзины", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Удаленный загрузочный клиент {downloadClientName} сообщил о файлах в {path}, но это недопустимый путь {osName}. Проверьте правильность указанных путей и настройки загрузочного клиента.", + "ReplaceWithDash": "Заменить на тире", + "ReplaceWithSpaceDash": "Заменить на пробел тире", + "ReplaceWithSpaceDashSpace": "Заменить на пробел тире пробел", + "RequiredHelpText": "Чтобы применить пользовательский формат, условие {implementationName} должно соответствовать. В противном случае достаточно одного совпадения {implementationName}.", + "RescanAfterRefreshSeriesHelpText": "Пересканируйте папку сериала после обновления", + "RescanSeriesFolderAfterRefresh": "Повторное сканирование папки сериала после обновления", + "Reset": "Сброс", + "Umask777Description": "{octal} - пишут все", + "Umask770Description": "{octal} - Владелец и группа - запись", + "UiSettings": "Настройки пользовательского интерфейса", + "UiLanguage": "Язык пользовательского интерфейса", + "Ui": "Пользовательский интерфейс", + "ShowSeriesTitleHelpText": "Показать название сериала под постером", + "ShowSeasonCount": "Показать количество сезонов", + "TorrentBlackholeSaveMagnetFilesExtension": "Сохранить расширение магнет файлов", + "ShowQualityProfile": "Показать профиль качества", + "SeriesTypes": "Типы сериалов", + "SelectLanguages": "Выбрать языки", + "SelectIndexerFlags": "Выбор флагов индексатора", + "SelectEpisodes": "Выберите эпизод(ы)", + "RootFolderMissingHealthCheckMessage": "Отсутствует корневая папка: {rootFolderPath}", + "Retention": "Удержание", + "UnmonitorDeletedEpisodesHelpText": "Эпизоды, удаленные с диска, автоматически не отслеживаются в приложении {appName}", + "Umask775Description": "{octal} - Владелец и группа - запись, другое - чтение", + "TheLogLevelDefault": "По умолчанию уровень журнала равен «Информация», и его можно изменить в [Общие настройки](/settings/general)", + "SystemTimeHealthCheckMessage": "Расхождение системного времени более чем на 1 день. Запланированные задачи могут работать некорректно, пока не будет исправлено время", + "UiSettingsSummary": "Параметры календаря, даты и опции для слабовидящих", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Вместо перемещения файлов это даст указание {appName} скопировать или установить жесткую ссылку (в зависимости от настроек/конфигурации системы)", + "TorrentBlackholeSaveMagnetFilesHelpText": "Сохраните магнитную ссылку, если файл .torrent недоступен (полезно только в том случае, если клиент загрузки поддерживает магниты, сохраненные в файле)", + "SelectEpisodesModalTitle": "{modalTitle} ‐ выберите эпизод(ы)", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Расширение для магнитных ссылок, по умолчанию «.magnet»", + "OverrideGrabNoSeries": "Необходимо выбрать сериал", + "NotificationsPushBulletSettingSenderIdHelpText": "Идентификатор устройства для отправки уведомлений. Используйте device_iden в URL-адресе устройства на pushbullet.com (оставьте пустым, чтобы отправлять уведомления от себя)", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages поддерживает суффикс `:EN+DE`, позволяющий фильтровать языки, включенные в имя файла. Используйте `-DE`, чтобы исключить определенные языки. Добавление `+` (например, `:EN+`) приведет к выводу `[EN]`/`[EN+--]`/`[--]` в зависимости от исключенных языков. Например `{MediaInfo Full:EN+DE}`.", + "MaximumSingleEpisodeAgeHelpText": "Во время поиска по всему сезону будут разрешены только сезонные пакеты, если последний эпизод сезона старше этой настройки. Только стандартные сериалы. Используйте 0 для отключения." } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index b82f40186..06a8fd01d 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -847,5 +847,8 @@ "AllSeriesInRootFolderHaveBeenImported": "{path} içerisindeki tüm diziler içeri aktarıldı", "AlternateTitles": "Alternatif Başlıklar", "AnEpisodeIsDownloading": "Bir bölüm indiriliyor", - "UnableToImportAutomatically": "Otomatikman İçe Aktarılamıyor" + "UnableToImportAutomatically": "Otomatikman İçe Aktarılamıyor", + "Any": "Herhangi", + "ShowTags": "Etiketleri göster", + "ShowTagsHelpText": "Etiketleri posterin altında göster" } From 1662521d40f558d19360a078ad881bc8cbbbeb27 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:41:40 +0300 Subject: [PATCH 390/762] Fixed: Display tag list when sort by tags on series Posters --- .../Series/Index/Posters/SeriesIndexPoster.tsx | 18 ++++++++++-------- .../Index/Posters/SeriesIndexPosterInfo.css | 8 ++++++++ .../Posters/SeriesIndexPosterInfo.css.d.ts | 2 ++ .../Index/Posters/SeriesIndexPosterInfo.tsx | 15 +++++++++++++++ .../Index/Posters/SeriesIndexPosters.tsx | 5 +++++ 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index 474a226d9..a5d5d4978 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -211,14 +211,6 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { </div> ) : null} - {showTags && tags.length ? ( - <div className={styles.tags}> - <div className={styles.tagsList}> - <TagListConnector tags={tags} /> - </div> - </div> - ) : null} - {nextAiring ? ( <div className={styles.nextAiring} @@ -238,6 +230,14 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { </div> ) : null} + {showTags && tags.length ? ( + <div className={styles.tags}> + <div className={styles.tagsList}> + <TagListConnector tags={tags} /> + </div> + </div> + ) : null} + <SeriesIndexPosterInfo originalLanguage={originalLanguage} network={network} @@ -253,6 +253,8 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { shortDateFormat={shortDateFormat} longDateFormat={longDateFormat} timeFormat={timeFormat} + tags={tags} + showTags={showTags} /> <EditSeriesModalConnector diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css index 17741773d..1163d7549 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css @@ -3,3 +3,11 @@ text-align: center; font-size: $smallFontSize; } + +.tags { + composes: tags from '~./SeriesIndexPoster.css'; +} + +.tagsList { + composes: tagsList from '~./SeriesIndexPoster.css'; +} diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css.d.ts b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css.d.ts index 062365d36..54504816d 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css.d.ts +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.css.d.ts @@ -2,6 +2,8 @@ // Please do not change this file! interface CssExports { 'info': string; + 'tags': string; + 'tagsList': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx index 9a4265324..559ee9532 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import TagListConnector from 'Components/TagListConnector'; import Language from 'Language/Language'; import QualityProfile from 'typings/QualityProfile'; import formatDateTime from 'Utilities/Date/formatDateTime'; @@ -17,11 +18,13 @@ interface SeriesIndexPosterInfoProps { seasonCount: number; path: string; sizeOnDisk?: number; + tags: number[]; sortKey: string; showRelativeDates: boolean; shortDateFormat: string; longDateFormat: string; timeFormat: string; + showTags: boolean; } function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { @@ -35,11 +38,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { seasonCount, path, sizeOnDisk, + tags, sortKey, showRelativeDates, shortDateFormat, longDateFormat, timeFormat, + showTags, } = props; if (sortKey === 'network' && network) { @@ -122,6 +127,16 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { return <div className={styles.info}>{seasons}</div>; } + if (!showTags && sortKey === 'tags' && tags.length) { + return ( + <div className={styles.tags}> + <div className={styles.tagsList}> + <TagListConnector tags={tags} /> + </div> + </div> + ); + } + if (sortKey === 'path') { return ( <div className={styles.info} title={translate('Path')}> diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index 32b238a6c..055685216 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -183,6 +183,11 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { heights.push(19); } break; + case 'tags': + if (!showTags) { + heights.push(21); + } + break; default: // No need to add a height of 0 } From b7dfb8999d467f4113958f30c24ddb14def98c92 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:10:04 +0300 Subject: [PATCH 391/762] Improve tooltip for Next Airing on series Overview --- .../src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx | 4 +++- src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 2bbe8a66c..6120c86af 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -236,7 +236,9 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) { <div className={styles.infos}> {!!nextAiring && ( <SeriesIndexOverviewInfoRow - title={formatDateTime(nextAiring, longDateFormat, timeFormat)} + title={translate('NextAiringDate', { + date: formatDateTime(nextAiring, longDateFormat, timeFormat), + })} iconName={icons.SCHEDULED} label={getRelativeDate({ date: nextAiring, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 468d9f4b8..cafc59138 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1244,6 +1244,7 @@ "Never": "Never", "New": "New", "NextAiring": "Next Airing", + "NextAiringDate": "Next Airing: {date}", "NextExecution": "Next Execution", "No": "No", "NoBackupsAreAvailable": "No backups are available", From 2a26c6722afa5c657fde162cbddbe9e8731f3a0c Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:30:56 +0300 Subject: [PATCH 392/762] New: Ignore Litestream tables in Database --- .../Datastore/Migration/Framework/SqliteSchemaDumper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs index e4b79f730..e6a91cf05 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs @@ -219,7 +219,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework protected virtual IList<TableDefinition> ReadTables() { - const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"; + const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name;"; var dtTable = Read(sqlCommand).Tables[0]; var tableDefinitionList = new List<TableDefinition>(); From fbda2d54c7d1961997936d4f02916acee219629f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:31:29 +0300 Subject: [PATCH 393/762] New: Display original language on series details and search results page Closes #6984 --- .../AddNewSeries/AddNewSeriesSearchResult.css | 5 +++ .../AddNewSeriesSearchResult.css.d.ts | 2 ++ .../AddNewSeries/AddNewSeriesSearchResult.js | 26 +++++++++++++- frontend/src/Helpers/Props/icons.js | 2 ++ frontend/src/Series/Details/SeriesDetails.css | 1 + .../src/Series/Details/SeriesDetails.css.d.ts | 1 + frontend/src/Series/Details/SeriesDetails.js | 36 +++++++++++++------ 7 files changed, 62 insertions(+), 11 deletions(-) diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css index 469385630..c32e6efcb 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css @@ -69,6 +69,11 @@ height: 55px; } +.originalLanguageName, +.network { + margin-left: 8px; +} + .tvdbLink { composes: link from '~Components/Link/Link.css'; diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts index 1380d41f3..4d51aab62 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts @@ -4,6 +4,8 @@ interface CssExports { 'alreadyExistsIcon': string; 'content': string; 'icons': string; + 'network': string; + 'originalLanguageName': string; 'overlay': string; 'overview': string; 'poster': string; diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js index 2efb480bc..9ec6cf283 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js @@ -55,6 +55,7 @@ class AddNewSeriesSearchResult extends Component { titleSlug, year, network, + originalLanguage, status, overview, statistics, @@ -150,10 +151,32 @@ class AddNewSeriesSearchResult extends Component { /> </Label> + { + originalLanguage?.name ? + <Label size={sizes.LARGE}> + <Icon + name={icons.LANGUAGE} + size={13} + /> + + <span className={styles.originalLanguageName}> + {originalLanguage.name} + </span> + </Label> : + null + } + { network ? <Label size={sizes.LARGE}> - {network} + <Icon + name={icons.NETWORK} + size={13} + /> + + <span className={styles.network}> + {network} + </span> </Label> : null } @@ -219,6 +242,7 @@ AddNewSeriesSearchResult.propTypes = { titleSlug: PropTypes.string.isRequired, year: PropTypes.number.isRequired, network: PropTypes.string, + originalLanguage: PropTypes.object, status: PropTypes.string.isRequired, overview: PropTypes.string, statistics: PropTypes.object.isRequired, diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 4fbd5914c..d297257db 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -69,6 +69,7 @@ import { faHistory as fasHistory, faHome as fasHome, faInfoCircle as fasInfoCircle, + faLanguage as fasLanguage, faLaptop as fasLaptop, faLevelUpAlt as fasLevelUpAlt, faListCheck as fasListCheck, @@ -168,6 +169,7 @@ export const IGNORE = fasTimesCircle; export const INFO = fasInfoCircle; export const INTERACTIVE = fasUser; export const KEYBOARD = farKeyboard; +export const LANGUAGE = fasLanguage; export const LOGOUT = fasSignOutAlt; export const MANAGE = fasListCheck; export const MEDIA_INFO = farFileInvoice; diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index d7a26e4f8..f62568a1d 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -129,6 +129,7 @@ .path, .sizeOnDisk, .qualityProfileName, +.originalLanguageName, .network, .links, .tags { diff --git a/frontend/src/Series/Details/SeriesDetails.css.d.ts b/frontend/src/Series/Details/SeriesDetails.css.d.ts index 1af767e38..ea026f8de 100644 --- a/frontend/src/Series/Details/SeriesDetails.css.d.ts +++ b/frontend/src/Series/Details/SeriesDetails.css.d.ts @@ -15,6 +15,7 @@ interface CssExports { 'links': string; 'monitorToggleButton': string; 'network': string; + 'originalLanguageName': string; 'overview': string; 'path': string; 'poster': string; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index babc171f4..10e7938ee 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -185,6 +185,7 @@ class SeriesDetails extends Component { monitored, status, network, + originalLanguage, overview, images, seasons, @@ -431,7 +432,6 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <div> <Icon name={icons.FOLDER} @@ -449,7 +449,6 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <div> <Icon name={icons.DRIVE} @@ -477,7 +476,6 @@ class SeriesDetails extends Component { title={translate('QualityProfile')} size={sizes.LARGE} > - <div> <Icon name={icons.PROFILE} @@ -497,7 +495,6 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <div> <Icon name={monitored ? icons.MONITORED : icons.UNMONITORED} @@ -514,7 +511,6 @@ class SeriesDetails extends Component { title={statusDetails.message} size={sizes.LARGE} > - <div> <Icon name={statusDetails.icon} @@ -527,23 +523,43 @@ class SeriesDetails extends Component { </Label> { - !!network && + originalLanguage?.name ? + <Label + className={styles.detailsLabel} + title={translate('OriginalLanguage')} + size={sizes.LARGE} + > + <div> + <Icon + name={icons.LANGUAGE} + size={17} + /> + <span className={styles.originalLanguageName}> + {originalLanguage.name} + </span> + </div> + </Label> : + null + } + + { + network ? <Label className={styles.detailsLabel} title={translate('Network')} size={sizes.LARGE} > - <div> <Icon name={icons.NETWORK} size={17} /> - <span className={styles.qualityProfileName}> + <span className={styles.network}> {network} </span> </div> - </Label> + </Label> : + null } <Tooltip @@ -552,7 +568,6 @@ class SeriesDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > - <div> <Icon name={icons.EXTERNAL_LINK} @@ -734,6 +749,7 @@ SeriesDetails.propTypes = { monitor: PropTypes.string, status: PropTypes.string.isRequired, network: PropTypes.string, + originalLanguage: PropTypes.object, overview: PropTypes.string.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, seasons: PropTypes.arrayOf(PropTypes.object).isRequired, From bde5f68142bda7b0447df3586385bd56d4ce34ba Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 19 Jul 2024 23:51:28 +0300 Subject: [PATCH 394/762] Refresh series with recently aired episodes with TBA titles Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com> --- src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs b/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs index 7918d1ba0..f89687394 100644 --- a/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs +++ b/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Tv { @@ -28,6 +29,19 @@ namespace NzbDrone.Core.Tv return true; } + var episodes = _episodeService.GetEpisodeBySeries(series.Id); + + var atLeastOneAiredEpisodeWithoutTitle = episodes.Any(e => + e.SeasonNumber > 0 && + e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow) && + e.Title.Equals("TBA", StringComparison.Ordinal)); + + if (atLeastOneAiredEpisodeWithoutTitle) + { + _logger.Trace("Series {0} with at least one aired episode with TBA title, should refresh.", series.Title); + return true; + } + if (series.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) { _logger.Trace("Series {0} last updated less than 6 hours ago, should not be refreshed.", series.Title); @@ -40,7 +54,7 @@ namespace NzbDrone.Core.Tv return true; } - var lastEpisode = _episodeService.GetEpisodeBySeries(series.Id).MaxBy(e => e.AirDateUtc); + var lastEpisode = episodes.MaxBy(e => e.AirDateUtc); if (lastEpisode != null && lastEpisode.AirDateUtc > DateTime.UtcNow.AddDays(-30)) { From 1ad722acda5258f58646dc56dba9feea4fa36565 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:32:09 +0300 Subject: [PATCH 395/762] Fixed: Improve performance in Select Series Modal --- .../Series/SelectSeriesModalContent.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx index ad5aee15e..86e46a5bb 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx @@ -163,16 +163,21 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { [allSeries, onSeriesSelect] ); - const items = useMemo(() => { - const sorted = [...allSeries].sort(sortByProp('sortTitle')); + const sortedSeries = useMemo( + () => [...allSeries].sort(sortByProp('sortTitle')), + [allSeries] + ); - return sorted.filter( - (item) => - item.title.toLowerCase().includes(filter.toLowerCase()) || - item.tvdbId.toString().includes(filter) || - item.imdbId?.includes(filter) - ); - }, [allSeries, filter]); + const items = useMemo( + () => + sortedSeries.filter( + (item) => + item.title.toLowerCase().includes(filter.toLowerCase()) || + item.tvdbId.toString().includes(filter) || + item.imdbId?.includes(filter) + ), + [sortedSeries, filter] + ); return ( <ModalContent onModalClose={onModalClose}> From 5ad3d2efcce7d983bd783b551f32666529086901 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 24 Jul 2024 17:07:30 -0700 Subject: [PATCH 396/762] Fixed: Don't treat SubFrench as French audio language Closes #6995 --- .../ParserTests/LanguageParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/LanguageParser.cs | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 1fe5b84fa..cbbe5bf1f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -41,6 +41,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Title.the.Italian.Series.S01E01.The.Family.720p.HDTV.x264-FTP")] [TestCase("Title.the.Italy.Series.S02E01.720p.HDTV.x264-TLA")] [TestCase("Series Title - S01E01 - Pilot.en.sub")] + [TestCase("Series.Title.S01E01.SUBFRENCH.1080p.WEB.x264-GROUP")] + public void should_parse_language_unknown(string postTitle) { var result = LanguageParser.ParseLanguages(postTitle); diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index eea408334..00df92fb1 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; - private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b))(?:(?i)(?![\W|_|^]SUB))", @@ -49,11 +49,6 @@ namespace NzbDrone.Core.Parser var languages = new List<Language>(); - if (lowerTitle.Contains("french")) - { - languages.Add(Language.French); - } - if (lowerTitle.Contains("spanish")) { languages.Add(Language.Spanish); From 9a613afa355fbc8cdf29c4d1b8eb1f1586405eb7 Mon Sep 17 00:00:00 2001 From: ManiMatter <124743318+ManiMatter@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:33:08 +0200 Subject: [PATCH 397/762] Treat forcedMetaDL from qBit as queued instead of downloading --- src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index bf1d1a14f..56eb302fe 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -281,6 +281,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent break; case "metaDL": // torrent magnet is being downloaded + case "forcedMetaDL": // torrent metadata is being forcibly downloaded if (config.DhtEnabled) { item.Status = DownloadItemStatus.Queued; @@ -295,7 +296,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent break; case "forcedDL": // torrent is being downloaded, and was forced started - case "forcedMetaDL": // torrent metadata is being forcibly downloaded case "moving": // torrent is being moved from a folder case "downloading": // torrent is being downloaded and data is being transferred item.Status = DownloadItemStatus.Downloading; From 578f95546bc34bb5252f23dbb764e3cecb544e0b Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 25 Jul 2024 04:30:32 +0000 Subject: [PATCH 398/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: marudosurdo <marudosurdo@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ja/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ja.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/ja.json b/src/NzbDrone.Core/Localization/Core/ja.json index 4f2c6f41d..90c02aff8 100644 --- a/src/NzbDrone.Core/Localization/Core/ja.json +++ b/src/NzbDrone.Core/Localization/Core/ja.json @@ -1,3 +1,8 @@ { - "About": "約" + "About": "約", + "AddANewPath": "新しいパスを追加", + "Add": "追加", + "AddCondition": "条件の追加", + "DeleteAutoTagHelpText": "オートタグ'{name}'を削除してもよろしいですか?", + "Activity": "アクティビティ" } From a80f5b794be6cdf12834edf88dd0b8fcb33305ce Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 26 Jul 2024 10:25:15 +0000 Subject: [PATCH 399/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 3 ++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index c95808918..24a36fa6a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2093,5 +2093,6 @@ "InstallMajorVersionUpdate": "Instalar actualización", "CountVotes": "{votes} votos", "InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?", - "InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información." + "InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.", + "NextAiringDate": "Siguiente emisión: {date}" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 39bb7d60f..305a82bf6 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2093,5 +2093,6 @@ "RatingVotes": "Votos de Avaliação", "Install": "Instalar", "InstallMajorVersionUpdate": "Instalar Atualização", - "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações." + "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.", + "NextAiringDate": "Próxima Exibição: {date}" } From 6dd85a5af931509b950ac418781fcb1521d2b08d Mon Sep 17 00:00:00 2001 From: jbstark <33739840+jbstark@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:57:22 -0700 Subject: [PATCH 400/762] New: 'Seasons Monitored Status' Custom Filter to replace 'Has Unmonitored Season' Closes #6896 --- .../Filter/Builder/FilterBuilderRow.js | 4 + ...onsMonitoredStatusFilterBuilderRowValue.js | 35 ++ .../Helpers/Props/filterBuilderValueTypes.js | 1 + frontend/src/Store/Actions/seriesActions.js | 37 +- ...210_add_monitored_seasons_filterFixture.cs | 372 ++++++++++++++++++ .../210_add_monitored_seasons_filter.cs | 76 ++++ src/NzbDrone.Core/Localization/Core/en.json | 4 + 7 files changed, 517 insertions(+), 12 deletions(-) create mode 100644 frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 46a38a258..e12f8c40f 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -13,6 +13,7 @@ import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue'; import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue'; @@ -79,6 +80,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.QUALITY_PROFILE: return QualityProfileFilterBuilderRowValueConnector; + case filterBuilderValueTypes.SEASONS_MONITORED_STATUS: + return SeasonsMonitoredStatusFilterBuilderRowValue; + case filterBuilderValueTypes.SERIES: return SeriesFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js new file mode 100644 index 000000000..b84260e3c --- /dev/null +++ b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js @@ -0,0 +1,35 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +const seasonsMonitoredStatusList = [ + { + id: 'all', + get name() { + return translate('SeasonsMonitoredAll'); + } + }, + { + id: 'partial', + get name() { + return translate('SeasonsMonitoredPartial'); + } + }, + { + id: 'none', + get name() { + return translate('SeasonsMonitoredNone'); + } + } +]; + +function SeasonsMonitoredStatusFilterBuilderRowValue(props) { + return ( + <FilterBuilderRowValue + tagList={seasonsMonitoredStatusList} + {...props} + /> + ); +} + +export default SeasonsMonitoredStatusFilterBuilderRowValue; diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 1f4227779..d9a5d58c7 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -8,6 +8,7 @@ export const LANGUAGE = 'language'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; +export const SEASONS_MONITORED_STATUS = 'seasonsMonitoredStatus'; export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; export const SERIES_TYPES = 'seriesType'; diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index 3aa9b7237..c18104065 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -202,20 +202,33 @@ export const filterPredicates = { return predicate(hasMissingSeason, filterValue); }, - hasUnmonitoredSeason: function(item, filterValue, type) { + seasonsMonitoredStatus: function(item, filterValue, type) { const predicate = filterTypePredicates[type]; const { seasons = [] } = item; - const hasUnmonitoredSeason = seasons.some((season) => { - const { - seasonNumber, - monitored - } = season; + const { monitoredCount, unmonitoredCount } = seasons.reduce((acc, { seasonNumber, monitored }) => { + if (seasonNumber <= 0) { + return acc; + } - return seasonNumber > 0 && !monitored; - }); + if (monitored) { + acc.monitoredCount++; + } else { + acc.unmonitoredCount++; + } - return predicate(hasUnmonitoredSeason, filterValue); + return acc; + }, { monitoredCount: 0, unmonitoredCount: 0 }); + + let seasonsMonitoredStatus = 'partial'; + + if (monitoredCount === 0) { + seasonsMonitoredStatus = 'none'; + } else if (unmonitoredCount === 0) { + seasonsMonitoredStatus = 'all'; + } + + return predicate(seasonsMonitoredStatus, filterValue); } }; @@ -383,10 +396,10 @@ export const filterBuilderProps = [ valueType: filterBuilderValueTypes.BOOL }, { - name: 'hasUnmonitoredSeason', - label: () => translate('HasUnmonitoredSeason'), + name: 'seasonsMonitoredStatus', + label: () => translate('SeasonsMonitoredStatus'), type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.BOOL + valueType: filterBuilderValueTypes.SEASONS_MONITORED_STATUS }, { name: 'year', diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs new file mode 100644 index 000000000..94bd7fb68 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs @@ -0,0 +1,372 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_monitored_seasons_filterFixture : MigrationTest<add_monitored_seasons_filter> + { + [Test] + public void equal_both_becomes_equal_every_option() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { true, false }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all", "partial", "none" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void notEqual_both_becomes_notEqual_every_option() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { true, false }, + type = "notEqual" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all", "partial", "none" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void equal_true_becomes_notEqual_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { true }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void notEqual_true_becomes_equal_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { true }, + type = "notEqual" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void equal_false_becomes_equal_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { false }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void notEqual_false_becomes_notEqual_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { false }, + type = "notEqual" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { "all" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void missing_hasUnmonitored_unchanged() + { + var filter = new FilterSettings210 + { + key = "monitored", + value = new List<object> { false }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("monitored"); + filters[0].value.Should().BeEquivalentTo(new List<object> { false }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void has_hasUnmonitored_not_in_first_entry() + { + var filter1 = new FilterSettings210 + { + key = "monitored", + value = new List<object> { false }, + type = "equal" + }; + var filter2 = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { true }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter1, filter2 }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("monitored"); + filters[0].value.Should().BeEquivalentTo(new List<object> { false }); + filters[0].type.Should().Be("equal"); + filters[1].key.Should().Be("seasonsMonitoredStatus"); + filters[1].value.Should().BeEquivalentTo(new List<object> { "all" }); + filters[1].type.Should().Be("notEqual"); + } + + [Test] + public void has_umonitored_is_empty() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List<object> { }, + type = "equal" + }; + + var filtersJson = new List<FilterSettings210> { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query<FilterDefinition210>("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject<List<FilterSettings210>>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List<object> { }); + filters[0].type.Should().Be("equal"); + } + } + + public class FilterDefinition210 + { + public int Id { get; set; } + public string Type { get; set; } + public string Label { get; set; } + public string Filters { get; set; } + } + + public class FilterSettings210 + { + public string key { get; set; } + public List<object> value { get; set; } + public string type { get; set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs b/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs new file mode 100644 index 000000000..ad07f3a93 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(210)] + public class add_monitored_seasons_filter : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeHasUnmonitoredSeason); + } + + private void ChangeHasUnmonitoredSeason(IDbConnection conn, IDbTransaction tran) + { + var updated = new List<object>(); + using (var getUnmonitoredSeasonFilter = conn.CreateCommand()) + { + getUnmonitoredSeasonFilter.Transaction = tran; + getUnmonitoredSeasonFilter.CommandText = "SELECT \"Id\", \"Filters\" FROM \"CustomFilters\" WHERE \"Type\" = 'series'"; + + using (var reader = getUnmonitoredSeasonFilter.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var filters = JArray.Parse(reader.GetString(1)); + + foreach (var filter in filters) + { + if (filter["key"]?.ToString() == "hasUnmonitoredSeason") + { + var value = filter["value"].ToString(); + var type = filter["type"].ToString(); + + filter["key"] = "seasonsMonitoredStatus"; + + if (value.Contains("true") && value.Contains("false")) + { + filter["value"] = new JArray("all", "partial", "none"); + } + else if (value.Contains("true")) + { + filter["type"] = type == "equal" ? "notEqual" : "equal"; + filter["value"] = new JArray("all"); + } + else if (value.Contains("false")) + { + filter["value"] = new JArray("all"); + } + else + { + filter["value"] = new JArray(); + } + } + } + + updated.Add(new + { + Filters = filters.ToJson(), + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"CustomFilters\" SET \"Filters\" = @Filters WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index cafc59138..5da122a3e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1784,6 +1784,10 @@ "SeasonPremiere": "Season Premiere", "SeasonPremieresOnly": "Season Premieres Only", "Seasons": "Seasons", + "SeasonsMonitoredAll": "All", + "SeasonsMonitoredPartial": "Partial", + "SeasonsMonitoredNone": "None", + "SeasonsMonitoredStatus": "Seasons Monitored", "SecretToken": "Secret Token", "Security": "Security", "Seeders": "Seeders", From e791f4b743d9660b0ad1decc4c5ed0e864f3b243 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jul 2024 16:57:54 -0700 Subject: [PATCH 401/762] Fixed: Updating series path from different OS paths Closes #6953 --- .../PathExtensionFixture.cs | 41 +++++-- src/NzbDrone.Common/Disk/OsPath.cs | 104 +++++++++++++++++- .../Extensions/PathExtensions.cs | 43 +++----- src/NzbDrone.Core/Tv/SeriesPathBuilder.cs | 16 ++- 4 files changed, 162 insertions(+), 42 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 967126a1c..ddb54c538 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -133,11 +133,16 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\test\", @"C:\Test\mydir")] [TestCase(@"C:\test", @"C:\Test\mydir\")] - public void path_should_be_parent_on_windows_only(string parentPath, string childPath) + public void windows_path_should_be_parent(string parentPath, string childPath) { - var expectedResult = OsInfo.IsWindows; + parentPath.IsParentPath(childPath).Should().Be(true); + } - parentPath.IsParentPath(childPath).Should().Be(expectedResult); + [TestCase("/test", "/test/mydir/")] + [TestCase("/test/", "/test/mydir")] + public void posix_path_should_be_parent(string parentPath, string childPath) + { + parentPath.IsParentPath(childPath).Should().Be(true); } [TestCase(@"C:\Test\mydir", @"C:\Test")] @@ -145,39 +150,57 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] - public void path_should_return_parent_windows(string path, string parentPath) + public void windows_path_should_return_parent(string path, string parentPath) { - WindowsOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] - public void path_should_return_parent_mono(string path, string parentPath) + [TestCase(@"/test/tv", "/test")] + public void unix_path_should_return_parent(string path, string parentPath) { - PosixOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"C:\Test\mydir", "Test")] [TestCase(@"C:\Test\", @"C:\")] + [TestCase(@"C:\Test", @"C:\")] [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] public void path_should_return_parent_name_windows(string path, string parentPath) { - WindowsOnly(); path.GetParentName().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] + [TestCase(@"/test/tv", "test")] public void path_should_return_parent_name_mono(string path, string parentPath) { - PosixOnly(); path.GetParentName().Should().Be(parentPath); } + [TestCase(@"C:\Test\mydir", "mydir")] + [TestCase(@"C:\Test\", "Test")] + [TestCase(@"C:\Test", "Test")] + [TestCase(@"C:\", "C:\\")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\test", "test")] + public void path_should_return_directory_name_windows(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + + [TestCase(@"/", "/")] + [TestCase(@"/test", "test")] + [TestCase(@"/test/tv", "tv")] + public void path_should_return_directory_name_mono(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + [Test] public void path_should_return_parent_for_oversized_path() { diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index f6f01fccf..45e520761 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -9,6 +10,8 @@ namespace NzbDrone.Common.Disk private readonly string _path; private readonly OsPathKind _kind; + private static readonly Regex UncPathRegex = new Regex(@"(?<unc>^\\\\(?:\?\\UNC\\)?[^\\]+\\[^\\]+)(?:\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public OsPath(string path) { if (path == null) @@ -96,6 +99,19 @@ namespace NzbDrone.Common.Disk return path; } + private static string TrimTrailingSlash(string path, OsPathKind kind) + { + switch (kind) + { + case OsPathKind.Windows when !path.EndsWith(":\\"): + return path.TrimEnd('\\'); + case OsPathKind.Unix when path != "/": + return path.TrimEnd('/'); + } + + return path; + } + public OsPathKind Kind => _kind; public bool IsWindowsPath => _kind == OsPathKind.Windows; @@ -130,7 +146,19 @@ namespace NzbDrone.Common.Disk if (index == -1) { - return new OsPath(null); + return Null; + } + + var rootLength = GetRootLength(); + + if (rootLength == _path.Length) + { + return Null; + } + + if (rootLength > index + 1) + { + return new OsPath(_path.Substring(0, rootLength)); } return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); @@ -139,6 +167,8 @@ namespace NzbDrone.Common.Disk public string FullPath => _path; + public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind); + public string FileName { get @@ -161,6 +191,30 @@ namespace NzbDrone.Common.Disk } } + public string Name + { + // Meant to behave similar to DirectoryInfo.Name + + get + { + var index = GetFileNameIndex(); + + if (index == -1) + { + return PathWithoutTrailingSlash; + } + + var rootLength = GetRootLength(); + + if (rootLength > index + 1) + { + return _path.Substring(0, rootLength); + } + + return TrimTrailingSlash(_path.Substring(index).TrimStart('/', '\\'), _kind); + } + } + public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs); private int GetFileNameIndex() @@ -190,11 +244,50 @@ namespace NzbDrone.Common.Disk return index; } + private int GetRootLength() + { + if (!IsRooted) + { + return 0; + } + + if (_kind == OsPathKind.Unix) + { + return 1; + } + + if (_kind == OsPathKind.Windows) + { + if (HasWindowsDriveLetter(_path)) + { + return 3; + } + + var uncMatch = UncPathRegex.Match(_path); + + // \\?\UNC\server\share\ or \\server\share + if (uncMatch.Success) + { + return uncMatch.Groups["unc"].Length; + } + + // \\?\C:\ + if (_path.StartsWith(@"\\?\")) + { + return 7; + } + } + + return 0; + } + private string[] GetFragments() { return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); } + public static OsPath Null => new (null); + public override string ToString() { return _path; @@ -267,6 +360,11 @@ namespace NzbDrone.Common.Disk } public bool Equals(OsPath other) + { + return Equals(other, false); + } + + public bool Equals(OsPath other, bool ignoreTrailingSlash) { if (ReferenceEquals(other, null)) { @@ -278,8 +376,8 @@ namespace NzbDrone.Common.Disk return true; } - var left = _path; - var right = other._path; + var left = ignoreTrailingSlash ? PathWithoutTrailingSlash : _path; + var right = ignoreTrailingSlash ? other.PathWithoutTrailingSlash : other._path; if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) { diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 4585326f1..7dced0c0e 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -92,26 +92,23 @@ namespace NzbDrone.Common.Extensions public static string GetParentPath(this string childPath) { - var cleanPath = childPath.GetCleanPath(); + var path = new OsPath(childPath).Directory; - if (cleanPath.IsNullOrWhiteSpace()) - { - return null; - } - - return Directory.GetParent(cleanPath)?.FullName; + return path == OsPath.Null ? null : path.PathWithoutTrailingSlash; } public static string GetParentName(this string childPath) { - var cleanPath = childPath.GetCleanPath(); + var path = new OsPath(childPath).Directory; - if (cleanPath.IsNullOrWhiteSpace()) - { - return null; - } + return path == OsPath.Null ? null : path.Name; + } - return Directory.GetParent(cleanPath)?.Name; + public static string GetDirectoryName(this string childPath) + { + var path = new OsPath(childPath); + + return path == OsPath.Null ? null : path.Name; } public static string GetCleanPath(this string path) @@ -125,27 +122,17 @@ namespace NzbDrone.Common.Extensions public static bool IsParentPath(this string parentPath, string childPath) { - if (parentPath != "/" && !parentPath.EndsWith(":\\")) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - } + var parent = new OsPath(parentPath); + var child = new OsPath(childPath); - if (childPath != "/" && !parentPath.EndsWith(":\\")) + while (child.Directory != OsPath.Null) { - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - } - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison)) + if (child.Directory.Equals(parent, true)) { return true; } - child = child.Parent; + child = child.Directory; } return false; diff --git a/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs index 5a222774f..738569a51 100644 --- a/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs +++ b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; using NzbDrone.Core.RootFolders; @@ -15,11 +16,13 @@ namespace NzbDrone.Core.Tv { private readonly IBuildFileNames _fileNameBuilder; private readonly IRootFolderService _rootFolderService; + private readonly Logger _logger; - public SeriesPathBuilder(IBuildFileNames fileNameBuilder, IRootFolderService rootFolderService) + public SeriesPathBuilder(IBuildFileNames fileNameBuilder, IRootFolderService rootFolderService, Logger logger) { _fileNameBuilder = fileNameBuilder; _rootFolderService = rootFolderService; + _logger = logger; } public string BuildPath(Series series, bool useExistingRelativeFolder) @@ -42,7 +45,16 @@ namespace NzbDrone.Core.Tv { var rootFolderPath = _rootFolderService.GetBestRootFolderPath(series.Path); - return rootFolderPath.GetRelativePath(series.Path); + if (rootFolderPath.IsParentPath(series.Path)) + { + return rootFolderPath.GetRelativePath(series.Path); + } + + var directoryName = series.Path.GetDirectoryName(); + + _logger.Warn("Unable to get relative path for series path {0}, using series folder name {1}", series.Path, directoryName); + + return directoryName; } } } From 63fdf8ca8ff9b22ce4cf8764cc05aad5d1d0ae62 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jul 2024 16:58:16 -0700 Subject: [PATCH 402/762] Cache root folders and improve getting disk space for series path roots --- .../DiskSpace/DiskSpaceServiceFixture.cs | 25 ++++++++----- .../DiskSpace/DiskSpaceService.cs | 15 ++++++-- .../RootFolders/RootFolderService.cs | 35 +++++++++++++------ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs index 875fe0b35..114c8295e 100644 --- a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.DiskSpace; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -16,14 +17,14 @@ namespace NzbDrone.Core.Test.DiskSpace { private string _seriesFolder; private string _seriesFolder2; - private string _droneFactoryFolder; + private string _rootFolder; [SetUp] public void SetUp() { _seriesFolder = @"G:\fasdlfsdf\series".AsOsAgnostic(); _seriesFolder2 = @"G:\fasdlfsdf\series2".AsOsAgnostic(); - _droneFactoryFolder = @"G:\dronefactory".AsOsAgnostic(); + _rootFolder = @"G:\fasdlfsdf".AsOsAgnostic(); Mocker.GetMock<IDiskProvider>() .Setup(v => v.GetMounts()) @@ -51,6 +52,13 @@ namespace NzbDrone.Core.Test.DiskSpace .Returns(new Dictionary<int, string>(seriesPaths.Select((value, i) => new KeyValuePair<int, string>(i, value)))); } + private void GivenRootFolder(string seriesPath, string rootFolderPath) + { + Mocker.GetMock<IRootFolderService>() + .Setup(v => v.GetBestRootFolderPath(seriesPath)) + .Returns(rootFolderPath); + } + private void GivenExistingFolder(string folder) { Mocker.GetMock<IDiskProvider>() @@ -62,8 +70,8 @@ namespace NzbDrone.Core.Test.DiskSpace public void should_check_diskspace_for_series_folders() { GivenSeries(_seriesFolder); - - GivenExistingFolder(_seriesFolder); + GivenRootFolder(_seriesFolder, _rootFolder); + GivenExistingFolder(_rootFolder); var freeSpace = Subject.GetFreeSpace(); @@ -74,9 +82,9 @@ namespace NzbDrone.Core.Test.DiskSpace public void should_check_diskspace_for_same_root_folder_only_once() { GivenSeries(_seriesFolder, _seriesFolder2); - - GivenExistingFolder(_seriesFolder); - GivenExistingFolder(_seriesFolder2); + GivenRootFolder(_seriesFolder, _rootFolder); + GivenRootFolder(_seriesFolder2, _rootFolder); + GivenExistingFolder(_rootFolder); var freeSpace = Subject.GetFreeSpace(); @@ -87,9 +95,10 @@ namespace NzbDrone.Core.Test.DiskSpace } [Test] - public void should_not_check_diskspace_for_missing_series_folders() + public void should_not_check_diskspace_for_missing_series_root_folders() { GivenSeries(_seriesFolder); + GivenRootFolder(_seriesFolder, _rootFolder); var freeSpace = Subject.GetFreeSpace(); diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index c9846b084..7b3950347 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; namespace NzbDrone.Core.DiskSpace @@ -18,14 +19,16 @@ namespace NzbDrone.Core.DiskSpace public class DiskSpaceService : IDiskSpaceService { private readonly ISeriesService _seriesService; + private readonly IRootFolderService _rootFolderService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled); - public DiskSpaceService(ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) + public DiskSpaceService(ISeriesService seriesService, IRootFolderService rootFolderService, IDiskProvider diskProvider, Logger logger) { _seriesService = seriesService; + _rootFolderService = rootFolderService; _diskProvider = diskProvider; _logger = logger; } @@ -43,9 +46,15 @@ namespace NzbDrone.Core.DiskSpace private IEnumerable<string> GetSeriesRootPaths() { + // Get all series paths and find the correct root folder for each. For each unique root folder path, + // ensure the path exists and get its path root and return all unique path roots. + return _seriesService.GetAllSeriesPaths() - .Where(s => s.Value.IsPathValid(PathValidationType.CurrentOs) && _diskProvider.FolderExists(s.Value)) - .Select(s => _diskProvider.GetPathRoot(s.Value)) + .Where(s => s.Value.IsPathValid(PathValidationType.CurrentOs)) + .Select(s => _rootFolderService.GetBestRootFolderPath(s.Value)) + .Distinct() + .Where(r => _diskProvider.FolderExists(r)) + .Select(r => _diskProvider.GetPathRoot(r)) .Distinct(); } diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index da039e967..09bbf7134 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; @@ -30,6 +31,8 @@ namespace NzbDrone.Core.RootFolders private readonly INamingConfigService _namingConfigService; private readonly Logger _logger; + private readonly ICached<string> _cache; + private static readonly HashSet<string> SpecialFolders = new HashSet<string> { "$recycle.bin", @@ -47,6 +50,7 @@ namespace NzbDrone.Core.RootFolders IDiskProvider diskProvider, ISeriesRepository seriesRepository, INamingConfigService namingConfigService, + ICacheManager cacheManager, Logger logger) { _rootFolderRepository = rootFolderRepository; @@ -54,6 +58,8 @@ namespace NzbDrone.Core.RootFolders _seriesRepository = seriesRepository; _namingConfigService = namingConfigService; _logger = logger; + + _cache = cacheManager.GetCache<string>(GetType()); } public List<RootFolder> All() @@ -110,13 +116,14 @@ namespace NzbDrone.Core.RootFolders if (!_diskProvider.FolderWritable(rootFolder.Path)) { - throw new UnauthorizedAccessException(string.Format("Root folder path '{0}' is not writable by user '{1}'", rootFolder.Path, Environment.UserName)); + throw new UnauthorizedAccessException($"Root folder path '{rootFolder.Path}' is not writable by user '{Environment.UserName}'"); } _rootFolderRepository.Insert(rootFolder); var seriesPaths = _seriesRepository.AllSeriesPaths(); GetDetails(rootFolder, seriesPaths, true); + _cache.Clear(); return rootFolder; } @@ -124,6 +131,7 @@ namespace NzbDrone.Core.RootFolders public void Remove(int id) { _rootFolderRepository.Delete(id); + _cache.Clear(); } private List<UnmappedFolder> GetUnmappedFolders(string path, Dictionary<int, string> seriesPaths) @@ -186,16 +194,7 @@ namespace NzbDrone.Core.RootFolders public string GetBestRootFolderPath(string path) { - var possibleRootFolder = All().Where(r => r.Path.IsParentPath(path)).MaxBy(r => r.Path.Length); - - if (possibleRootFolder == null) - { - var osPath = new OsPath(path); - - return osPath.Directory.ToString().TrimEnd(osPath.IsUnixPath ? '/' : '\\'); - } - - return possibleRootFolder?.Path; + return _cache.Get(path, () => GetBestRootFolderPathInternal(path), TimeSpan.FromDays(1)); } private void GetDetails(RootFolder rootFolder, Dictionary<int, string> seriesPaths, bool timeout) @@ -211,5 +210,19 @@ namespace NzbDrone.Core.RootFolders } }).Wait(timeout ? 5000 : -1); } + + private string GetBestRootFolderPathInternal(string path) + { + var possibleRootFolder = All().Where(r => r.Path.IsParentPath(path)).MaxBy(r => r.Path.Length); + + if (possibleRootFolder == null) + { + var osPath = new OsPath(path); + + return osPath.Directory.ToString().TrimEnd(osPath.IsUnixPath ? '/' : '\\'); + } + + return possibleRootFolder.Path; + } } } From 5c2c490cb28c102121d036b77d10fd79d844c0e2 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:57:56 +0300 Subject: [PATCH 403/762] Improve messaging for renamed episode files progress info --- src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs index a4f9d97a9..8993ba661 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs @@ -164,8 +164,8 @@ namespace NzbDrone.Core.MediaFiles var episodeFiles = _mediaFileService.Get(message.Files); _logger.ProgressInfo("Renaming {0} files for {1}", episodeFiles.Count, series.Title); - RenameFiles(episodeFiles, series); - _logger.ProgressInfo("Selected episode files renamed for {0}", series.Title); + var renamedFiles = RenameFiles(episodeFiles, series); + _logger.ProgressInfo("{0} selected episode files renamed for {1}", renamedFiles.Count, series.Title); _eventAggregator.PublishEvent(new RenameCompletedEvent()); } @@ -179,8 +179,8 @@ namespace NzbDrone.Core.MediaFiles { var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); _logger.ProgressInfo("Renaming all files in series: {0}", series.Title); - RenameFiles(episodeFiles, series); - _logger.ProgressInfo("All episode files renamed for {0}", series.Title); + var renamedFiles = RenameFiles(episodeFiles, series); + _logger.ProgressInfo("{0} episode files renamed for {1}", renamedFiles.Count, series.Title); } _eventAggregator.PublishEvent(new RenameCompletedEvent()); From 60cba74c39855b90633f155d81f6b8709f05a0a0 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:02:51 +0300 Subject: [PATCH 404/762] Bump ImageSharp to 3.1.5 https://github.com/advisories/GHSA-63p8-c4ww-9cg7 --- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index e66e1483c..b5de0b05f 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -19,7 +19,7 @@ <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.1.4" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> From 5ac6c0e651400aa4d2e7126b0ccf1bcd4c6224b2 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 25 Jul 2024 17:11:21 -0700 Subject: [PATCH 405/762] Fix height of tags in tag inputs --- frontend/src/Components/Form/TagInputTag.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Components/Form/TagInputTag.css b/frontend/src/Components/Form/TagInputTag.css index 7e66a4d12..1a8ff45d6 100644 --- a/frontend/src/Components/Form/TagInputTag.css +++ b/frontend/src/Components/Form/TagInputTag.css @@ -30,5 +30,6 @@ .label { composes: label from '~Components/Label.css'; + display: flex; max-width: 100%; } From 33b62a2def7321313018e49e3fc8ed08b3234786 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 29 Jul 2024 02:59:10 +0300 Subject: [PATCH 406/762] New: Add TVMaze and TMDB IDs to Kodi .nfo (#7011) Closes #6895 ignore-downstream --- .../Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 249a9cbd3..a65168bdf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -176,6 +176,20 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc tvShow.Add(imdbId); } + if (series.TmdbId > 0) + { + var tmdbId = new XElement("uniqueid", series.TmdbId); + tmdbId.SetAttributeValue("type", "tmdb"); + tvShow.Add(tmdbId); + } + + if (series.TvMazeId > 0) + { + var tvMazeId = new XElement("uniqueid", series.TvMazeId); + tvMazeId.SetAttributeValue("type", "tvmaze"); + tvShow.Add(tvMazeId); + } + foreach (var genre in series.Genres) { tvShow.Add(new XElement("genre", genre)); From bc7799139e52b92956eb595fb87f44d7dda9a320 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jul 2024 16:28:44 -0700 Subject: [PATCH 407/762] Don't hash files in development builds --- frontend/build/webpack.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index e0ec27c27..616ee5637 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -67,7 +67,7 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: '[name]-[contenthash].js', + filename: isProduction ? '[name]-[contenthash].js' : '[name].js', sourceMapFilename: '[file].map' }, @@ -92,7 +92,7 @@ module.exports = (env) => { new MiniCssExtractPlugin({ filename: 'Content/styles.css', - chunkFilename: 'Content/[id]-[chunkhash].css' + chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' }), new HtmlWebpackPlugin({ @@ -202,7 +202,7 @@ module.exports = (env) => { options: { importLoaders: 1, modules: { - localIdentName: '[name]/[local]/[hash:base64:5]' + localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' } } }, From f2f4a98eed5bc83224917897642a28381ca648b9 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 27 Jul 2024 11:13:00 +0200 Subject: [PATCH 408/762] Fixed: Interactive Import dropdown width on mobile Closes #7015 --- .../Interactive/InteractiveImportModalContent.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css index 3be9c8f36..b15e0196e 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -18,12 +18,17 @@ .leftButtons, .rightButtons { display: flex; - flex: 1 0 50%; flex-wrap: wrap; + min-width: 0; +} + +.leftButtons { + flex: 0 1 auto; } .rightButtons { justify-content: flex-end; + flex: 1 1 50%; } .deleteButton { @@ -37,6 +42,7 @@ composes: select from '~Components/Form/SelectInput.css'; margin-right: 10px; + max-width: 100%; width: auto; } @@ -49,10 +55,12 @@ .leftButtons, .rightButtons { flex-direction: column; + gap: 3px; } .leftButtons { align-items: flex-start; + max-width: fit-content; } .rightButtons { From 15e3c3efb18242caf28b9bfc77a72a78296018bf Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:48:39 +0300 Subject: [PATCH 409/762] Include available version in update health check --- .../HealthCheck/Checks/UpdateCheck.cs | 14 ++++++++++++-- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index c723e1c4e..b505785a1 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -86,9 +86,19 @@ namespace NzbDrone.Core.HealthCheck.Checks } } - if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null) + if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14)) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage")); + var latestAvailable = _checkUpdateService.AvailableUpdate(); + + if (latestAvailable != null) + { + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary<string, object> + { + { "version", $"v{latestAvailable.Version}" } + })); + } } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5da122a3e..42ad9dc5c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -2037,7 +2037,7 @@ "UpcomingSeriesDescription": "Series has been announced but no exact air date yet", "UpdateAll": "Update All", "UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates", - "UpdateAvailableHealthCheckMessage": "New update is available", + "UpdateAvailableHealthCheckMessage": "New update is available: {version}", "UpdateFiltered": "Update Filtered", "UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script", "UpdateMonitoring": "Update Monitoring", From 3824eff5ebf3971f8c6d14fff1f5ca730ff7d5c0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 27 Jul 2024 22:10:20 -0700 Subject: [PATCH 410/762] New: Parse Chinese Anime that separates titles with vertical bar Closes #7014 --- .../ParserTests/UnicodeReleaseParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs index 24844a9a9..f9b8c44a3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UnicodeReleaseParserFixture.cs @@ -53,6 +53,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[UHA-WINGS][Anime-Series Title S02][01][x264 1080p][CHT].mp4", "Anime-Series Title S2", "UHA-WINGS", 1)] [TestCase("[Suzuya Raws] 腼腆英雄 东京夺还篇 / Series 2nd Season - 01 [CR WebRip 1080p HEVC-10bit AAC][Multi-Subs]", "Series 2nd Season", "Suzuya Raws", 1)] [TestCase("[ANi] SERIES / SERIES 靦腆英雄 - 11 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]", "SERIES", "ANi", 11)] + [TestCase("[Q] 全职高手 第3季 / Series S3 - 09 (1080p HBR HEVC Multi-Sub)", "Series S3", "Q", 9)] + [TestCase("[Q] 全职高手 第3季 | Series S3 - 09 (1080p HBR HEVC Multi-Sub)", "Series S3", "Q", 9)] public void should_parse_chinese_anime_season_episode_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber) { postTitle = XmlCleaner.ReplaceUnicode(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index acdcf849c..daad36a6f 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -47,7 +47,10 @@ namespace NzbDrone.Core.Parser new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), // GM-Team releases with lots of square brackets - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled) + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), }; private static readonly Regex[] ReportTitleRegex = new[] From ee80564dd427ca1dc14c192955efaa61f386ad44 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 18 Jul 2024 22:44:06 -0700 Subject: [PATCH 411/762] Convert Blocklist to TypeScript --- frontend/src/Activity/Blocklist/Blocklist.js | 284 --------------- frontend/src/Activity/Blocklist/Blocklist.tsx | 326 ++++++++++++++++++ .../Activity/Blocklist/BlocklistConnector.js | 161 --------- .../Blocklist/BlocklistDetailsModal.js | 90 ----- .../Blocklist/BlocklistDetailsModal.tsx | 64 ++++ .../src/Activity/Blocklist/BlocklistRow.js | 212 ------------ .../src/Activity/Blocklist/BlocklistRow.tsx | 163 +++++++++ .../Blocklist/BlocklistRowConnector.js | 26 -- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppSectionState.ts | 10 +- frontend/src/App/State/BlocklistAppState.ts | 12 +- frontend/src/Components/Table/Column.ts | 1 + frontend/src/Components/Table/usePaging.ts | 54 +++ .../src/DownloadClient/DownloadProtocol.ts | 6 +- frontend/src/Helpers/Hooks/useCurrentPage.ts | 9 + frontend/src/Series/useSeries.ts | 19 + .../src/Store/Actions/blocklistActions.js | 12 - frontend/src/typings/Blocklist.ts | 6 +- frontend/src/typings/Table.ts | 6 + src/NzbDrone.Core/Localization/Core/en.json | 2 +- 20 files changed, 670 insertions(+), 797 deletions(-) delete mode 100644 frontend/src/Activity/Blocklist/Blocklist.js create mode 100644 frontend/src/Activity/Blocklist/Blocklist.tsx delete mode 100644 frontend/src/Activity/Blocklist/BlocklistConnector.js delete mode 100644 frontend/src/Activity/Blocklist/BlocklistDetailsModal.js create mode 100644 frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx delete mode 100644 frontend/src/Activity/Blocklist/BlocklistRow.js create mode 100644 frontend/src/Activity/Blocklist/BlocklistRow.tsx delete mode 100644 frontend/src/Activity/Blocklist/BlocklistRowConnector.js create mode 100644 frontend/src/Components/Table/usePaging.ts create mode 100644 frontend/src/Helpers/Hooks/useCurrentPage.ts create mode 100644 frontend/src/Series/useSeries.ts create mode 100644 frontend/src/typings/Table.ts diff --git a/frontend/src/Activity/Blocklist/Blocklist.js b/frontend/src/Activity/Blocklist/Blocklist.js deleted file mode 100644 index 19026beb5..000000000 --- a/frontend/src/Activity/Blocklist/Blocklist.js +++ /dev/null @@ -1,284 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import TablePager from 'Components/Table/TablePager'; -import { align, icons, kinds } from 'Helpers/Props'; -import getRemovedItems from 'Utilities/Object/getRemovedItems'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; -import BlocklistFilterModal from './BlocklistFilterModal'; -import BlocklistRowConnector from './BlocklistRowConnector'; - -class Blocklist extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - allSelected: false, - allUnselected: false, - lastToggled: null, - selectedState: {}, - isConfirmRemoveModalOpen: false, - isConfirmClearModalOpen: false, - items: props.items - }; - } - - componentDidUpdate(prevProps) { - const { - items - } = this.props; - - if (hasDifferentItems(prevProps.items, items)) { - this.setState((state) => { - return { - ...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)), - items - }; - }); - - return; - } - } - - // - // Control - - getSelectedIds = () => { - return getSelectedIds(this.state.selectedState); - }; - - // - // Listeners - - onSelectAllChange = ({ value }) => { - this.setState(selectAll(this.state.selectedState, value)); - }; - - onSelectedChange = ({ id, value, shiftKey = false }) => { - this.setState((state) => { - return toggleSelected(state, this.props.items, id, value, shiftKey); - }); - }; - - onRemoveSelectedPress = () => { - this.setState({ isConfirmRemoveModalOpen: true }); - }; - - onRemoveSelectedConfirmed = () => { - this.props.onRemoveSelected(this.getSelectedIds()); - this.setState({ isConfirmRemoveModalOpen: false }); - }; - - onConfirmRemoveModalClose = () => { - this.setState({ isConfirmRemoveModalOpen: false }); - }; - - onClearBlocklistPress = () => { - this.setState({ isConfirmClearModalOpen: true }); - }; - - onClearBlocklistConfirmed = () => { - this.props.onClearBlocklistPress(); - this.setState({ isConfirmClearModalOpen: false }); - }; - - onConfirmClearModalClose = () => { - this.setState({ isConfirmClearModalOpen: false }); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - items, - columns, - selectedFilterKey, - filters, - customFilters, - totalRecords, - isRemoving, - isClearingBlocklistExecuting, - onFilterSelect, - ...otherProps - } = this.props; - - const { - allSelected, - allUnselected, - selectedState, - isConfirmRemoveModalOpen, - isConfirmClearModalOpen - } = this.state; - - const selectedIds = this.getSelectedIds(); - - return ( - <PageContent title={translate('Blocklist')}> - <PageToolbar> - <PageToolbarSection> - <PageToolbarButton - label={translate('RemoveSelected')} - iconName={icons.REMOVE} - isDisabled={!selectedIds.length} - isSpinning={isRemoving} - onPress={this.onRemoveSelectedPress} - /> - - <PageToolbarButton - label={translate('Clear')} - iconName={icons.CLEAR} - isDisabled={!items.length} - isSpinning={isClearingBlocklistExecuting} - onPress={this.onClearBlocklistPress} - /> - </PageToolbarSection> - - <PageToolbarSection alignContent={align.RIGHT}> - <TableOptionsModalWrapper - {...otherProps} - columns={columns} - > - <PageToolbarButton - label={translate('Options')} - iconName={icons.TABLE} - /> - </TableOptionsModalWrapper> - - <FilterMenu - alignMenu={align.RIGHT} - selectedFilterKey={selectedFilterKey} - filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={BlocklistFilterModal} - onFilterSelect={onFilterSelect} - /> - </PageToolbarSection> - </PageToolbar> - - <PageContentBody> - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - !isFetching && !!error && - <Alert kind={kinds.DANGER}> - {translate('BlocklistLoadError')} - </Alert> - } - - { - isPopulated && !error && !items.length && - <Alert kind={kinds.INFO}> - { - selectedFilterKey === 'all' ? - translate('NoHistoryBlocklist') : - translate('BlocklistFilterHasNoItems') - } - </Alert> - } - - { - isPopulated && !error && !!items.length && - <div> - <Table - selectAll={true} - allSelected={allSelected} - allUnselected={allUnselected} - columns={columns} - {...otherProps} - onSelectAllChange={this.onSelectAllChange} - > - <TableBody> - { - items.map((item) => { - return ( - <BlocklistRowConnector - key={item.id} - isSelected={selectedState[item.id] || false} - columns={columns} - {...item} - onSelectedChange={this.onSelectedChange} - /> - ); - }) - } - </TableBody> - </Table> - - <TablePager - totalRecords={totalRecords} - isFetching={isFetching} - {...otherProps} - /> - </div> - } - </PageContentBody> - - <ConfirmModal - isOpen={isConfirmRemoveModalOpen} - kind={kinds.DANGER} - title={translate('RemoveSelected')} - message={translate('RemoveSelectedBlocklistMessageText')} - confirmLabel={translate('RemoveSelected')} - onConfirm={this.onRemoveSelectedConfirmed} - onCancel={this.onConfirmRemoveModalClose} - /> - - <ConfirmModal - isOpen={isConfirmClearModalOpen} - kind={kinds.DANGER} - title={translate('ClearBlocklist')} - message={translate('ClearBlocklistMessageText')} - confirmLabel={translate('Clear')} - onConfirm={this.onClearBlocklistConfirmed} - onCancel={this.onConfirmClearModalClose} - /> - </PageContent> - ); - } -} - -Blocklist.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - totalRecords: PropTypes.number, - isRemoving: PropTypes.bool.isRequired, - isClearingBlocklistExecuting: PropTypes.bool.isRequired, - onRemoveSelected: PropTypes.func.isRequired, - onClearBlocklistPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired -}; - -export default Blocklist; diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx new file mode 100644 index 000000000..4205ae12e --- /dev/null +++ b/frontend/src/Activity/Blocklist/Blocklist.tsx @@ -0,0 +1,326 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider } from 'App/SelectContext'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import TablePager from 'Components/Table/TablePager'; +import usePaging from 'Components/Table/usePaging'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import useSelectState from 'Helpers/Hooks/useSelectState'; +import { align, icons, kinds } from 'Helpers/Props'; +import { + clearBlocklist, + fetchBlocklist, + gotoBlocklistPage, + removeBlocklistItems, + setBlocklistFilter, + setBlocklistSort, + setBlocklistTableOption, +} from 'Store/Actions/blocklistActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; +import { TableOptionsChangePayload } from 'typings/Table'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import BlocklistFilterModal from './BlocklistFilterModal'; +import BlocklistRow from './BlocklistRow'; + +function Blocklist() { + const requestCurrentPage = useCurrentPage(); + + const { + isFetching, + isPopulated, + error, + items, + columns, + selectedFilterKey, + filters, + sortKey, + sortDirection, + page, + totalPages, + totalRecords, + isRemoving, + } = useSelector((state: AppState) => state.blocklist); + + const customFilters = useSelector(createCustomFiltersSelector('blocklist')); + const isClearingBlocklistExecuting = useSelector( + createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST) + ); + const dispatch = useDispatch(); + + const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = + useState(false); + const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false); + + const [selectState, setSelectState] = useSelectState(); + const { allSelected, allUnselected, selectedState } = selectState; + + const selectedIds = useMemo(() => { + return getSelectedIds(selectedState); + }, [selectedState]); + + const wasClearingBlocklistExecuting = usePrevious( + isClearingBlocklistExecuting + ); + + const handleSelectAllChange = useCallback( + ({ value }: CheckInputChanged) => { + setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + }, + [items, setSelectState] + ); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + setSelectState({ + type: 'toggleSelected', + items, + id, + isSelected: value, + shiftKey, + }); + }, + [items, setSelectState] + ); + + const handleRemoveSelectedPress = useCallback(() => { + setIsConfirmRemoveModalOpen(true); + }, [setIsConfirmRemoveModalOpen]); + + const handleRemoveSelectedConfirmed = useCallback(() => { + dispatch(removeBlocklistItems({ ids: selectedIds })); + setIsConfirmRemoveModalOpen(false); + }, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]); + + const handleConfirmRemoveModalClose = useCallback(() => { + setIsConfirmRemoveModalOpen(false); + }, [setIsConfirmRemoveModalOpen]); + + const handleClearBlocklistPress = useCallback(() => { + setIsConfirmClearModalOpen(true); + }, [setIsConfirmClearModalOpen]); + + const handleClearBlocklistConfirmed = useCallback(() => { + dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST })); + setIsConfirmClearModalOpen(false); + }, [setIsConfirmClearModalOpen, dispatch]); + + const handleConfirmClearModalClose = useCallback(() => { + setIsConfirmClearModalOpen(false); + }, [setIsConfirmClearModalOpen]); + + const { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + } = usePaging({ + page, + totalPages, + gotoPage: gotoBlocklistPage, + }); + + const handleFilterSelect = useCallback( + (selectedFilterKey: string) => { + dispatch(setBlocklistFilter({ selectedFilterKey })); + }, + [dispatch] + ); + + const handleSortPress = useCallback( + (sortKey: string) => { + dispatch(setBlocklistSort({ sortKey })); + }, + [dispatch] + ); + + const handleTableOptionChange = useCallback( + (payload: TableOptionsChangePayload) => { + dispatch(setBlocklistTableOption(payload)); + + if (payload.pageSize) { + dispatch(gotoBlocklistPage({ page: 1 })); + } + }, + [dispatch] + ); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchBlocklist()); + } else { + dispatch(gotoBlocklistPage({ page: 1 })); + } + + return () => { + dispatch(clearBlocklist()); + }; + }, [requestCurrentPage, dispatch]); + + useEffect(() => { + const repopulate = () => { + dispatch(fetchBlocklist()); + }; + + registerPagePopulator(repopulate); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [dispatch]); + + useEffect(() => { + if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) { + dispatch(gotoBlocklistPage({ page: 1 })); + } + }, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]); + + return ( + <SelectProvider items={items}> + <PageContent title={translate('Blocklist')}> + <PageToolbar> + <PageToolbarSection> + <PageToolbarButton + label={translate('RemoveSelected')} + iconName={icons.REMOVE} + isDisabled={!selectedIds.length} + isSpinning={isRemoving} + onPress={handleRemoveSelectedPress} + /> + + <PageToolbarButton + label={translate('Clear')} + iconName={icons.CLEAR} + isDisabled={!items.length} + isSpinning={isClearingBlocklistExecuting} + onPress={handleClearBlocklistPress} + /> + </PageToolbarSection> + + <PageToolbarSection alignContent={align.RIGHT}> + <TableOptionsModalWrapper + columns={columns} + onTableOptionChange={handleTableOptionChange} + > + <PageToolbarButton + label={translate('Options')} + iconName={icons.TABLE} + /> + </TableOptionsModalWrapper> + + <FilterMenu + alignMenu={align.RIGHT} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + filterModalConnectorComponent={BlocklistFilterModal} + onFilterSelect={handleFilterSelect} + /> + </PageToolbarSection> + </PageToolbar> + + <PageContentBody> + {isFetching && !isPopulated ? <LoadingIndicator /> : null} + + {!isFetching && !!error ? ( + <Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert> + ) : null} + + {isPopulated && !error && !items.length ? ( + <Alert kind={kinds.INFO}> + {selectedFilterKey === 'all' + ? translate('NoBlocklistItems') + : translate('BlocklistFilterHasNoItems')} + </Alert> + ) : null} + + {isPopulated && !error && !!items.length ? ( + <div> + <Table + selectAll={true} + allSelected={allSelected} + allUnselected={allUnselected} + columns={columns} + sortKey={sortKey} + sortDirection={sortDirection} + onTableOptionChange={handleTableOptionChange} + onSelectAllChange={handleSelectAllChange} + onSortPress={handleSortPress} + > + <TableBody> + {items.map((item) => { + return ( + <BlocklistRow + key={item.id} + isSelected={selectedState[item.id] || false} + columns={columns} + {...item} + onSelectedChange={handleSelectedChange} + /> + ); + })} + </TableBody> + </Table> + <TablePager + page={page} + totalPages={totalPages} + totalRecords={totalRecords} + isFetching={isFetching} + onFirstPagePress={handleFirstPagePress} + onPreviousPagePress={handlePreviousPagePress} + onNextPagePress={handleNextPagePress} + onLastPagePress={handleLastPagePress} + onPageSelect={handlePageSelect} + /> + </div> + ) : null} + </PageContentBody> + + <ConfirmModal + isOpen={isConfirmRemoveModalOpen} + kind={kinds.DANGER} + title={translate('RemoveSelected')} + message={translate('RemoveSelectedBlocklistMessageText')} + confirmLabel={translate('RemoveSelected')} + onConfirm={handleRemoveSelectedConfirmed} + onCancel={handleConfirmRemoveModalClose} + /> + + <ConfirmModal + isOpen={isConfirmClearModalOpen} + kind={kinds.DANGER} + title={translate('ClearBlocklist')} + message={translate('ClearBlocklistMessageText')} + confirmLabel={translate('Clear')} + onConfirm={handleClearBlocklistConfirmed} + onCancel={handleConfirmClearModalClose} + /> + </PageContent> + </SelectProvider> + ); +} + +export default Blocklist; diff --git a/frontend/src/Activity/Blocklist/BlocklistConnector.js b/frontend/src/Activity/Blocklist/BlocklistConnector.js deleted file mode 100644 index 5eb055a06..000000000 --- a/frontend/src/Activity/Blocklist/BlocklistConnector.js +++ /dev/null @@ -1,161 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import withCurrentPage from 'Components/withCurrentPage'; -import * as blocklistActions from 'Store/Actions/blocklistActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; -import Blocklist from './Blocklist'; - -function createMapStateToProps() { - return createSelector( - (state) => state.blocklist, - createCustomFiltersSelector('blocklist'), - createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST), - (blocklist, customFilters, isClearingBlocklistExecuting) => { - return { - isClearingBlocklistExecuting, - customFilters, - ...blocklist - }; - } - ); -} - -const mapDispatchToProps = { - ...blocklistActions, - executeCommand -}; - -class BlocklistConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - useCurrentPage, - fetchBlocklist, - gotoBlocklistFirstPage - } = this.props; - - registerPagePopulator(this.repopulate); - - if (useCurrentPage) { - fetchBlocklist(); - } else { - gotoBlocklistFirstPage(); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) { - this.props.gotoBlocklistFirstPage(); - } - } - - componentWillUnmount() { - this.props.clearBlocklist(); - unregisterPagePopulator(this.repopulate); - } - - // - // Control - - repopulate = () => { - this.props.fetchBlocklist(); - }; - // - // Listeners - - onFirstPagePress = () => { - this.props.gotoBlocklistFirstPage(); - }; - - onPreviousPagePress = () => { - this.props.gotoBlocklistPreviousPage(); - }; - - onNextPagePress = () => { - this.props.gotoBlocklistNextPage(); - }; - - onLastPagePress = () => { - this.props.gotoBlocklistLastPage(); - }; - - onPageSelect = (page) => { - this.props.gotoBlocklistPage({ page }); - }; - - onRemoveSelected = (ids) => { - this.props.removeBlocklistItems({ ids }); - }; - - onSortPress = (sortKey) => { - this.props.setBlocklistSort({ sortKey }); - }; - - onFilterSelect = (selectedFilterKey) => { - this.props.setBlocklistFilter({ selectedFilterKey }); - }; - - onClearBlocklistPress = () => { - this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST }); - }; - - onTableOptionChange = (payload) => { - this.props.setBlocklistTableOption(payload); - - if (payload.pageSize) { - this.props.gotoBlocklistFirstPage(); - } - }; - - // - // Render - - render() { - return ( - <Blocklist - onFirstPagePress={this.onFirstPagePress} - onPreviousPagePress={this.onPreviousPagePress} - onNextPagePress={this.onNextPagePress} - onLastPagePress={this.onLastPagePress} - onPageSelect={this.onPageSelect} - onRemoveSelected={this.onRemoveSelected} - onSortPress={this.onSortPress} - onFilterSelect={this.onFilterSelect} - onTableOptionChange={this.onTableOptionChange} - onClearBlocklistPress={this.onClearBlocklistPress} - {...this.props} - /> - ); - } -} - -BlocklistConnector.propTypes = { - useCurrentPage: PropTypes.bool.isRequired, - isClearingBlocklistExecuting: PropTypes.bool.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchBlocklist: PropTypes.func.isRequired, - gotoBlocklistFirstPage: PropTypes.func.isRequired, - gotoBlocklistPreviousPage: PropTypes.func.isRequired, - gotoBlocklistNextPage: PropTypes.func.isRequired, - gotoBlocklistLastPage: PropTypes.func.isRequired, - gotoBlocklistPage: PropTypes.func.isRequired, - removeBlocklistItems: PropTypes.func.isRequired, - setBlocklistSort: PropTypes.func.isRequired, - setBlocklistFilter: PropTypes.func.isRequired, - setBlocklistTableOption: PropTypes.func.isRequired, - clearBlocklist: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired -}; - -export default withCurrentPage( - connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector) -); diff --git a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js deleted file mode 100644 index 5f8b98d3d..000000000 --- a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import Button from 'Components/Link/Button'; -import Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import translate from 'Utilities/String/translate'; - -class BlocklistDetailsModal extends Component { - - // - // Render - - render() { - const { - isOpen, - sourceTitle, - protocol, - indexer, - message, - onModalClose - } = this.props; - - return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > - <ModalContent - onModalClose={onModalClose} - > - <ModalHeader> - Details - </ModalHeader> - - <ModalBody> - <DescriptionList> - <DescriptionListItem - title={translate('Name')} - data={sourceTitle} - /> - - <DescriptionListItem - title={translate('Protocol')} - data={protocol} - /> - - { - !!message && - <DescriptionListItem - title={translate('Indexer')} - data={indexer} - /> - } - - { - !!message && - <DescriptionListItem - title={translate('Message')} - data={message} - /> - } - </DescriptionList> - </ModalBody> - - <ModalFooter> - <Button onPress={onModalClose}> - {translate('Close')} - </Button> - </ModalFooter> - </ModalContent> - </Modal> - ); - } -} - -BlocklistDetailsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - sourceTitle: PropTypes.string.isRequired, - protocol: PropTypes.string.isRequired, - indexer: PropTypes.string, - message: PropTypes.string, - onModalClose: PropTypes.func.isRequired -}; - -export default BlocklistDetailsModal; diff --git a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx new file mode 100644 index 000000000..ec026ae92 --- /dev/null +++ b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import translate from 'Utilities/String/translate'; + +interface BlocklistDetailsModalProps { + isOpen: boolean; + sourceTitle: string; + protocol: DownloadProtocol; + indexer?: string; + message?: string; + onModalClose: () => void; +} + +function BlocklistDetailsModal(props: BlocklistDetailsModalProps) { + const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } = + props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <ModalContent onModalClose={onModalClose}> + <ModalHeader>Details</ModalHeader> + + <ModalBody> + <DescriptionList> + <DescriptionListItem title={translate('Name')} data={sourceTitle} /> + + <DescriptionListItem + title={translate('Protocol')} + data={protocol} + /> + + {message ? ( + <DescriptionListItem + title={translate('Indexer')} + data={indexer} + /> + ) : null} + + {message ? ( + <DescriptionListItem + title={translate('Message')} + data={message} + /> + ) : null} + </DescriptionList> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Close')}</Button> + </ModalFooter> + </ModalContent> + </Modal> + ); +} + +export default BlocklistDetailsModal; diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.js b/frontend/src/Activity/Blocklist/BlocklistRow.js deleted file mode 100644 index b6bd2863c..000000000 --- a/frontend/src/Activity/Blocklist/BlocklistRow.js +++ /dev/null @@ -1,212 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import TableRow from 'Components/Table/TableRow'; -import EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import { icons, kinds } from 'Helpers/Props'; -import SeriesTitleLink from 'Series/SeriesTitleLink'; -import translate from 'Utilities/String/translate'; -import BlocklistDetailsModal from './BlocklistDetailsModal'; -import styles from './BlocklistRow.css'; - -class BlocklistRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onDetailsPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - // - // Render - - render() { - const { - id, - series, - sourceTitle, - languages, - quality, - customFormats, - date, - protocol, - indexer, - message, - isSelected, - columns, - onSelectedChange, - onRemovePress - } = this.props; - - return ( - <TableRow> - <TableSelectCell - id={id} - isSelected={isSelected} - onSelectedChange={onSelectedChange} - /> - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'series.sortTitle') { - return ( - <TableRowCell key={name}> - <SeriesTitleLink - titleSlug={series.titleSlug} - title={series.title} - /> - </TableRowCell> - ); - } - - if (name === 'sourceTitle') { - return ( - <TableRowCell key={name}> - {sourceTitle} - </TableRowCell> - ); - } - - if (name === 'languages') { - return ( - <TableRowCell - key={name} - className={styles.languages} - > - <EpisodeLanguages - languages={languages} - /> - </TableRowCell> - ); - } - - if (name === 'quality') { - return ( - <TableRowCell - key={name} - className={styles.quality} - > - <EpisodeQuality - quality={quality} - /> - </TableRowCell> - ); - } - - if (name === 'customFormats') { - return ( - <TableRowCell key={name}> - <EpisodeFormats - formats={customFormats} - /> - </TableRowCell> - ); - } - - if (name === 'date') { - return ( - <RelativeDateCellConnector - key={name} - date={date} - /> - ); - } - - if (name === 'indexer') { - return ( - <TableRowCell - key={name} - className={styles.indexer} - > - {indexer} - </TableRowCell> - ); - } - - if (name === 'actions') { - return ( - <TableRowCell - key={name} - className={styles.actions} - > - <IconButton - name={icons.INFO} - onPress={this.onDetailsPress} - /> - - <IconButton - title={translate('RemoveFromBlocklist')} - name={icons.REMOVE} - kind={kinds.DANGER} - onPress={onRemovePress} - /> - </TableRowCell> - ); - } - - return null; - }) - } - - <BlocklistDetailsModal - isOpen={this.state.isDetailsModalOpen} - sourceTitle={sourceTitle} - protocol={protocol} - indexer={indexer} - message={message} - onModalClose={this.onDetailsModalClose} - /> - </TableRow> - ); - } - -} - -BlocklistRow.propTypes = { - id: PropTypes.number.isRequired, - series: PropTypes.object.isRequired, - sourceTitle: PropTypes.string.isRequired, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - quality: PropTypes.object.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object).isRequired, - date: PropTypes.string.isRequired, - protocol: PropTypes.string.isRequired, - indexer: PropTypes.string, - message: PropTypes.string, - isSelected: PropTypes.bool.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onSelectedChange: PropTypes.func.isRequired, - onRemovePress: PropTypes.func.isRequired -}; - -export default BlocklistRow; diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.tsx b/frontend/src/Activity/Blocklist/BlocklistRow.tsx new file mode 100644 index 000000000..58d75b1dd --- /dev/null +++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx @@ -0,0 +1,163 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import { icons, kinds } from 'Helpers/Props'; +import SeriesTitleLink from 'Series/SeriesTitleLink'; +import useSeries from 'Series/useSeries'; +import { removeBlocklistItem } from 'Store/Actions/blocklistActions'; +import Blocklist from 'typings/Blocklist'; +import { SelectStateInputProps } from 'typings/props'; +import translate from 'Utilities/String/translate'; +import BlocklistDetailsModal from './BlocklistDetailsModal'; +import styles from './BlocklistRow.css'; + +interface BlocklistRowProps extends Blocklist { + isSelected: boolean; + columns: Column[]; + onSelectedChange: (options: SelectStateInputProps) => void; +} + +function BlocklistRow(props: BlocklistRowProps) { + const { + id, + seriesId, + sourceTitle, + languages, + quality, + customFormats, + date, + protocol, + indexer, + message, + isSelected, + columns, + onSelectedChange, + } = props; + + const series = useSeries(seriesId); + const dispatch = useDispatch(); + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const handleDetailsPress = useCallback(() => { + setIsDetailsModalOpen(true); + }, [setIsDetailsModalOpen]); + + const handleDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(false); + }, [setIsDetailsModalOpen]); + + const handleRemovePress = useCallback(() => { + dispatch(removeBlocklistItem({ id })); + }, [id, dispatch]); + + if (!series) { + return null; + } + + return ( + <TableRow> + <TableSelectCell + id={id} + isSelected={isSelected} + onSelectedChange={onSelectedChange} + /> + + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'series.sortTitle') { + return ( + <TableRowCell key={name}> + <SeriesTitleLink + titleSlug={series.titleSlug} + title={series.title} + /> + </TableRowCell> + ); + } + + if (name === 'sourceTitle') { + return <TableRowCell key={name}>{sourceTitle}</TableRowCell>; + } + + if (name === 'languages') { + return ( + <TableRowCell key={name} className={styles.languages}> + <EpisodeLanguages languages={languages} /> + </TableRowCell> + ); + } + + if (name === 'quality') { + return ( + <TableRowCell key={name} className={styles.quality}> + <EpisodeQuality quality={quality} /> + </TableRowCell> + ); + } + + if (name === 'customFormats') { + return ( + <TableRowCell key={name}> + <EpisodeFormats formats={customFormats} /> + </TableRowCell> + ); + } + + if (name === 'date') { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts(2739) + return <RelativeDateCellConnector key={name} date={date} />; + } + + if (name === 'indexer') { + return ( + <TableRowCell key={name} className={styles.indexer}> + {indexer} + </TableRowCell> + ); + } + + if (name === 'actions') { + return ( + <TableRowCell key={name} className={styles.actions}> + <IconButton name={icons.INFO} onPress={handleDetailsPress} /> + + <IconButton + title={translate('RemoveFromBlocklist')} + name={icons.REMOVE} + kind={kinds.DANGER} + onPress={handleRemovePress} + /> + </TableRowCell> + ); + } + + return null; + })} + + <BlocklistDetailsModal + isOpen={isDetailsModalOpen} + sourceTitle={sourceTitle} + protocol={protocol} + indexer={indexer} + message={message} + onModalClose={handleDetailsModalClose} + /> + </TableRow> + ); +} + +export default BlocklistRow; diff --git a/frontend/src/Activity/Blocklist/BlocklistRowConnector.js b/frontend/src/Activity/Blocklist/BlocklistRowConnector.js deleted file mode 100644 index f0b93cd25..000000000 --- a/frontend/src/Activity/Blocklist/BlocklistRowConnector.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { removeBlocklistItem } from 'Store/Actions/blocklistActions'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import BlocklistRow from './BlocklistRow'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - (series) => { - return { - series - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onRemovePress() { - dispatch(removeBlocklistItem({ id: props.id })); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow); diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 34e23ac3f..e7f8a37ff 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Redirect, Route } from 'react-router-dom'; -import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector'; +import Blocklist from 'Activity/Blocklist/Blocklist'; import HistoryConnector from 'Activity/History/HistoryConnector'; import QueueConnector from 'Activity/Queue/QueueConnector'; import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector'; @@ -135,7 +135,7 @@ function AppRoutes(props) { <Route path="/activity/blocklist" - component={BlocklistConnector} + component={Blocklist} /> {/* diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 30af90d34..f89eb25f7 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,5 +1,6 @@ +import Column from 'Components/Table/Column'; import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp } from './AppState'; +import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { responseJSON: { @@ -18,11 +19,18 @@ export interface AppSectionSaveState { } export interface PagedAppSectionState { + page: number; pageSize: number; + totalPages: number; totalRecords?: number; } +export interface TableAppSectionState { + columns: Column[]; +} export interface AppSectionFilterState<T> { + selectedFilterKey: string; + filters: PropertyFilter[]; filterBuilderProps: FilterBuilderProp<T>[]; } diff --git a/frontend/src/App/State/BlocklistAppState.ts b/frontend/src/App/State/BlocklistAppState.ts index e838ad625..004a30732 100644 --- a/frontend/src/App/State/BlocklistAppState.ts +++ b/frontend/src/App/State/BlocklistAppState.ts @@ -1,8 +1,16 @@ import Blocklist from 'typings/Blocklist'; -import AppSectionState, { AppSectionFilterState } from './AppSectionState'; +import AppSectionState, { + AppSectionFilterState, + PagedAppSectionState, + TableAppSectionState, +} from './AppSectionState'; interface BlocklistAppState extends AppSectionState<Blocklist>, - AppSectionFilterState<Blocklist> {} + AppSectionFilterState<Blocklist>, + PagedAppSectionState, + TableAppSectionState { + isRemoving: boolean; +} export default BlocklistAppState; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 31a696df7..f5644357b 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -2,6 +2,7 @@ import React from 'react'; type PropertyFunction<T> = () => T; +// TODO: Convert to generic so `name` can be a type interface Column { name: string; label: string | PropertyFunction<string> | React.ReactNode; diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts new file mode 100644 index 000000000..dfebb2355 --- /dev/null +++ b/frontend/src/Components/Table/usePaging.ts @@ -0,0 +1,54 @@ +import { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +interface PagingOptions { + page: number; + totalPages: number; + gotoPage: ({ page }: { page: number }) => void; +} + +function usePaging(options: PagingOptions) { + const { page, totalPages, gotoPage } = options; + const dispatch = useDispatch(); + + const handleFirstPagePress = useCallback(() => { + dispatch(gotoPage({ page: 1 })); + }, [dispatch, gotoPage]); + + const handlePreviousPagePress = useCallback(() => { + dispatch(gotoPage({ page: Math.max(page - 1, 1) })); + }, [page, dispatch, gotoPage]); + + const handleNextPagePress = useCallback(() => { + dispatch(gotoPage({ page: Math.min(page + 1, totalPages) })); + }, [page, totalPages, dispatch, gotoPage]); + + const handleLastPagePress = useCallback(() => { + dispatch(gotoPage({ page: totalPages })); + }, [totalPages, dispatch, gotoPage]); + + const handlePageSelect = useCallback( + (page: number) => { + dispatch(gotoPage({ page })); + }, + [dispatch, gotoPage] + ); + + return useMemo(() => { + return { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + }; + }, [ + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + ]); +} + +export default usePaging; diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts index 090a1a087..417db8178 100644 --- a/frontend/src/DownloadClient/DownloadProtocol.ts +++ b/frontend/src/DownloadClient/DownloadProtocol.ts @@ -1,7 +1,3 @@ -enum DownloadProtocol { - Unknown = 'unknown', - Usenet = 'usenet', - Torrent = 'torrent', -} +type DownloadProtocol = 'usenet' | 'torrent' | 'unknown'; export default DownloadProtocol; diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts new file mode 100644 index 000000000..3caf66df2 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useCurrentPage.ts @@ -0,0 +1,9 @@ +import { useHistory } from 'react-router-dom'; + +function useCurrentPage() { + const history = useHistory(); + + return history.action === 'POP'; +} + +export default useCurrentPage; diff --git a/frontend/src/Series/useSeries.ts b/frontend/src/Series/useSeries.ts new file mode 100644 index 000000000..073f41541 --- /dev/null +++ b/frontend/src/Series/useSeries.ts @@ -0,0 +1,19 @@ +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +export function createSeriesSelector(seriesId?: number) { + return createSelector( + (state: AppState) => state.series.itemMap, + (state: AppState) => state.series.items, + (itemMap, allSeries) => { + return seriesId ? allSeries[itemMap[seriesId]] : undefined; + } + ); +} + +function useSeries(seriesId?: number) { + return useSelector(createSeriesSelector(seriesId)); +} + +export default useSeries; diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js index 6303ad2d1..87ffe7f7c 100644 --- a/frontend/src/Store/Actions/blocklistActions.js +++ b/frontend/src/Store/Actions/blocklistActions.js @@ -117,10 +117,6 @@ export const persistState = [ // Action Types export const FETCH_BLOCKLIST = 'blocklist/fetchBlocklist'; -export const GOTO_FIRST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistFirstPage'; -export const GOTO_PREVIOUS_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPreviousPage'; -export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage'; -export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage'; export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage'; export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort'; export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter'; @@ -133,10 +129,6 @@ export const CLEAR_BLOCKLIST = 'blocklist/clearBlocklist'; // Action Creators export const fetchBlocklist = createThunk(FETCH_BLOCKLIST); -export const gotoBlocklistFirstPage = createThunk(GOTO_FIRST_BLOCKLIST_PAGE); -export const gotoBlocklistPreviousPage = createThunk(GOTO_PREVIOUS_BLOCKLIST_PAGE); -export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE); -export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE); export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE); export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT); export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER); @@ -155,10 +147,6 @@ export const actionHandlers = handleThunks({ fetchBlocklist, { [serverSideCollectionHandlers.FETCH]: FETCH_BLOCKLIST, - [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_BLOCKLIST_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_BLOCKLIST_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE, [serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT, [serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER diff --git a/frontend/src/typings/Blocklist.ts b/frontend/src/typings/Blocklist.ts index 4cc675cc5..bbf4cacae 100644 --- a/frontend/src/typings/Blocklist.ts +++ b/frontend/src/typings/Blocklist.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; @@ -9,8 +10,11 @@ interface Blocklist extends ModelBase { customFormats: CustomFormat[]; title: string; date?: string; - protocol: string; + protocol: DownloadProtocol; + sourceTitle: string; seriesId?: number; + indexer?: string; + message?: string; } export default Blocklist; diff --git a/frontend/src/typings/Table.ts b/frontend/src/typings/Table.ts new file mode 100644 index 000000000..4f99e2045 --- /dev/null +++ b/frontend/src/typings/Table.ts @@ -0,0 +1,6 @@ +import Column from 'Components/Table/Column'; + +export interface TableOptionsChangePayload { + pageSize?: number; + columns: Column[]; +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 42ad9dc5c..01456d5b0 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1248,6 +1248,7 @@ "NextExecution": "Next Execution", "No": "No", "NoBackupsAreAvailable": "No backups are available", + "NoBlocklistItems": "No blocklist items", "NoChange": "No Change", "NoChanges": "No Changes", "NoDelay": "No Delay", @@ -1259,7 +1260,6 @@ "NoEpisodesInThisSeason": "No episodes in this season", "NoEventsFound": "No events found", "NoHistory": "No history", - "NoHistoryBlocklist": "No history blocklist", "NoHistoryFound": "No history found", "NoImportListsFound": "No import lists found", "NoIndexersFound": "No indexers found", From 824ed0a36931ce7aae9aa544a7baf0738dae568c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 19 Jul 2024 20:42:59 -0700 Subject: [PATCH 412/762] Convert History to TypeScript --- .../src/Activity/Blocklist/BlocklistRow.tsx | 4 +- .../History/Details/HistoryDetails.js | 354 ------------------ .../History/Details/HistoryDetails.tsx | 289 ++++++++++++++ .../Details/HistoryDetailsConnector.js | 19 - ...etailsModal.js => HistoryDetailsModal.tsx} | 72 ++-- frontend/src/Activity/History/History.js | 180 --------- frontend/src/Activity/History/History.tsx | 228 +++++++++++ .../src/Activity/History/HistoryConnector.js | 165 -------- ...ntTypeCell.js => HistoryEventTypeCell.tsx} | 52 +-- frontend/src/Activity/History/HistoryRow.js | 312 --------------- frontend/src/Activity/History/HistoryRow.tsx | 275 ++++++++++++++ .../Activity/History/HistoryRowConnector.js | 76 ---- frontend/src/Activity/Queue/QueueRow.js | 6 +- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppState.ts | 1 + frontend/src/App/State/HistoryAppState.ts | 6 +- .../Table/Cells/RelativeDateCell.js | 69 ---- .../Table/Cells/RelativeDateCell.tsx | 57 +++ .../Table/Cells/RelativeDateCellConnector.js | 21 -- frontend/src/Episode/EpisodeNumber.js | 143 ------- frontend/src/Episode/EpisodeNumber.tsx | 140 +++++++ .../{EpisodeQuality.js => EpisodeQuality.tsx} | 48 +-- frontend/src/Episode/EpisodeTitleLink.tsx | 10 +- .../src/Episode/History/EpisodeHistoryRow.js | 8 +- frontend/src/Episode/SeasonEpisodeNumber.js | 32 -- frontend/src/Episode/SeasonEpisodeNumber.tsx | 26 ++ .../Episode/createEpisodesFetchingSelector.ts | 17 + frontend/src/Episode/useEpisode.ts | 44 +++ .../Folder/RecentFolderRow.js | 4 +- frontend/src/Series/Details/EpisodeRow.js | 4 +- .../src/Series/History/SeriesHistoryRow.js | 8 +- .../src/Series/Index/Table/SeriesIndexRow.tsx | 8 +- frontend/src/System/Backup/BackupRow.js | 4 +- frontend/src/System/Events/LogsTableRow.js | 4 +- .../src/System/Logs/Files/LogFilesTableRow.js | 4 +- .../src/Utilities/Object/selectUniqueIds.js | 15 - .../src/Utilities/Object/selectUniqueIds.ts | 13 + .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 4 +- frontend/src/Wanted/Missing/MissingRow.js | 4 +- frontend/src/typings/Helpers/KeysMatching.ts | 2 +- frontend/src/typings/History.ts | 59 ++- 41 files changed, 1276 insertions(+), 1515 deletions(-) delete mode 100644 frontend/src/Activity/History/Details/HistoryDetails.js create mode 100644 frontend/src/Activity/History/Details/HistoryDetails.tsx delete mode 100644 frontend/src/Activity/History/Details/HistoryDetailsConnector.js rename frontend/src/Activity/History/Details/{HistoryDetailsModal.js => HistoryDetailsModal.tsx} (58%) delete mode 100644 frontend/src/Activity/History/History.js create mode 100644 frontend/src/Activity/History/History.tsx delete mode 100644 frontend/src/Activity/History/HistoryConnector.js rename frontend/src/Activity/History/{HistoryEventTypeCell.js => HistoryEventTypeCell.tsx} (60%) delete mode 100644 frontend/src/Activity/History/HistoryRow.js create mode 100644 frontend/src/Activity/History/HistoryRow.tsx delete mode 100644 frontend/src/Activity/History/HistoryRowConnector.js delete mode 100644 frontend/src/Components/Table/Cells/RelativeDateCell.js create mode 100644 frontend/src/Components/Table/Cells/RelativeDateCell.tsx delete mode 100644 frontend/src/Components/Table/Cells/RelativeDateCellConnector.js delete mode 100644 frontend/src/Episode/EpisodeNumber.js create mode 100644 frontend/src/Episode/EpisodeNumber.tsx rename frontend/src/Episode/{EpisodeQuality.js => EpisodeQuality.tsx} (69%) delete mode 100644 frontend/src/Episode/SeasonEpisodeNumber.js create mode 100644 frontend/src/Episode/SeasonEpisodeNumber.tsx create mode 100644 frontend/src/Episode/createEpisodesFetchingSelector.ts create mode 100644 frontend/src/Episode/useEpisode.ts delete mode 100644 frontend/src/Utilities/Object/selectUniqueIds.js create mode 100644 frontend/src/Utilities/Object/selectUniqueIds.ts diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.tsx b/frontend/src/Activity/Blocklist/BlocklistRow.tsx index 58d75b1dd..c7410320d 100644 --- a/frontend/src/Activity/Blocklist/BlocklistRow.tsx +++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import IconButton from 'Components/Link/IconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; @@ -119,7 +119,7 @@ function BlocklistRow(props: BlocklistRowProps) { if (name === 'date') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ts(2739) - return <RelativeDateCellConnector key={name} date={date} />; + return <RelativeDateCell key={name} date={date} />; } if (name === 'indexer') { diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js deleted file mode 100644 index 862d8707e..000000000 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ /dev/null @@ -1,354 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; -import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; -import Link from 'Components/Link/Link'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatAge from 'Utilities/Number/formatAge'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; -import styles from './HistoryDetails.css'; - -function HistoryDetails(props) { - const { - eventType, - sourceTitle, - data, - downloadId, - shortDateFormat, - timeFormat - } = props; - - if (eventType === 'grabbed') { - const { - indexer, - releaseGroup, - seriesMatchType, - customFormatScore, - nzbInfoUrl, - downloadClient, - downloadClientName, - age, - ageHours, - ageMinutes, - publishedDate - } = data; - - const downloadClientNameInfo = downloadClientName ?? downloadClient; - - return ( - <DescriptionList> - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Name')} - data={sourceTitle} - /> - - { - indexer ? - <DescriptionListItem - title={translate('Indexer')} - data={indexer} - /> : - null - } - - { - releaseGroup ? - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('ReleaseGroup')} - data={releaseGroup} - /> : - null - } - - { - customFormatScore && customFormatScore !== '0' ? - <DescriptionListItem - title={translate('CustomFormatScore')} - data={formatCustomFormatScore(customFormatScore)} - /> : - null - } - - { - seriesMatchType ? - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('SeriesMatchType')} - data={seriesMatchType} - /> : - null - } - - { - nzbInfoUrl ? - <span> - <DescriptionListItemTitle> - {translate('InfoUrl')} - </DescriptionListItemTitle> - - <DescriptionListItemDescription> - <Link to={nzbInfoUrl}>{nzbInfoUrl}</Link> - </DescriptionListItemDescription> - </span> : - null - } - - { - downloadClientNameInfo ? - <DescriptionListItem - title={translate('DownloadClient')} - data={downloadClientNameInfo} - /> : - null - } - - { - downloadId ? - <DescriptionListItem - title={translate('GrabId')} - data={downloadId} - /> : - null - } - - { - age || ageHours || ageMinutes ? - <DescriptionListItem - title={translate('AgeWhenGrabbed')} - data={formatAge(age, ageHours, ageMinutes)} - /> : - null - } - - { - publishedDate ? - <DescriptionListItem - title={translate('PublishedDate')} - data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })} - /> : - null - } - </DescriptionList> - ); - } - - if (eventType === 'downloadFailed') { - const { - message - } = data; - - return ( - <DescriptionList> - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Name')} - data={sourceTitle} - /> - - { - downloadId ? - <DescriptionListItem - title={translate('GrabId')} - data={downloadId} - /> : - null - } - - { - message ? - <DescriptionListItem - title={translate('Message')} - data={message} - /> : - null - } - </DescriptionList> - ); - } - - if (eventType === 'downloadFolderImported') { - const { - customFormatScore, - droppedPath, - importedPath - } = data; - - return ( - <DescriptionList> - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Name')} - data={sourceTitle} - /> - - { - droppedPath ? - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Source')} - data={droppedPath} - /> : - null - } - - { - importedPath ? - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('ImportedTo')} - data={importedPath} - /> : - null - } - - { - customFormatScore && customFormatScore !== '0' ? - <DescriptionListItem - title={translate('CustomFormatScore')} - data={formatCustomFormatScore(customFormatScore)} - /> : - null - } - </DescriptionList> - ); - } - - if (eventType === 'episodeFileDeleted') { - const { - reason, - customFormatScore - } = data; - - let reasonMessage = ''; - - switch (reason) { - case 'Manual': - reasonMessage = translate('DeletedReasonManual'); - break; - case 'MissingFromDisk': - reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk'); - break; - case 'Upgrade': - reasonMessage = translate('DeletedReasonUpgrade'); - break; - default: - reasonMessage = ''; - } - - return ( - <DescriptionList> - <DescriptionListItem - title={translate('Name')} - data={sourceTitle} - /> - - <DescriptionListItem - title={translate('Reason')} - data={reasonMessage} - /> - - { - customFormatScore && customFormatScore !== '0' ? - <DescriptionListItem - title={translate('CustomFormatScore')} - data={formatCustomFormatScore(customFormatScore)} - /> : - null - } - </DescriptionList> - ); - } - - if (eventType === 'episodeFileRenamed') { - const { - sourcePath, - sourceRelativePath, - path, - relativePath - } = data; - - return ( - <DescriptionList> - <DescriptionListItem - title={translate('SourcePath')} - data={sourcePath} - /> - - <DescriptionListItem - title={translate('SourceRelativePath')} - data={sourceRelativePath} - /> - - <DescriptionListItem - title={translate('DestinationPath')} - data={path} - /> - - <DescriptionListItem - title={translate('DestinationRelativePath')} - data={relativePath} - /> - </DescriptionList> - ); - } - - if (eventType === 'downloadIgnored') { - const { - message - } = data; - - return ( - <DescriptionList> - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Name')} - data={sourceTitle} - /> - - { - downloadId ? - <DescriptionListItem - title={translate('GrabId')} - data={downloadId} - /> : - null - } - - { - message ? - <DescriptionListItem - title={translate('Message')} - data={message} - /> : - null - } - </DescriptionList> - ); - } - - return ( - <DescriptionList> - <DescriptionListItem - descriptionClassName={styles.description} - title={translate('Name')} - data={sourceTitle} - /> - </DescriptionList> - ); -} - -HistoryDetails.propTypes = { - eventType: PropTypes.string.isRequired, - sourceTitle: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, - downloadId: PropTypes.string, - shortDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired -}; - -export default HistoryDetails; diff --git a/frontend/src/Activity/History/Details/HistoryDetails.tsx b/frontend/src/Activity/History/Details/HistoryDetails.tsx new file mode 100644 index 000000000..d4c8f9f4f --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetails.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import Link from 'Components/Link/Link'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { + DownloadFailedHistory, + DownloadFolderImportedHistory, + DownloadIgnoredHistory, + EpisodeFileDeletedHistory, + EpisodeFileRenamedHistory, + GrabbedHistoryData, + HistoryData, + HistoryEventType, +} from 'typings/History'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import translate from 'Utilities/String/translate'; +import styles from './HistoryDetails.css'; + +interface HistoryDetailsProps { + eventType: HistoryEventType; + sourceTitle: string; + data: HistoryData; + downloadId?: string; + shortDateFormat: string; + timeFormat: string; +} + +function HistoryDetails(props: HistoryDetailsProps) { + const { eventType, sourceTitle, data, downloadId } = props; + + const { shortDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + + if (eventType === 'grabbed') { + const { + indexer, + releaseGroup, + seriesMatchType, + customFormatScore, + nzbInfoUrl, + downloadClient, + downloadClientName, + age, + ageHours, + ageMinutes, + publishedDate, + } = data as GrabbedHistoryData; + + const downloadClientNameInfo = downloadClientName ?? downloadClient; + + return ( + <DescriptionList> + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Name')} + data={sourceTitle} + /> + + {indexer ? ( + <DescriptionListItem title={translate('Indexer')} data={indexer} /> + ) : null} + + {releaseGroup ? ( + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('ReleaseGroup')} + data={releaseGroup} + /> + ) : null} + + {customFormatScore && customFormatScore !== '0' ? ( + <DescriptionListItem + title={translate('CustomFormatScore')} + data={formatCustomFormatScore(parseInt(customFormatScore))} + /> + ) : null} + + {seriesMatchType ? ( + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('SeriesMatchType')} + data={seriesMatchType} + /> + ) : null} + + {nzbInfoUrl ? ( + <span> + <DescriptionListItemTitle> + {translate('InfoUrl')} + </DescriptionListItemTitle> + + <DescriptionListItemDescription> + <Link to={nzbInfoUrl}>{nzbInfoUrl}</Link> + </DescriptionListItemDescription> + </span> + ) : null} + + {downloadClientNameInfo ? ( + <DescriptionListItem + title={translate('DownloadClient')} + data={downloadClientNameInfo} + /> + ) : null} + + {downloadId ? ( + <DescriptionListItem title={translate('GrabId')} data={downloadId} /> + ) : null} + + {age || ageHours || ageMinutes ? ( + <DescriptionListItem + title={translate('AgeWhenGrabbed')} + data={formatAge(age, ageHours, ageMinutes)} + /> + ) : null} + + {publishedDate ? ( + <DescriptionListItem + title={translate('PublishedDate')} + data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { + includeSeconds: true, + })} + /> + ) : null} + </DescriptionList> + ); + } + + if (eventType === 'downloadFailed') { + const { message } = data as DownloadFailedHistory; + + return ( + <DescriptionList> + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Name')} + data={sourceTitle} + /> + + {downloadId ? ( + <DescriptionListItem title={translate('GrabId')} data={downloadId} /> + ) : null} + + {message ? ( + <DescriptionListItem title={translate('Message')} data={message} /> + ) : null} + </DescriptionList> + ); + } + + if (eventType === 'downloadFolderImported') { + const { customFormatScore, droppedPath, importedPath } = + data as DownloadFolderImportedHistory; + + return ( + <DescriptionList> + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Name')} + data={sourceTitle} + /> + + {droppedPath ? ( + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Source')} + data={droppedPath} + /> + ) : null} + + {importedPath ? ( + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('ImportedTo')} + data={importedPath} + /> + ) : null} + + {customFormatScore && customFormatScore !== '0' ? ( + <DescriptionListItem + title={translate('CustomFormatScore')} + data={formatCustomFormatScore(parseInt(customFormatScore))} + /> + ) : null} + </DescriptionList> + ); + } + + if (eventType === 'episodeFileDeleted') { + const { reason, customFormatScore } = data as EpisodeFileDeletedHistory; + + let reasonMessage = ''; + + switch (reason) { + case 'Manual': + reasonMessage = translate('DeletedReasonManual'); + break; + case 'MissingFromDisk': + reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk'); + break; + case 'Upgrade': + reasonMessage = translate('DeletedReasonUpgrade'); + break; + default: + reasonMessage = ''; + } + + return ( + <DescriptionList> + <DescriptionListItem title={translate('Name')} data={sourceTitle} /> + + <DescriptionListItem title={translate('Reason')} data={reasonMessage} /> + + {customFormatScore && customFormatScore !== '0' ? ( + <DescriptionListItem + title={translate('CustomFormatScore')} + data={formatCustomFormatScore(parseInt(customFormatScore))} + /> + ) : null} + </DescriptionList> + ); + } + + if (eventType === 'episodeFileRenamed') { + const { sourcePath, sourceRelativePath, path, relativePath } = + data as EpisodeFileRenamedHistory; + + return ( + <DescriptionList> + <DescriptionListItem + title={translate('SourcePath')} + data={sourcePath} + /> + + <DescriptionListItem + title={translate('SourceRelativePath')} + data={sourceRelativePath} + /> + + <DescriptionListItem title={translate('DestinationPath')} data={path} /> + + <DescriptionListItem + title={translate('DestinationRelativePath')} + data={relativePath} + /> + </DescriptionList> + ); + } + + if (eventType === 'downloadIgnored') { + const { message } = data as DownloadIgnoredHistory; + + return ( + <DescriptionList> + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Name')} + data={sourceTitle} + /> + + {downloadId ? ( + <DescriptionListItem title={translate('GrabId')} data={downloadId} /> + ) : null} + + {message ? ( + <DescriptionListItem title={translate('Message')} data={message} /> + ) : null} + </DescriptionList> + ); + } + + return ( + <DescriptionList> + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('Name')} + data={sourceTitle} + /> + </DescriptionList> + ); +} + +export default HistoryDetails; diff --git a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js deleted file mode 100644 index 0848c7905..000000000 --- a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js +++ /dev/null @@ -1,19 +0,0 @@ -import _ from 'lodash'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import HistoryDetails from './HistoryDetails'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return _.pick(uiSettings, [ - 'shortDateFormat', - 'timeFormat' - ]); - } - ); -} - -export default connect(createMapStateToProps)(HistoryDetails); diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx similarity index 58% rename from frontend/src/Activity/History/Details/HistoryDetailsModal.js rename to frontend/src/Activity/History/Details/HistoryDetailsModal.tsx index ddeea5b78..a833bca5b 100644 --- a/frontend/src/Activity/History/Details/HistoryDetailsModal.js +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; @@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; +import { HistoryData, HistoryEventType } from 'typings/History'; import translate from 'Utilities/String/translate'; import HistoryDetails from './HistoryDetails'; import styles from './HistoryDetailsModal.css'; -function getHeaderTitle(eventType) { +function getHeaderTitle(eventType: HistoryEventType) { switch (eventType) { case 'grabbed': return translate('Grabbed'); @@ -31,7 +31,20 @@ function getHeaderTitle(eventType) { } } -function HistoryDetailsModal(props) { +interface HistoryDetailsModalProps { + isOpen: boolean; + eventType: HistoryEventType; + sourceTitle: string; + data: HistoryData; + downloadId?: string; + isMarkingAsFailed: boolean; + shortDateFormat: string; + timeFormat: string; + onMarkAsFailedPress: () => void; + onModalClose: () => void; +} + +function HistoryDetailsModal(props: HistoryDetailsModalProps) { const { isOpen, eventType, @@ -42,18 +55,13 @@ function HistoryDetailsModal(props) { shortDateFormat, timeFormat, onMarkAsFailedPress, - onModalClose + onModalClose, } = props; return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > + <Modal isOpen={isOpen} onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {getHeaderTitle(eventType)} - </ModalHeader> + <ModalHeader>{getHeaderTitle(eventType)}</ModalHeader> <ModalBody> <HistoryDetails @@ -67,44 +75,26 @@ function HistoryDetailsModal(props) { </ModalBody> <ModalFooter> - { - eventType === 'grabbed' && - <SpinnerButton - className={styles.markAsFailedButton} - kind={kinds.DANGER} - isSpinning={isMarkingAsFailed} - onPress={onMarkAsFailedPress} - > - {translate('MarkAsFailed')} - </SpinnerButton> - } + {eventType === 'grabbed' && ( + <SpinnerButton + className={styles.markAsFailedButton} + kind={kinds.DANGER} + isSpinning={isMarkingAsFailed} + onPress={onMarkAsFailedPress} + > + {translate('MarkAsFailed')} + </SpinnerButton> + )} - <Button - onPress={onModalClose} - > - {translate('Close')} - </Button> + <Button onPress={onModalClose}>{translate('Close')}</Button> </ModalFooter> </ModalContent> </Modal> ); } -HistoryDetailsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - eventType: PropTypes.string.isRequired, - sourceTitle: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, - downloadId: PropTypes.string, - isMarkingAsFailed: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - HistoryDetailsModal.defaultProps = { - isMarkingAsFailed: false + isMarkingAsFailed: false, }; export default HistoryDetailsModal; diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js deleted file mode 100644 index e5cc31ecd..000000000 --- a/frontend/src/Activity/History/History.js +++ /dev/null @@ -1,180 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import TablePager from 'Components/Table/TablePager'; -import { align, icons, kinds } from 'Helpers/Props'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import translate from 'Utilities/String/translate'; -import HistoryFilterModal from './HistoryFilterModal'; -import HistoryRowConnector from './HistoryRowConnector'; - -class History extends Component { - - // - // Lifecycle - - shouldComponentUpdate(nextProps) { - // Don't update when fetching has completed if items have changed, - // before episodes start fetching or when episodes start fetching. - - if ( - ( - this.props.isFetching && - nextProps.isPopulated && - hasDifferentItems(this.props.items, nextProps.items) - ) || - (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) - ) { - return false; - } - - return true; - } - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - items, - columns, - selectedFilterKey, - filters, - customFilters, - totalRecords, - isEpisodesFetching, - isEpisodesPopulated, - episodesError, - onFilterSelect, - onFirstPagePress, - ...otherProps - } = this.props; - - const isFetchingAny = isFetching || isEpisodesFetching; - const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length); - const hasError = error || episodesError; - - return ( - <PageContent title={translate('History')}> - <PageToolbar> - <PageToolbarSection> - <PageToolbarButton - label={translate('Refresh')} - iconName={icons.REFRESH} - isSpinning={isFetching} - onPress={onFirstPagePress} - /> - </PageToolbarSection> - - <PageToolbarSection alignContent={align.RIGHT}> - <TableOptionsModalWrapper - {...otherProps} - columns={columns} - > - <PageToolbarButton - label={translate('Options')} - iconName={icons.TABLE} - /> - </TableOptionsModalWrapper> - - <FilterMenu - alignMenu={align.RIGHT} - selectedFilterKey={selectedFilterKey} - filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={HistoryFilterModal} - onFilterSelect={onFilterSelect} - /> - </PageToolbarSection> - </PageToolbar> - - <PageContentBody> - { - isFetchingAny && !isAllPopulated && - <LoadingIndicator /> - } - - { - !isFetchingAny && hasError && - <Alert kind={kinds.DANGER}> - {translate('HistoryLoadError')} - </Alert> - } - - { - // If history isPopulated and it's empty show no history found and don't - // wait for the episodes to populate because they are never coming. - - isPopulated && !hasError && !items.length && - <Alert kind={kinds.INFO}> - {translate('NoHistoryFound')} - </Alert> - } - - { - isAllPopulated && !hasError && !!items.length && - <div> - <Table - columns={columns} - {...otherProps} - > - <TableBody> - { - items.map((item) => { - return ( - <HistoryRowConnector - key={item.id} - columns={columns} - {...item} - /> - ); - }) - } - </TableBody> - </Table> - - <TablePager - totalRecords={totalRecords} - isFetching={isFetchingAny} - onFirstPagePress={onFirstPagePress} - {...otherProps} - /> - </div> - } - </PageContentBody> - </PageContent> - ); - } -} - -History.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - totalRecords: PropTypes.number, - isEpisodesFetching: PropTypes.bool.isRequired, - isEpisodesPopulated: PropTypes.bool.isRequired, - episodesError: PropTypes.object, - onFilterSelect: PropTypes.func.isRequired, - onFirstPagePress: PropTypes.func.isRequired -}; - -export default History; diff --git a/frontend/src/Activity/History/History.tsx b/frontend/src/Activity/History/History.tsx new file mode 100644 index 000000000..1020d90ea --- /dev/null +++ b/frontend/src/Activity/History/History.tsx @@ -0,0 +1,228 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import TablePager from 'Components/Table/TablePager'; +import usePaging from 'Components/Table/usePaging'; +import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import { align, icons, kinds } from 'Helpers/Props'; +import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; +import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; +import { + clearHistory, + fetchHistory, + gotoHistoryPage, + setHistoryFilter, + setHistorySort, + setHistoryTableOption, +} from 'Store/Actions/historyActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; +import HistoryItem from 'typings/History'; +import { TableOptionsChangePayload } from 'typings/Table'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import HistoryFilterModal from './HistoryFilterModal'; +import HistoryRow from './HistoryRow'; + +function History() { + const requestCurrentPage = useCurrentPage(); + + const { + isFetching, + isPopulated, + error, + items, + columns, + selectedFilterKey, + filters, + sortKey, + sortDirection, + page, + totalPages, + totalRecords, + } = useSelector((state: AppState) => state.history); + + const { isEpisodesFetching, isEpisodesPopulated, episodesError } = + useSelector(createEpisodesFetchingSelector()); + const customFilters = useSelector(createCustomFiltersSelector('history')); + const dispatch = useDispatch(); + + const isFetchingAny = isFetching || isEpisodesFetching; + const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length); + const hasError = error || episodesError; + + const { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + } = usePaging({ + page, + totalPages, + gotoPage: gotoHistoryPage, + }); + + const handleFilterSelect = useCallback( + (selectedFilterKey: string) => { + dispatch(setHistoryFilter({ selectedFilterKey })); + }, + [dispatch] + ); + + const handleSortPress = useCallback( + (sortKey: string) => { + dispatch(setHistorySort({ sortKey })); + }, + [dispatch] + ); + + const handleTableOptionChange = useCallback( + (payload: TableOptionsChangePayload) => { + dispatch(setHistoryTableOption(payload)); + + if (payload.pageSize) { + dispatch(gotoHistoryPage({ page: 1 })); + } + }, + [dispatch] + ); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchHistory()); + } else { + dispatch(gotoHistoryPage({ page: 1 })); + } + + return () => { + dispatch(clearHistory()); + dispatch(clearEpisodes()); + dispatch(clearEpisodeFiles()); + }; + }, [requestCurrentPage, dispatch]); + + useEffect(() => { + const episodeIds = selectUniqueIds<HistoryItem, number>(items, 'episodeId'); + + if (episodeIds.length) { + dispatch(fetchEpisodes({ episodeIds })); + } else { + dispatch(clearEpisodes()); + } + }, [items, dispatch]); + + useEffect(() => { + const repopulate = () => { + dispatch(fetchHistory()); + }; + + registerPagePopulator(repopulate); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [dispatch]); + + return ( + <PageContent title={translate('History')}> + <PageToolbar> + <PageToolbarSection> + <PageToolbarButton + label={translate('Refresh')} + iconName={icons.REFRESH} + isSpinning={isFetching} + onPress={handleFirstPagePress} + /> + </PageToolbarSection> + + <PageToolbarSection alignContent={align.RIGHT}> + <TableOptionsModalWrapper + columns={columns} + onTableOptionChange={handleTableOptionChange} + > + <PageToolbarButton + label={translate('Options')} + iconName={icons.TABLE} + /> + </TableOptionsModalWrapper> + + <FilterMenu + alignMenu={align.RIGHT} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + filterModalConnectorComponent={HistoryFilterModal} + onFilterSelect={handleFilterSelect} + /> + </PageToolbarSection> + </PageToolbar> + + <PageContentBody> + {isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null} + + {!isFetchingAny && hasError ? ( + <Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert> + ) : null} + + { + // If history isPopulated and it's empty show no history found and don't + // wait for the episodes to populate because they are never coming. + + isPopulated && !hasError && !items.length ? ( + <Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert> + ) : null + } + + {isAllPopulated && !hasError && items.length ? ( + <div> + <Table + columns={columns} + sortKey={sortKey} + sortDirection={sortDirection} + onTableOptionChange={handleTableOptionChange} + onSortPress={handleSortPress} + > + <TableBody> + {items.map((item) => { + return ( + <HistoryRow key={item.id} columns={columns} {...item} /> + ); + })} + </TableBody> + </Table> + + <TablePager + page={page} + totalPages={totalPages} + totalRecords={totalRecords} + isFetching={isFetching} + onFirstPagePress={handleFirstPagePress} + onPreviousPagePress={handlePreviousPagePress} + onNextPagePress={handleNextPagePress} + onLastPagePress={handleLastPagePress} + onPageSelect={handlePageSelect} + /> + </div> + ) : null} + </PageContentBody> + </PageContent> + ); +} + +export default History; diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js deleted file mode 100644 index b407960bd..000000000 --- a/frontend/src/Activity/History/HistoryConnector.js +++ /dev/null @@ -1,165 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import withCurrentPage from 'Components/withCurrentPage'; -import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; -import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; -import * as historyActions from 'Store/Actions/historyActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; -import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; -import History from './History'; - -function createMapStateToProps() { - return createSelector( - (state) => state.history, - (state) => state.episodes, - createCustomFiltersSelector('history'), - (history, episodes, customFilters) => { - return { - isEpisodesFetching: episodes.isFetching, - isEpisodesPopulated: episodes.isPopulated, - episodesError: episodes.error, - customFilters, - ...history - }; - } - ); -} - -const mapDispatchToProps = { - ...historyActions, - fetchEpisodes, - clearEpisodes, - clearEpisodeFiles -}; - -class HistoryConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - useCurrentPage, - fetchHistory, - gotoHistoryFirstPage - } = this.props; - - registerPagePopulator(this.repopulate); - - if (useCurrentPage) { - fetchHistory(); - } else { - gotoHistoryFirstPage(); - } - } - - componentDidUpdate(prevProps) { - if (hasDifferentItems(prevProps.items, this.props.items)) { - const episodeIds = selectUniqueIds(this.props.items, 'episodeId'); - - if (episodeIds.length) { - this.props.fetchEpisodes({ episodeIds }); - } else { - this.props.clearEpisodes(); - } - } - } - - componentWillUnmount() { - unregisterPagePopulator(this.repopulate); - this.props.clearHistory(); - this.props.clearEpisodes(); - this.props.clearEpisodeFiles(); - } - - // - // Control - - repopulate = () => { - this.props.fetchHistory(); - }; - - // - // Listeners - - onFirstPagePress = () => { - this.props.gotoHistoryFirstPage(); - }; - - onPreviousPagePress = () => { - this.props.gotoHistoryPreviousPage(); - }; - - onNextPagePress = () => { - this.props.gotoHistoryNextPage(); - }; - - onLastPagePress = () => { - this.props.gotoHistoryLastPage(); - }; - - onPageSelect = (page) => { - this.props.gotoHistoryPage({ page }); - }; - - onSortPress = (sortKey) => { - this.props.setHistorySort({ sortKey }); - }; - - onFilterSelect = (selectedFilterKey) => { - this.props.setHistoryFilter({ selectedFilterKey }); - }; - - onTableOptionChange = (payload) => { - this.props.setHistoryTableOption(payload); - - if (payload.pageSize) { - this.props.gotoHistoryFirstPage(); - } - }; - - // - // Render - - render() { - return ( - <History - onFirstPagePress={this.onFirstPagePress} - onPreviousPagePress={this.onPreviousPagePress} - onNextPagePress={this.onNextPagePress} - onLastPagePress={this.onLastPagePress} - onPageSelect={this.onPageSelect} - onSortPress={this.onSortPress} - onFilterSelect={this.onFilterSelect} - onTableOptionChange={this.onTableOptionChange} - {...this.props} - /> - ); - } -} - -HistoryConnector.propTypes = { - useCurrentPage: PropTypes.bool.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchHistory: PropTypes.func.isRequired, - gotoHistoryFirstPage: PropTypes.func.isRequired, - gotoHistoryPreviousPage: PropTypes.func.isRequired, - gotoHistoryNextPage: PropTypes.func.isRequired, - gotoHistoryLastPage: PropTypes.func.isRequired, - gotoHistoryPage: PropTypes.func.isRequired, - setHistorySort: PropTypes.func.isRequired, - setHistoryFilter: PropTypes.func.isRequired, - setHistoryTableOption: PropTypes.func.isRequired, - clearHistory: PropTypes.func.isRequired, - fetchEpisodes: PropTypes.func.isRequired, - clearEpisodes: PropTypes.func.isRequired, - clearEpisodeFiles: PropTypes.func.isRequired -}; - -export default withCurrentPage( - connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector) -); diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.tsx similarity index 60% rename from frontend/src/Activity/History/HistoryEventTypeCell.js rename to frontend/src/Activity/History/HistoryEventTypeCell.tsx index 2f5ef6ee1..adedf08c0 100644 --- a/frontend/src/Activity/History/HistoryEventTypeCell.js +++ b/frontend/src/Activity/History/HistoryEventTypeCell.tsx @@ -1,12 +1,17 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import { icons, kinds } from 'Helpers/Props'; +import { + EpisodeFileDeletedHistory, + GrabbedHistoryData, + HistoryData, + HistoryEventType, +} from 'typings/History'; import translate from 'Utilities/String/translate'; import styles from './HistoryEventTypeCell.css'; -function getIconName(eventType, data) { +function getIconName(eventType: HistoryEventType, data: HistoryData) { switch (eventType) { case 'grabbed': return icons.DOWNLOADING; @@ -17,7 +22,9 @@ function getIconName(eventType, data) { case 'downloadFailed': return icons.DOWNLOADING; case 'episodeFileDeleted': - return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE; + return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk' + ? icons.FILE_MISSING + : icons.DELETE; case 'episodeFileRenamed': return icons.ORGANIZE; case 'downloadIgnored': @@ -27,7 +34,7 @@ function getIconName(eventType, data) { } } -function getIconKind(eventType) { +function getIconKind(eventType: HistoryEventType) { switch (eventType) { case 'downloadFailed': return kinds.DANGER; @@ -36,10 +43,13 @@ function getIconKind(eventType) { } } -function getTooltip(eventType, data) { +function getTooltip(eventType: HistoryEventType, data: HistoryData) { switch (eventType) { case 'grabbed': - return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient }); + return translate('EpisodeGrabbedTooltip', { + indexer: (data as GrabbedHistoryData).indexer, + downloadClient: (data as GrabbedHistoryData).downloadClient, + }); case 'seriesFolderImported': return translate('SeriesFolderImportedTooltip'); case 'downloadFolderImported': @@ -47,7 +57,9 @@ function getTooltip(eventType, data) { case 'downloadFailed': return translate('DownloadFailedEpisodeTooltip'); case 'episodeFileDeleted': - return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip'); + return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk' + ? translate('EpisodeFileMissingTooltip') + : translate('EpisodeFileDeletedTooltip'); case 'episodeFileRenamed': return translate('EpisodeFileRenamedTooltip'); case 'downloadIgnored': @@ -57,31 +69,21 @@ function getTooltip(eventType, data) { } } -function HistoryEventTypeCell({ eventType, data }) { +interface HistoryEventTypeCellProps { + eventType: HistoryEventType; + data: HistoryData; +} + +function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) { const iconName = getIconName(eventType, data); const iconKind = getIconKind(eventType); const tooltip = getTooltip(eventType, data); return ( - <TableRowCell - className={styles.cell} - title={tooltip} - > - <Icon - name={iconName} - kind={iconKind} - /> + <TableRowCell className={styles.cell} title={tooltip}> + <Icon name={iconName} kind={iconKind} /> </TableRowCell> ); } -HistoryEventTypeCell.propTypes = { - eventType: PropTypes.string.isRequired, - data: PropTypes.object -}; - -HistoryEventTypeCell.defaultProps = { - data: {} -}; - export default HistoryEventTypeCell; diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js deleted file mode 100644 index 507fdc2d7..000000000 --- a/frontend/src/Activity/History/HistoryRow.js +++ /dev/null @@ -1,312 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import Tooltip from 'Components/Tooltip/Tooltip'; -import episodeEntities from 'Episode/episodeEntities'; -import EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; -import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons, tooltipPositions } from 'Helpers/Props'; -import SeriesTitleLink from 'Series/SeriesTitleLink'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import HistoryDetailsModal from './Details/HistoryDetailsModal'; -import HistoryEventTypeCell from './HistoryEventTypeCell'; -import styles from './HistoryRow.css'; - -class HistoryRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - componentDidUpdate(prevProps) { - if ( - prevProps.isMarkingAsFailed && - !this.props.isMarkingAsFailed && - !this.props.markAsFailedError - ) { - this.setState({ isDetailsModalOpen: false }); - } - } - - // - // Listeners - - onDetailsPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - // - // Render - - render() { - const { - episodeId, - series, - episode, - languages, - quality, - customFormats, - customFormatScore, - qualityCutoffNotMet, - eventType, - sourceTitle, - date, - data, - downloadId, - isMarkingAsFailed, - columns, - shortDateFormat, - timeFormat, - onMarkAsFailedPress - } = this.props; - - if (!series || !episode) { - return null; - } - - return ( - <TableRow> - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'eventType') { - return ( - <HistoryEventTypeCell - key={name} - eventType={eventType} - data={data} - /> - ); - } - - if (name === 'series.sortTitle') { - return ( - <TableRowCell key={name}> - <SeriesTitleLink - titleSlug={series.titleSlug} - title={series.title} - /> - </TableRowCell> - ); - } - - if (name === 'episode') { - return ( - <TableRowCell key={name}> - <SeasonEpisodeNumber - seasonNumber={episode.seasonNumber} - episodeNumber={episode.episodeNumber} - absoluteEpisodeNumber={episode.absoluteEpisodeNumber} - seriesType={series.seriesType} - alternateTitles={series.alternateTitles} - sceneSeasonNumber={episode.sceneSeasonNumber} - sceneEpisodeNumber={episode.sceneEpisodeNumber} - sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber} - /> - </TableRowCell> - ); - } - - if (name === 'episodes.title') { - return ( - <TableRowCell key={name}> - <EpisodeTitleLink - episodeId={episodeId} - episodeEntity={episodeEntities.EPISODES} - seriesId={series.id} - episodeTitle={episode.title} - showOpenSeriesButton={true} - /> - </TableRowCell> - ); - } - - if (name === 'languages') { - return ( - <TableRowCell key={name}> - <EpisodeLanguages languages={languages} /> - </TableRowCell> - ); - } - - if (name === 'quality') { - return ( - <TableRowCell key={name}> - <EpisodeQuality - quality={quality} - isCutoffMet={qualityCutoffNotMet} - /> - </TableRowCell> - ); - } - - if (name === 'customFormats') { - return ( - <TableRowCell key={name}> - <EpisodeFormats - formats={customFormats} - /> - </TableRowCell> - ); - } - - if (name === 'date') { - return ( - <RelativeDateCellConnector - key={name} - date={date} - /> - ); - } - - if (name === 'downloadClient') { - return ( - <TableRowCell - key={name} - className={styles.downloadClient} - > - {data.downloadClient} - </TableRowCell> - ); - } - - if (name === 'indexer') { - return ( - <TableRowCell - key={name} - className={styles.indexer} - > - {data.indexer} - </TableRowCell> - ); - } - - if (name === 'customFormatScore') { - return ( - <TableRowCell - key={name} - className={styles.customFormatScore} - > - <Tooltip - anchor={formatCustomFormatScore( - customFormatScore, - customFormats.length - )} - tooltip={<EpisodeFormats formats={customFormats} />} - position={tooltipPositions.BOTTOM} - /> - </TableRowCell> - ); - } - - if (name === 'releaseGroup') { - return ( - <TableRowCell - key={name} - className={styles.releaseGroup} - > - {data.releaseGroup} - </TableRowCell> - ); - } - - if (name === 'sourceTitle') { - return ( - <TableRowCell - key={name} - > - {sourceTitle} - </TableRowCell> - ); - } - - if (name === 'details') { - return ( - <TableRowCell - key={name} - className={styles.details} - > - <div className={styles.actionContents}> - <IconButton - name={icons.INFO} - onPress={this.onDetailsPress} - /> - </div> - </TableRowCell> - ); - } - - return null; - }) - } - - <HistoryDetailsModal - isOpen={this.state.isDetailsModalOpen} - eventType={eventType} - sourceTitle={sourceTitle} - data={data} - downloadId={downloadId} - isMarkingAsFailed={isMarkingAsFailed} - shortDateFormat={shortDateFormat} - timeFormat={timeFormat} - onMarkAsFailedPress={onMarkAsFailedPress} - onModalClose={this.onDetailsModalClose} - /> - </TableRow> - ); - } - -} - -HistoryRow.propTypes = { - episodeId: PropTypes.number, - series: PropTypes.object.isRequired, - episode: PropTypes.object, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - quality: PropTypes.object.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - qualityCutoffNotMet: PropTypes.bool.isRequired, - eventType: PropTypes.string.isRequired, - sourceTitle: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, - downloadId: PropTypes.string, - isMarkingAsFailed: PropTypes.bool, - markAsFailedError: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - shortDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired -}; - -HistoryRow.defaultProps = { - customFormats: [] -}; - -export default HistoryRow; diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx new file mode 100644 index 000000000..bcd84f606 --- /dev/null +++ b/frontend/src/Activity/History/HistoryRow.tsx @@ -0,0 +1,275 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import episodeEntities from 'Episode/episodeEntities'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import useEpisode from 'Episode/useEpisode'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons, tooltipPositions } from 'Helpers/Props'; +import { QualityModel } from 'Quality/Quality'; +import SeriesTitleLink from 'Series/SeriesTitleLink'; +import useSeries from 'Series/useSeries'; +import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CustomFormat from 'typings/CustomFormat'; +import { HistoryData, HistoryEventType } from 'typings/History'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import HistoryDetailsModal from './Details/HistoryDetailsModal'; +import HistoryEventTypeCell from './HistoryEventTypeCell'; +import styles from './HistoryRow.css'; + +interface HistoryRowProps { + id: number; + episodeId: number; + seriesId: number; + languages: object[]; + quality: QualityModel; + customFormats?: CustomFormat[]; + customFormatScore: number; + qualityCutoffNotMet: boolean; + eventType: HistoryEventType; + sourceTitle: string; + date: string; + data: HistoryData; + downloadId?: string; + isMarkingAsFailed?: boolean; + markAsFailedError?: object; + columns: Column[]; +} + +function HistoryRow(props: HistoryRowProps) { + const { + id, + episodeId, + seriesId, + languages, + quality, + customFormats = [], + customFormatScore, + qualityCutoffNotMet, + eventType, + sourceTitle, + date, + data, + downloadId, + isMarkingAsFailed, + markAsFailedError, + columns, + } = props; + + const wasMarkingAsFailed = usePrevious(isMarkingAsFailed); + const dispatch = useDispatch(); + const series = useSeries(seriesId); + const episode = useEpisode(episodeId, 'episodes'); + + const { shortDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const handleDetailsPress = useCallback(() => { + setIsDetailsModalOpen(true); + }, [setIsDetailsModalOpen]); + + const handleDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(false); + }, [setIsDetailsModalOpen]); + + const handleMarkAsFailedPress = useCallback(() => { + dispatch(markAsFailed({ id })); + }, [id, dispatch]); + + useEffect(() => { + if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) { + setIsDetailsModalOpen(false); + dispatch(fetchHistory()); + } + }, [ + wasMarkingAsFailed, + isMarkingAsFailed, + markAsFailedError, + setIsDetailsModalOpen, + dispatch, + ]); + + if (!series || !episode) { + return null; + } + + return ( + <TableRow> + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'eventType') { + return ( + <HistoryEventTypeCell + key={name} + eventType={eventType} + data={data} + /> + ); + } + + if (name === 'series.sortTitle') { + return ( + <TableRowCell key={name}> + <SeriesTitleLink + titleSlug={series.titleSlug} + title={series.title} + /> + </TableRowCell> + ); + } + + if (name === 'episode') { + return ( + <TableRowCell key={name}> + <SeasonEpisodeNumber + seasonNumber={episode.seasonNumber} + episodeNumber={episode.episodeNumber} + absoluteEpisodeNumber={episode.absoluteEpisodeNumber} + seriesType={series.seriesType} + alternateTitles={series.alternateTitles} + sceneSeasonNumber={episode.sceneSeasonNumber} + sceneEpisodeNumber={episode.sceneEpisodeNumber} + sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber} + /> + </TableRowCell> + ); + } + + if (name === 'episodes.title') { + return ( + <TableRowCell key={name}> + <EpisodeTitleLink + episodeId={episodeId} + episodeEntity={episodeEntities.EPISODES} + seriesId={series.id} + episodeTitle={episode.title} + showOpenSeriesButton={true} + /> + </TableRowCell> + ); + } + + if (name === 'languages') { + return ( + <TableRowCell key={name}> + <EpisodeLanguages languages={languages} /> + </TableRowCell> + ); + } + + if (name === 'quality') { + return ( + <TableRowCell key={name}> + <EpisodeQuality + quality={quality} + isCutoffNotMet={qualityCutoffNotMet} + /> + </TableRowCell> + ); + } + + if (name === 'customFormats') { + return ( + <TableRowCell key={name}> + <EpisodeFormats formats={customFormats} /> + </TableRowCell> + ); + } + + if (name === 'date') { + return <RelativeDateCell key={name} date={date} />; + } + + if (name === 'downloadClient') { + return ( + <TableRowCell key={name} className={styles.downloadClient}> + {'downloadClient' in data ? data.downloadClient : ''} + </TableRowCell> + ); + } + + if (name === 'indexer') { + return ( + <TableRowCell key={name} className={styles.indexer}> + {'indexer' in data ? data.indexer : ''} + </TableRowCell> + ); + } + + if (name === 'customFormatScore') { + return ( + <TableRowCell key={name} className={styles.customFormatScore}> + <Tooltip + anchor={formatCustomFormatScore( + customFormatScore, + customFormats.length + )} + tooltip={<EpisodeFormats formats={customFormats} />} + position={tooltipPositions.BOTTOM} + /> + </TableRowCell> + ); + } + + if (name === 'releaseGroup') { + return ( + <TableRowCell key={name} className={styles.releaseGroup}> + {'releaseGroup' in data ? data.releaseGroup : ''} + </TableRowCell> + ); + } + + if (name === 'sourceTitle') { + return <TableRowCell key={name}>{sourceTitle}</TableRowCell>; + } + + if (name === 'details') { + return ( + <TableRowCell key={name} className={styles.details}> + <IconButton name={icons.INFO} onPress={handleDetailsPress} /> + </TableRowCell> + ); + } + + return null; + })} + + <HistoryDetailsModal + isOpen={isDetailsModalOpen} + eventType={eventType} + sourceTitle={sourceTitle} + data={data} + downloadId={downloadId} + isMarkingAsFailed={isMarkingAsFailed} + shortDateFormat={shortDateFormat} + timeFormat={timeFormat} + onMarkAsFailedPress={handleMarkAsFailedPress} + onModalClose={handleDetailsModalClose} + /> + </TableRow> + ); +} + +HistoryRow.defaultProps = { + customFormats: [], +}; + +export default HistoryRow; diff --git a/frontend/src/Activity/History/HistoryRowConnector.js b/frontend/src/Activity/History/HistoryRowConnector.js deleted file mode 100644 index b5d6223f6..000000000 --- a/frontend/src/Activity/History/HistoryRowConnector.js +++ /dev/null @@ -1,76 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions'; -import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import HistoryRow from './HistoryRow'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - createEpisodeSelector(), - createUISettingsSelector(), - (series, episode, uiSettings) => { - return { - series, - episode, - shortDateFormat: uiSettings.shortDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -const mapDispatchToProps = { - fetchHistory, - markAsFailed -}; - -class HistoryRowConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - if ( - prevProps.isMarkingAsFailed && - !this.props.isMarkingAsFailed && - !this.props.markAsFailedError - ) { - this.props.fetchHistory(); - } - } - - // - // Listeners - - onMarkAsFailedPress = () => { - this.props.markAsFailed({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - <HistoryRow - {...this.props} - onMarkAsFailedPress={this.onMarkAsFailedPress} - /> - ); - } - -} - -HistoryRowConnector.propTypes = { - id: PropTypes.number.isRequired, - isMarkingAsFailed: PropTypes.bool, - markAsFailedError: PropTypes.object, - fetchHistory: PropTypes.func.isRequired, - markAsFailed: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector); diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index f143ace3f..523147078 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import ProgressBar from 'Components/ProgressBar'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; @@ -217,7 +217,7 @@ class QueueRow extends Component { if (name === 'episodes.airDateUtc') { if (episode) { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={episode.airDateUtc} /> @@ -366,7 +366,7 @@ class QueueRow extends Component { if (name === 'added') { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={added} /> diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index e7f8a37ff..4a8330a6c 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Redirect, Route } from 'react-router-dom'; import Blocklist from 'Activity/Blocklist/Blocklist'; -import HistoryConnector from 'Activity/History/HistoryConnector'; +import History from 'Activity/History/History'; import QueueConnector from 'Activity/Queue/QueueConnector'; import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector'; import ImportSeries from 'AddSeries/ImportSeries/ImportSeries'; @@ -125,7 +125,7 @@ function AppRoutes(props) { <Route path="/activity/history" - component={HistoryConnector} + component={History} /> <Route diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 7f7ae00a8..b72e95760 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -59,6 +59,7 @@ interface AppState { blocklist: BlocklistAppState; calendar: CalendarAppState; commands: CommandAppState; + episodes: EpisodesAppState; episodeFiles: EpisodeFilesAppState; episodesSelection: EpisodesAppState; history: HistoryAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts index e368ff86e..632b82179 100644 --- a/frontend/src/App/State/HistoryAppState.ts +++ b/frontend/src/App/State/HistoryAppState.ts @@ -1,10 +1,14 @@ import AppSectionState, { AppSectionFilterState, + PagedAppSectionState, + TableAppSectionState, } from 'App/State/AppSectionState'; import History from 'typings/History'; interface HistoryAppState extends AppSectionState<History>, - AppSectionFilterState<History> {} + AppSectionFilterState<History>, + PagedAppSectionState, + TableAppSectionState {} export default HistoryAppState; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js deleted file mode 100644 index 37d23e8f9..000000000 --- a/frontend/src/Components/Table/Cells/RelativeDateCell.js +++ /dev/null @@ -1,69 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import getRelativeDate from 'Utilities/Date/getRelativeDate'; -import TableRowCell from './TableRowCell'; -import styles from './RelativeDateCell.css'; - -class RelativeDateCell extends PureComponent { - - // - // Render - - render() { - const { - className, - date, - includeSeconds, - includeTime, - showRelativeDates, - shortDateFormat, - longDateFormat, - timeFormat, - component: Component, - dispatch, - ...otherProps - } = this.props; - - if (!date) { - return ( - <Component - className={className} - {...otherProps} - /> - ); - } - - return ( - <Component - className={className} - title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })} - {...otherProps} - > - {getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })} - </Component> - ); - } -} - -RelativeDateCell.propTypes = { - className: PropTypes.string.isRequired, - date: PropTypes.string, - includeSeconds: PropTypes.bool.isRequired, - includeTime: PropTypes.bool.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - component: PropTypes.elementType, - dispatch: PropTypes.func -}; - -RelativeDateCell.defaultProps = { - className: styles.cell, - includeSeconds: false, - includeTime: false, - component: TableRowCell -}; - -export default RelativeDateCell; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.tsx b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx new file mode 100644 index 000000000..1c5be48be --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import TableRowCell from './TableRowCell'; +import styles from './RelativeDateCell.css'; + +interface RelativeDateCellProps { + className?: string; + date?: string; + includeSeconds?: boolean; + includeTime?: boolean; + component?: React.ElementType; +} + +function RelativeDateCell(props: RelativeDateCellProps) { + const { + className = styles.cell, + date, + includeSeconds = false, + includeTime = false, + + component: Component = TableRowCell, + ...otherProps + } = props; + + const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = + useSelector(createUISettingsSelector()); + + if (!date) { + return <Component className={className} {...otherProps} />; + } + + return ( + <Component + className={className} + title={formatDateTime(date, longDateFormat, timeFormat, { + includeSeconds, + includeRelativeDay: !showRelativeDates, + })} + {...otherProps} + > + {getRelativeDate({ + date, + shortDateFormat, + showRelativeDates, + timeFormat, + includeSeconds, + includeTime, + timeForToday: true, + })} + </Component> + ); +} + +export default RelativeDateCell; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js deleted file mode 100644 index ed996abbe..000000000 --- a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js +++ /dev/null @@ -1,21 +0,0 @@ -import _ from 'lodash'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import RelativeDateCell from './RelativeDateCell'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return _.pick(uiSettings, [ - 'showRelativeDates', - 'shortDateFormat', - 'longDateFormat', - 'timeFormat' - ]); - } - ); -} - -export default connect(createMapStateToProps, null)(RelativeDateCell); diff --git a/frontend/src/Episode/EpisodeNumber.js b/frontend/src/Episode/EpisodeNumber.js deleted file mode 100644 index 7f5c448f1..000000000 --- a/frontend/src/Episode/EpisodeNumber.js +++ /dev/null @@ -1,143 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import Icon from 'Components/Icon'; -import Popover from 'Components/Tooltip/Popover'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import padNumber from 'Utilities/Number/padNumber'; -import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles'; -import translate from 'Utilities/String/translate'; -import SceneInfo from './SceneInfo'; -import styles from './EpisodeNumber.css'; - -function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber) { - const messages = []; - - if (unverifiedSceneNumbering) { - messages.push(translate('SceneNumberNotVerified')); - } - - if (seriesType === 'anime' && !absoluteEpisodeNumber) { - messages.push(translate('EpisodeMissingAbsoluteNumber')); - } - - return messages.join('\n'); -} - -function EpisodeNumber(props) { - const { - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - sceneSeasonNumber, - sceneEpisodeNumber, - sceneAbsoluteEpisodeNumber, - useSceneNumbering, - unverifiedSceneNumbering, - alternateTitles: seriesAlternateTitles, - seriesType, - showSeasonNumber - } = props; - - const alternateTitles = filterAlternateTitles(seriesAlternateTitles, null, useSceneNumbering, seasonNumber, sceneSeasonNumber); - - const hasSceneInformation = sceneSeasonNumber !== undefined || - sceneEpisodeNumber !== undefined || - (seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined) || - !!alternateTitles.length; - - const warningMessage = getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber); - - return ( - <span> - { - hasSceneInformation ? - <Popover - anchor={ - <span> - { - showSeasonNumber && seasonNumber != null && - <Fragment> - {seasonNumber}x - </Fragment> - } - - {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} - - { - seriesType === 'anime' && !!absoluteEpisodeNumber && - <span className={styles.absoluteEpisodeNumber}> - ({absoluteEpisodeNumber}) - </span> - } - </span> - } - title={translate('SceneInformation')} - body={ - <SceneInfo - seasonNumber={seasonNumber} - episodeNumber={episodeNumber} - sceneSeasonNumber={sceneSeasonNumber} - sceneEpisodeNumber={sceneEpisodeNumber} - sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber} - alternateTitles={alternateTitles} - seriesType={seriesType} - /> - } - position={tooltipPositions.RIGHT} - /> : - <span> - { - showSeasonNumber && seasonNumber != null && - <Fragment> - {seasonNumber}x - </Fragment> - } - - {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} - - { - seriesType === 'anime' && !!absoluteEpisodeNumber && - <span className={styles.absoluteEpisodeNumber}> - ({absoluteEpisodeNumber}) - </span> - } - </span> - } - - { - warningMessage ? - <Icon - className={styles.warning} - name={icons.WARNING} - kind={kinds.WARNING} - title={warningMessage} - /> : - null - } - - </span> - ); -} - -EpisodeNumber.propTypes = { - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - sceneSeasonNumber: PropTypes.number, - sceneEpisodeNumber: PropTypes.number, - sceneAbsoluteEpisodeNumber: PropTypes.number, - useSceneNumbering: PropTypes.bool.isRequired, - unverifiedSceneNumbering: PropTypes.bool.isRequired, - alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, - seriesType: PropTypes.string, - showSeasonNumber: PropTypes.bool.isRequired -}; - -EpisodeNumber.defaultProps = { - useSceneNumbering: false, - unverifiedSceneNumbering: false, - alternateTitles: [], - showSeasonNumber: false -}; - -export default EpisodeNumber; diff --git a/frontend/src/Episode/EpisodeNumber.tsx b/frontend/src/Episode/EpisodeNumber.tsx new file mode 100644 index 000000000..596174499 --- /dev/null +++ b/frontend/src/Episode/EpisodeNumber.tsx @@ -0,0 +1,140 @@ +import React, { Fragment } from 'react'; +import Icon from 'Components/Icon'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import { AlternateTitle, SeriesType } from 'Series/Series'; +import padNumber from 'Utilities/Number/padNumber'; +import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles'; +import translate from 'Utilities/String/translate'; +import SceneInfo from './SceneInfo'; +import styles from './EpisodeNumber.css'; + +function getWarningMessage( + unverifiedSceneNumbering: boolean, + seriesType: SeriesType | undefined, + absoluteEpisodeNumber: number | undefined +) { + const messages = []; + + if (unverifiedSceneNumbering) { + messages.push(translate('SceneNumberNotVerified')); + } + + if (seriesType === 'anime' && !absoluteEpisodeNumber) { + messages.push(translate('EpisodeMissingAbsoluteNumber')); + } + + return messages.join('\n'); +} + +export interface EpisodeNumberProps { + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + sceneSeasonNumber?: number; + sceneEpisodeNumber?: number; + sceneAbsoluteEpisodeNumber?: number; + useSceneNumbering?: boolean; + unverifiedSceneNumbering?: boolean; + alternateTitles?: AlternateTitle[]; + seriesType?: SeriesType; + showSeasonNumber?: boolean; +} + +function EpisodeNumber(props: EpisodeNumberProps) { + const { + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + useSceneNumbering = false, + unverifiedSceneNumbering = false, + alternateTitles: seriesAlternateTitles = [], + seriesType, + showSeasonNumber = false, + } = props; + + const alternateTitles = filterAlternateTitles( + seriesAlternateTitles, + null, + useSceneNumbering, + seasonNumber, + sceneSeasonNumber + ); + + const hasSceneInformation = + sceneSeasonNumber !== undefined || + sceneEpisodeNumber !== undefined || + (seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined) || + !!alternateTitles.length; + + const warningMessage = getWarningMessage( + unverifiedSceneNumbering, + seriesType, + absoluteEpisodeNumber + ); + + return ( + <span> + {hasSceneInformation ? ( + <Popover + anchor={ + <span> + {showSeasonNumber && seasonNumber != null && ( + <Fragment>{seasonNumber}x</Fragment> + )} + + {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} + + {seriesType === 'anime' && !!absoluteEpisodeNumber && ( + <span className={styles.absoluteEpisodeNumber}> + ({absoluteEpisodeNumber}) + </span> + )} + </span> + } + title={translate('SceneInformation')} + body={ + <SceneInfo + seasonNumber={seasonNumber} + episodeNumber={episodeNumber} + sceneSeasonNumber={sceneSeasonNumber} + sceneEpisodeNumber={sceneEpisodeNumber} + sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber} + alternateTitles={alternateTitles} + seriesType={seriesType} + /> + } + position={tooltipPositions.RIGHT} + /> + ) : ( + <span> + {showSeasonNumber && seasonNumber != null && ( + <Fragment>{seasonNumber}x</Fragment> + )} + + {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} + + {seriesType === 'anime' && !!absoluteEpisodeNumber && ( + <span className={styles.absoluteEpisodeNumber}> + ({absoluteEpisodeNumber}) + </span> + )} + </span> + )} + + {warningMessage ? ( + <Icon + className={styles.warning} + name={icons.WARNING} + kind={kinds.WARNING} + title={warningMessage} + /> + ) : null} + </span> + ); +} + +export default EpisodeNumber; diff --git a/frontend/src/Episode/EpisodeQuality.js b/frontend/src/Episode/EpisodeQuality.tsx similarity index 69% rename from frontend/src/Episode/EpisodeQuality.js rename to frontend/src/Episode/EpisodeQuality.tsx index 5ca64224a..4de00b0b0 100644 --- a/frontend/src/Episode/EpisodeQuality.js +++ b/frontend/src/Episode/EpisodeQuality.tsx @@ -1,11 +1,15 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Label from 'Components/Label'; import { kinds } from 'Helpers/Props'; +import { QualityModel } from 'Quality/Quality'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; -function getTooltip(title, quality, size) { +function getTooltip( + title: string, + quality: QualityModel, + size: number | undefined +) { if (!title) { return; } @@ -27,7 +31,11 @@ function getTooltip(title, quality, size) { return title; } -function revisionLabel(className, quality, showRevision) { +function revisionLabel( + className: string | undefined, + quality: QualityModel, + showRevision: boolean +) { if (!showRevision) { return; } @@ -55,16 +63,27 @@ function revisionLabel(className, quality, showRevision) { </Label> ); } + + return null; } -function EpisodeQuality(props) { +interface EpisodeQualityProps { + className?: string; + title?: string; + quality: QualityModel; + size?: number; + isCutoffNotMet?: boolean; + showRevision?: boolean; +} + +function EpisodeQuality(props: EpisodeQualityProps) { const { className, - title, + title = '', quality, size, isCutoffNotMet, - showRevision + showRevision = false, } = props; if (!quality) { @@ -79,23 +98,10 @@ function EpisodeQuality(props) { title={getTooltip(title, quality, size)} > {quality.quality.name} - </Label>{revisionLabel(className, quality, showRevision)} + </Label> + {revisionLabel(className, quality, showRevision)} </span> ); } -EpisodeQuality.propTypes = { - className: PropTypes.string, - title: PropTypes.string, - quality: PropTypes.object.isRequired, - size: PropTypes.number, - isCutoffNotMet: PropTypes.bool, - showRevision: PropTypes.bool -}; - -EpisodeQuality.defaultProps = { - title: '', - showRevision: false -}; - export default EpisodeQuality; diff --git a/frontend/src/Episode/EpisodeTitleLink.tsx b/frontend/src/Episode/EpisodeTitleLink.tsx index f683820e0..e7455312d 100644 --- a/frontend/src/Episode/EpisodeTitleLink.tsx +++ b/frontend/src/Episode/EpisodeTitleLink.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import Link from 'Components/Link/Link'; import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; @@ -6,8 +5,12 @@ import FinaleType from './FinaleType'; import styles from './EpisodeTitleLink.css'; interface EpisodeTitleLinkProps { + episodeId: number; + seriesId: number; + episodeEntity: string; episodeTitle: string; finaleType?: string; + showOpenSeriesButton: boolean; } function EpisodeTitleLink(props: EpisodeTitleLinkProps) { @@ -38,9 +41,4 @@ function EpisodeTitleLink(props: EpisodeTitleLinkProps) { ); } -EpisodeTitleLink.propTypes = { - episodeTitle: PropTypes.string.isRequired, - finaleType: PropTypes.string, -}; - export default EpisodeTitleLink; diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js index 93cdb7c26..fd7fea827 100644 --- a/frontend/src/Episode/History/EpisodeHistoryRow.js +++ b/frontend/src/Episode/History/EpisodeHistoryRow.js @@ -1,11 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; +import HistoryDetails from 'Activity/History/Details/HistoryDetails'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import Popover from 'Components/Tooltip/Popover'; @@ -109,7 +109,7 @@ class EpisodeHistoryRow extends Component { {formatCustomFormatScore(customFormatScore, customFormats.length)} </TableRowCell> - <RelativeDateCellConnector + <RelativeDateCell date={date} includeSeconds={true} includeTime={true} @@ -124,7 +124,7 @@ class EpisodeHistoryRow extends Component { } title={getTitle(eventType)} body={ - <HistoryDetailsConnector + <HistoryDetails eventType={eventType} sourceTitle={sourceTitle} data={data} diff --git a/frontend/src/Episode/SeasonEpisodeNumber.js b/frontend/src/Episode/SeasonEpisodeNumber.js deleted file mode 100644 index e4d391002..000000000 --- a/frontend/src/Episode/SeasonEpisodeNumber.js +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import EpisodeNumber from './EpisodeNumber'; - -function SeasonEpisodeNumber(props) { - const { - airDate, - seriesType, - ...otherProps - } = props; - - if (seriesType === 'daily' && airDate) { - return ( - <span>{airDate}</span> - ); - } - - return ( - <EpisodeNumber - seriesType={seriesType} - showSeasonNumber={true} - {...otherProps} - /> - ); -} - -SeasonEpisodeNumber.propTypes = { - airDate: PropTypes.string, - seriesType: PropTypes.string -}; - -export default SeasonEpisodeNumber; diff --git a/frontend/src/Episode/SeasonEpisodeNumber.tsx b/frontend/src/Episode/SeasonEpisodeNumber.tsx new file mode 100644 index 000000000..c0a0afa51 --- /dev/null +++ b/frontend/src/Episode/SeasonEpisodeNumber.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { SeriesType } from 'Series/Series'; +import EpisodeNumber, { EpisodeNumberProps } from './EpisodeNumber'; + +interface SeasonEpisodeNumberProps extends EpisodeNumberProps { + airDate?: string; + seriesType?: SeriesType; +} + +function SeasonEpisodeNumber(props: SeasonEpisodeNumberProps) { + const { airDate, seriesType, ...otherProps } = props; + + if (seriesType === 'daily' && airDate) { + return <span>{airDate}</span>; + } + + return ( + <EpisodeNumber + seriesType={seriesType} + showSeasonNumber={true} + {...otherProps} + /> + ); +} + +export default SeasonEpisodeNumber; diff --git a/frontend/src/Episode/createEpisodesFetchingSelector.ts b/frontend/src/Episode/createEpisodesFetchingSelector.ts new file mode 100644 index 000000000..c1ed3bbc6 --- /dev/null +++ b/frontend/src/Episode/createEpisodesFetchingSelector.ts @@ -0,0 +1,17 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createEpisodesFetchingSelector() { + return createSelector( + (state: AppState) => state.episodes, + (episodes) => { + return { + isEpisodesFetching: episodes.isFetching, + isEpisodesPopulated: episodes.isPopulated, + episodesError: episodes.error, + }; + } + ); +} + +export default createEpisodesFetchingSelector; diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts new file mode 100644 index 000000000..4f32ffe7f --- /dev/null +++ b/frontend/src/Episode/useEpisode.ts @@ -0,0 +1,44 @@ +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +export type EpisodeEntities = + | 'calendar' + | 'episodes' + | 'interactiveImport' + | 'cutoffUnmet' + | 'missing'; + +function createEpisodeSelector(episodeId: number) { + return createSelector( + (state: AppState) => state.episodes.items, + (episodes) => { + return episodes.find((e) => e.id === episodeId); + } + ); +} + +function createCalendarEpisodeSelector(episodeId: number) { + return createSelector( + (state: AppState) => state.calendar.items, + (episodes) => { + return episodes.find((e) => e.id === episodeId); + } + ); +} + +function useEpisode(episodeId: number, episodeEntity: EpisodeEntities) { + let selector = createEpisodeSelector; + + switch (episodeEntity) { + case 'calendar': + selector = createCalendarEpisodeSelector; + break; + default: + break; + } + + return useSelector(selector(episodeId)); +} + +export default useEpisode; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js index 3903f0d71..83c7493c4 100644 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import IconButton from 'Components/Link/IconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowButton from 'Components/Table/TableRowButton'; import { icons } from 'Helpers/Props'; @@ -41,7 +41,7 @@ class RecentFolderRow extends Component { <TableRowButton onPress={this.onPress}> <TableRowCell>{folder}</TableRowCell> - <RelativeDateCellConnector date={lastUsed} /> + <RelativeDateCell date={lastUsed} /> <TableRowCell className={styles.actions}> <IconButton diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index c7dbf04b9..5f24a18e3 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Icon from 'Components/Icon'; import MonitorToggleButton from 'Components/MonitorToggleButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import Popover from 'Components/Tooltip/Popover'; @@ -176,7 +176,7 @@ class EpisodeRow extends Component { if (name === 'airDateUtc') { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={airDateUtc} /> diff --git a/frontend/src/Series/History/SeriesHistoryRow.js b/frontend/src/Series/History/SeriesHistoryRow.js index 743c8c869..19ec358ee 100644 --- a/frontend/src/Series/History/SeriesHistoryRow.js +++ b/frontend/src/Series/History/SeriesHistoryRow.js @@ -1,11 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; +import HistoryDetails from 'Activity/History/Details/HistoryDetails'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import Popover from 'Components/Tooltip/Popover'; @@ -133,7 +133,7 @@ class SeriesHistoryRow extends Component { {formatCustomFormatScore(customFormatScore, customFormats.length)} </TableRowCell> - <RelativeDateCellConnector + <RelativeDateCell date={date} includeSeconds={true} includeTime={true} @@ -148,7 +148,7 @@ class SeriesHistoryRow extends Component { } title={getTitle(eventType)} body={ - <HistoryDetailsConnector + <HistoryDetails eventType={eventType} sourceTitle={sourceTitle} data={data} diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 887776427..0e6548535 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -8,7 +8,7 @@ import HeartRating from 'Components/HeartRating'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import Column from 'Components/Table/Column'; @@ -252,7 +252,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ts(2739) - <RelativeDateCellConnector + <RelativeDateCell key={name} className={styles[name]} date={nextAiring} @@ -265,7 +265,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ts(2739) - <RelativeDateCellConnector + <RelativeDateCell key={name} className={styles[name]} date={previousAiring} @@ -278,7 +278,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ts(2739) - <RelativeDateCellConnector + <RelativeDateCell key={name} className={styles[name]} date={added} diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index ad63544e3..c82dfeef3 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -4,7 +4,7 @@ import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import { icons, kinds } from 'Helpers/Props'; @@ -110,7 +110,7 @@ class BackupRow extends Component { {formatBytes(size)} </TableRowCell> - <RelativeDateCellConnector + <RelativeDateCell date={time} /> diff --git a/frontend/src/System/Events/LogsTableRow.js b/frontend/src/System/Events/LogsTableRow.js index 2de54a189..2c38ea10c 100644 --- a/frontend/src/System/Events/LogsTableRow.js +++ b/frontend/src/System/Events/LogsTableRow.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Icon from 'Components/Icon'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowButton from 'Components/Table/TableRowButton'; import { icons } from 'Helpers/Props'; @@ -98,7 +98,7 @@ class LogsTableRow extends Component { if (name === 'time') { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={time} /> diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.js b/frontend/src/System/Logs/Files/LogFilesTableRow.js index ba0339b84..1e5ad552d 100644 --- a/frontend/src/System/Logs/Files/LogFilesTableRow.js +++ b/frontend/src/System/Logs/Files/LogFilesTableRow.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Link from 'Components/Link/Link'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import translate from 'Utilities/String/translate'; @@ -23,7 +23,7 @@ class LogFilesTableRow extends Component { <TableRow> <TableRowCell>{filename}</TableRowCell> - <RelativeDateCellConnector + <RelativeDateCell date={lastWriteTime} /> diff --git a/frontend/src/Utilities/Object/selectUniqueIds.js b/frontend/src/Utilities/Object/selectUniqueIds.js deleted file mode 100644 index c2c0c17e3..000000000 --- a/frontend/src/Utilities/Object/selectUniqueIds.js +++ /dev/null @@ -1,15 +0,0 @@ -import _ from 'lodash'; - -function selectUniqueIds(items, idProp) { - const ids = _.reduce(items, (result, item) => { - if (item[idProp]) { - result.push(item[idProp]); - } - - return result; - }, []); - - return _.uniq(ids); -} - -export default selectUniqueIds; diff --git a/frontend/src/Utilities/Object/selectUniqueIds.ts b/frontend/src/Utilities/Object/selectUniqueIds.ts new file mode 100644 index 000000000..847613c83 --- /dev/null +++ b/frontend/src/Utilities/Object/selectUniqueIds.ts @@ -0,0 +1,13 @@ +import KeysMatching from 'typings/Helpers/KeysMatching'; + +function selectUniqueIds<T, K>(items: T[], idProp: KeysMatching<T, K>) { + return items.reduce((acc: K[], item) => { + if (item[idProp] && acc.indexOf(item[idProp] as K) === -1) { + acc.push(item[idProp] as K); + } + + return acc; + }, []); +} + +export default selectUniqueIds; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 7d98eaee3..76fe0e0dd 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; @@ -99,7 +99,7 @@ function CutoffUnmetRow(props) { if (name === 'episodes.airDateUtc') { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={airDateUtc} /> diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index 92ecd451e..7064d9a9a 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; @@ -102,7 +102,7 @@ function MissingRow(props) { if (name === 'episodes.airDateUtc') { return ( - <RelativeDateCellConnector + <RelativeDateCell key={name} date={airDateUtc} /> diff --git a/frontend/src/typings/Helpers/KeysMatching.ts b/frontend/src/typings/Helpers/KeysMatching.ts index 0e20206ef..107e0904f 100644 --- a/frontend/src/typings/Helpers/KeysMatching.ts +++ b/frontend/src/typings/Helpers/KeysMatching.ts @@ -1,4 +1,4 @@ -type KeysMatching<T, V> = { +export type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never; }[keyof T]; diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts index 99fabe275..d20895f37 100644 --- a/frontend/src/typings/History.ts +++ b/frontend/src/typings/History.ts @@ -11,6 +11,63 @@ export type HistoryEventType = | 'episodeFileRenamed' | 'downloadIgnored'; +export interface GrabbedHistoryData { + indexer: string; + nzbInfoUrl: string; + releaseGroup: string; + age: string; + ageHours: string; + ageMinutes: string; + publishedDate: string; + downloadClient: string; + downloadClientName: string; + size: string; + downloadUrl: string; + guid: string; + tvdbId: string; + tvRageId: string; + protocol: string; + customFormatScore?: string; + seriesMatchType: string; + releaseSource: string; + indexerFlags: string; + releaseType: string; +} + +export interface DownloadFailedHistory { + message: string; +} + +export interface DownloadFolderImportedHistory { + customFormatScore?: string; + droppedPath: string; + importedPath: string; +} + +export interface EpisodeFileDeletedHistory { + customFormatScore?: string; + reason: 'Manual' | 'MissingFromDisk' | 'Upgrade'; +} + +export interface EpisodeFileRenamedHistory { + sourcePath: string; + sourceRelativePath: string; + path: string; + relativePath: string; +} + +export interface DownloadIgnoredHistory { + message: string; +} + +export type HistoryData = + | GrabbedHistoryData + | DownloadFailedHistory + | DownloadFolderImportedHistory + | EpisodeFileDeletedHistory + | EpisodeFileRenamedHistory + | DownloadIgnoredHistory; + export default interface History { episodeId: number; seriesId: number; @@ -23,6 +80,6 @@ export default interface History { date: string; downloadId: string; eventType: HistoryEventType; - data: unknown; + data: HistoryData; id: number; } From 76650af9fdc7ef06d13ce252986d21574903d293 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 21 Jul 2024 09:34:26 -0700 Subject: [PATCH 413/762] Convert Queue to TypeScript --- frontend/src/Activity/Queue/ProtocolLabel.css | 4 + .../src/Activity/Queue/ProtocolLabel.css.d.ts | 1 + frontend/src/Activity/Queue/ProtocolLabel.js | 20 - frontend/src/Activity/Queue/ProtocolLabel.tsx | 16 + frontend/src/Activity/Queue/Queue.js | 372 -------------- frontend/src/Activity/Queue/Queue.tsx | 411 +++++++++++++++ frontend/src/Activity/Queue/QueueConnector.js | 203 -------- .../{QueueDetails.js => QueueDetails.tsx} | 59 +-- frontend/src/Activity/Queue/QueueOptions.js | 78 --- frontend/src/Activity/Queue/QueueOptions.tsx | 46 ++ .../Activity/Queue/QueueOptionsConnector.js | 19 - frontend/src/Activity/Queue/QueueRow.js | 481 ------------------ frontend/src/Activity/Queue/QueueRow.tsx | 411 +++++++++++++++ .../src/Activity/Queue/QueueRowConnector.js | 70 --- .../Queue/{QueueStatus.js => QueueStatus.tsx} | 99 ++-- .../src/Activity/Queue/QueueStatusCell.js | 47 -- .../src/Activity/Queue/QueueStatusCell.tsx | 45 ++ .../Activity/Queue/RemoveQueueItemModal.tsx | 6 +- .../src/Activity/Queue/Status/QueueStatus.tsx | 37 ++ .../Queue/Status/QueueStatusConnector.js | 76 --- .../Queue/Status/createQueueStatusSelector.ts | 32 ++ .../{TimeleftCell.js => TimeleftCell.tsx} | 45 +- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/QueueAppState.ts | 21 +- .../Components/Page/Sidebar/PageSidebar.js | 4 +- frontend/src/Episode/useEpisode.ts | 9 +- frontend/src/Helpers/Props/TooltipPosition.ts | 7 +- .../Overview/SeriesIndexOverviewInfo.tsx | 15 +- .../src/Series/Index/Table/SeasonsCell.tsx | 3 +- ...getRelativeDate.tsx => getRelativeDate.ts} | 2 +- frontend/src/typings/Queue.ts | 6 +- 32 files changed, 1148 insertions(+), 1503 deletions(-) delete mode 100644 frontend/src/Activity/Queue/ProtocolLabel.js create mode 100644 frontend/src/Activity/Queue/ProtocolLabel.tsx delete mode 100644 frontend/src/Activity/Queue/Queue.js create mode 100644 frontend/src/Activity/Queue/Queue.tsx delete mode 100644 frontend/src/Activity/Queue/QueueConnector.js rename frontend/src/Activity/Queue/{QueueDetails.js => QueueDetails.tsx} (60%) delete mode 100644 frontend/src/Activity/Queue/QueueOptions.js create mode 100644 frontend/src/Activity/Queue/QueueOptions.tsx delete mode 100644 frontend/src/Activity/Queue/QueueOptionsConnector.js delete mode 100644 frontend/src/Activity/Queue/QueueRow.js create mode 100644 frontend/src/Activity/Queue/QueueRow.tsx delete mode 100644 frontend/src/Activity/Queue/QueueRowConnector.js rename frontend/src/Activity/Queue/{QueueStatus.js => QueueStatus.tsx} (63%) delete mode 100644 frontend/src/Activity/Queue/QueueStatusCell.js create mode 100644 frontend/src/Activity/Queue/QueueStatusCell.tsx create mode 100644 frontend/src/Activity/Queue/Status/QueueStatus.tsx delete mode 100644 frontend/src/Activity/Queue/Status/QueueStatusConnector.js create mode 100644 frontend/src/Activity/Queue/Status/createQueueStatusSelector.ts rename frontend/src/Activity/Queue/{TimeleftCell.js => TimeleftCell.tsx} (76%) rename frontend/src/Utilities/Date/{getRelativeDate.tsx => getRelativeDate.ts} (99%) diff --git a/frontend/src/Activity/Queue/ProtocolLabel.css b/frontend/src/Activity/Queue/ProtocolLabel.css index 110c7e01c..c94e383b1 100644 --- a/frontend/src/Activity/Queue/ProtocolLabel.css +++ b/frontend/src/Activity/Queue/ProtocolLabel.css @@ -11,3 +11,7 @@ border-color: var(--usenetColor); background-color: var(--usenetColor); } + +.unknown { + composes: label from '~Components/Label.css'; +} diff --git a/frontend/src/Activity/Queue/ProtocolLabel.css.d.ts b/frontend/src/Activity/Queue/ProtocolLabel.css.d.ts index f3b389e3d..ba0cb260d 100644 --- a/frontend/src/Activity/Queue/ProtocolLabel.css.d.ts +++ b/frontend/src/Activity/Queue/ProtocolLabel.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'torrent': string; + 'unknown': string; 'usenet': string; } export const cssExports: CssExports; diff --git a/frontend/src/Activity/Queue/ProtocolLabel.js b/frontend/src/Activity/Queue/ProtocolLabel.js deleted file mode 100644 index e8a08943c..000000000 --- a/frontend/src/Activity/Queue/ProtocolLabel.js +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import styles from './ProtocolLabel.css'; - -function ProtocolLabel({ protocol }) { - const protocolName = protocol === 'usenet' ? 'nzb' : protocol; - - return ( - <Label className={styles[protocol]}> - {protocolName} - </Label> - ); -} - -ProtocolLabel.propTypes = { - protocol: PropTypes.string.isRequired -}; - -export default ProtocolLabel; diff --git a/frontend/src/Activity/Queue/ProtocolLabel.tsx b/frontend/src/Activity/Queue/ProtocolLabel.tsx new file mode 100644 index 000000000..c1824452a --- /dev/null +++ b/frontend/src/Activity/Queue/ProtocolLabel.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Label from 'Components/Label'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import styles from './ProtocolLabel.css'; + +interface ProtocolLabelProps { + protocol: DownloadProtocol; +} + +function ProtocolLabel({ protocol }: ProtocolLabelProps) { + const protocolName = protocol === 'usenet' ? 'nzb' : protocol; + + return <Label className={styles[protocol]}>{protocolName}</Label>; +} + +export default ProtocolLabel; diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js deleted file mode 100644 index 64bfbc085..000000000 --- a/frontend/src/Activity/Queue/Queue.js +++ /dev/null @@ -1,372 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import TablePager from 'Components/Table/TablePager'; -import { align, icons, kinds } from 'Helpers/Props'; -import getRemovedItems from 'Utilities/Object/getRemovedItems'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; -import QueueFilterModal from './QueueFilterModal'; -import QueueOptionsConnector from './QueueOptionsConnector'; -import QueueRowConnector from './QueueRowConnector'; -import RemoveQueueItemModal from './RemoveQueueItemModal'; - -class Queue extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._shouldBlockRefresh = false; - - this.state = { - allSelected: false, - allUnselected: false, - lastToggled: null, - selectedState: {}, - isPendingSelected: false, - isConfirmRemoveModalOpen: false, - items: props.items - }; - } - - shouldComponentUpdate() { - if (this._shouldBlockRefresh) { - return false; - } - - return true; - } - - componentDidUpdate(prevProps) { - const { - items, - isEpisodesFetching - } = this.props; - - if ( - (!isEpisodesFetching && prevProps.isEpisodesFetching) || - (hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId)) - ) { - this.setState((state) => { - return { - ...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)), - items - }; - }); - - return; - } - - const nextState = {}; - - if (prevProps.items !== items) { - nextState.items = items; - } - - const selectedIds = this.getSelectedIds(); - const isPendingSelected = _.some(this.props.items, (item) => { - return selectedIds.indexOf(item.id) > -1 && item.status === 'delay'; - }); - - if (isPendingSelected !== this.state.isPendingSelected) { - nextState.isPendingSelected = isPendingSelected; - } - - if (!_.isEmpty(nextState)) { - this.setState(nextState); - } - } - - // - // Control - - getSelectedIds = () => { - return getSelectedIds(this.state.selectedState); - }; - - // - // Listeners - - onQueueRowModalOpenOrClose = (isOpen) => { - this._shouldBlockRefresh = isOpen; - }; - - onSelectAllChange = ({ value }) => { - this.setState(selectAll(this.state.selectedState, value)); - }; - - onSelectedChange = ({ id, value, shiftKey = false }) => { - this.setState((state) => { - return toggleSelected(state, this.props.items, id, value, shiftKey); - }); - }; - - onGrabSelectedPress = () => { - this.props.onGrabSelectedPress(this.getSelectedIds()); - }; - - onRemoveSelectedPress = () => { - this.setState({ isConfirmRemoveModalOpen: true }, () => { - this._shouldBlockRefresh = true; - }); - }; - - onRemoveSelectedConfirmed = (payload) => { - this._shouldBlockRefresh = false; - this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload }); - this.setState({ isConfirmRemoveModalOpen: false }); - }; - - onConfirmRemoveModalClose = () => { - this._shouldBlockRefresh = false; - this.setState({ isConfirmRemoveModalOpen: false }); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - isEpisodesFetching, - isEpisodesPopulated, - episodesError, - columns, - selectedFilterKey, - filters, - customFilters, - count, - totalRecords, - isGrabbing, - isRemoving, - isRefreshMonitoredDownloadsExecuting, - onRefreshPress, - onFilterSelect, - ...otherProps - } = this.props; - - const { - allSelected, - allUnselected, - selectedState, - isConfirmRemoveModalOpen, - isPendingSelected, - items - } = this.state; - - const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting; - const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId)); - const hasError = error || episodesError; - const selectedIds = this.getSelectedIds(); - const selectedCount = selectedIds.length; - const disableSelectedActions = selectedCount === 0; - - return ( - <PageContent title={translate('Queue')}> - <PageToolbar> - <PageToolbarSection> - <PageToolbarButton - label="Refresh" - iconName={icons.REFRESH} - isSpinning={isRefreshing} - onPress={onRefreshPress} - /> - - <PageToolbarSeparator /> - - <PageToolbarButton - label={translate('GrabSelected')} - iconName={icons.DOWNLOAD} - isDisabled={disableSelectedActions || !isPendingSelected} - isSpinning={isGrabbing} - onPress={this.onGrabSelectedPress} - /> - - <PageToolbarButton - label={translate('RemoveSelected')} - iconName={icons.REMOVE} - isDisabled={disableSelectedActions} - isSpinning={isRemoving} - onPress={this.onRemoveSelectedPress} - /> - </PageToolbarSection> - - <PageToolbarSection - alignContent={align.RIGHT} - > - <TableOptionsModalWrapper - columns={columns} - maxPageSize={200} - {...otherProps} - optionsComponent={QueueOptionsConnector} - > - <PageToolbarButton - label={translate('Options')} - iconName={icons.TABLE} - /> - </TableOptionsModalWrapper> - - <FilterMenu - alignMenu={align.RIGHT} - selectedFilterKey={selectedFilterKey} - filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={QueueFilterModal} - onFilterSelect={onFilterSelect} - /> - </PageToolbarSection> - </PageToolbar> - - <PageContentBody> - { - isRefreshing && !isAllPopulated ? - <LoadingIndicator /> : - null - } - - { - !isRefreshing && hasError ? - <Alert kind={kinds.DANGER}> - {translate('QueueLoadError')} - </Alert> : - null - } - - { - isAllPopulated && !hasError && !items.length ? - <Alert kind={kinds.INFO}> - { - selectedFilterKey !== 'all' && count > 0 ? - translate('QueueFilterHasNoItems') : - translate('QueueIsEmpty') - } - </Alert> : - null - } - - { - isAllPopulated && !hasError && !!items.length ? - <div> - <Table - columns={columns} - selectAll={true} - allSelected={allSelected} - allUnselected={allUnselected} - {...otherProps} - optionsComponent={QueueOptionsConnector} - onSelectAllChange={this.onSelectAllChange} - > - <TableBody> - { - items.map((item) => { - return ( - <QueueRowConnector - key={item.id} - episodeId={item.episodeId} - isSelected={selectedState[item.id]} - columns={columns} - {...item} - onSelectedChange={this.onSelectedChange} - onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose} - /> - ); - }) - } - </TableBody> - </Table> - - <TablePager - totalRecords={totalRecords} - isFetching={isRefreshing} - {...otherProps} - /> - </div> : - null - } - </PageContentBody> - - <RemoveQueueItemModal - isOpen={isConfirmRemoveModalOpen} - selectedCount={selectedCount} - canChangeCategory={isConfirmRemoveModalOpen && ( - selectedIds.every((id) => { - const item = items.find((i) => i.id === id); - - return !!(item && item.downloadClientHasPostImportCategory); - }) - )} - canIgnore={isConfirmRemoveModalOpen && ( - selectedIds.every((id) => { - const item = items.find((i) => i.id === id); - - return !!(item && item.seriesId && item.episodeId); - }) - )} - pending={isConfirmRemoveModalOpen && ( - selectedIds.every((id) => { - const item = items.find((i) => i.id === id); - - if (!item) { - return false; - } - - return item.status === 'delay' || item.status === 'downloadClientUnavailable'; - }) - )} - onRemovePress={this.onRemoveSelectedConfirmed} - onModalClose={this.onConfirmRemoveModalClose} - /> - </PageContent> - ); - } -} - -Queue.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - isEpisodesFetching: PropTypes.bool.isRequired, - isEpisodesPopulated: PropTypes.bool.isRequired, - episodesError: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - count: PropTypes.number.isRequired, - totalRecords: PropTypes.number, - isGrabbing: PropTypes.bool.isRequired, - isRemoving: PropTypes.bool.isRequired, - isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, - onRefreshPress: PropTypes.func.isRequired, - onGrabSelectedPress: PropTypes.func.isRequired, - onRemoveSelectedPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired -}; - -Queue.defaultProps = { - count: 0 -}; - -export default Queue; diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx new file mode 100644 index 000000000..6c5e0fb1b --- /dev/null +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -0,0 +1,411 @@ +import React, { + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import TablePager from 'Components/Table/TablePager'; +import usePaging from 'Components/Table/usePaging'; +import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import useSelectState from 'Helpers/Hooks/useSelectState'; +import { align, icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; +import { + clearQueue, + fetchQueue, + gotoQueuePage, + grabQueueItems, + removeQueueItems, + setQueueFilter, + setQueueSort, + setQueueTableOption, +} from 'Store/Actions/queueActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; +import QueueItem from 'typings/Queue'; +import { TableOptionsChangePayload } from 'typings/Table'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import QueueFilterModal from './QueueFilterModal'; +import QueueOptions from './QueueOptions'; +import QueueRow from './QueueRow'; +import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal'; +import createQueueStatusSelector from './Status/createQueueStatusSelector'; + +function Queue() { + const requestCurrentPage = useCurrentPage(); + const dispatch = useDispatch(); + + const { + isFetching, + isPopulated, + error, + items, + columns, + selectedFilterKey, + filters, + sortKey, + sortDirection, + page, + totalPages, + totalRecords, + isGrabbing, + isRemoving, + } = useSelector((state: AppState) => state.queue.paged); + + const { count } = useSelector(createQueueStatusSelector()); + const { isEpisodesFetching, isEpisodesPopulated, episodesError } = + useSelector(createEpisodesFetchingSelector()); + const customFilters = useSelector(createCustomFiltersSelector('queue')); + + const isRefreshMonitoredDownloadsExecuting = useSelector( + createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS) + ); + + const shouldBlockRefresh = useRef(false); + const currentQueue = useRef<ReactElement | null>(null); + + const [selectState, setSelectState] = useSelectState(); + const { allSelected, allUnselected, selectedState } = selectState; + + const selectedIds = useMemo(() => { + return getSelectedIds(selectedState); + }, [selectedState]); + + const isPendingSelected = useMemo(() => { + return items.some((item) => { + return selectedIds.indexOf(item.id) > -1 && item.status === 'delay'; + }); + }, [items, selectedIds]); + + const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = + useState(false); + + const isRefreshing = + isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting; + const isAllPopulated = + isPopulated && + (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId)); + const hasError = error || episodesError; + const selectedCount = selectedIds.length; + const disableSelectedActions = selectedCount === 0; + + const handleSelectAllChange = useCallback( + ({ value }: CheckInputChanged) => { + setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + }, + [items, setSelectState] + ); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + setSelectState({ + type: 'toggleSelected', + items, + id, + isSelected: value, + shiftKey, + }); + }, + [items, setSelectState] + ); + + const handleRefreshPress = useCallback(() => { + dispatch( + executeCommand({ + name: commandNames.REFRESH_MONITORED_DOWNLOADS, + }) + ); + }, [dispatch]); + + const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => { + shouldBlockRefresh.current = isOpen; + }, []); + + const handleGrabSelectedPress = useCallback(() => { + dispatch(grabQueueItems({ ids: selectedIds })); + }, [selectedIds, dispatch]); + + const handleRemoveSelectedPress = useCallback(() => { + shouldBlockRefresh.current = true; + setIsConfirmRemoveModalOpen(true); + }, [setIsConfirmRemoveModalOpen]); + + const handleRemoveSelectedConfirmed = useCallback( + (payload: RemovePressProps) => { + shouldBlockRefresh.current = false; + dispatch(removeQueueItems({ ids: selectedIds, ...payload })); + setIsConfirmRemoveModalOpen(false); + }, + [selectedIds, setIsConfirmRemoveModalOpen, dispatch] + ); + + const handleConfirmRemoveModalClose = useCallback(() => { + shouldBlockRefresh.current = false; + setIsConfirmRemoveModalOpen(false); + }, [setIsConfirmRemoveModalOpen]); + + const { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + } = usePaging({ + page, + totalPages, + gotoPage: gotoQueuePage, + }); + + const handleFilterSelect = useCallback( + (selectedFilterKey: string) => { + dispatch(setQueueFilter({ selectedFilterKey })); + }, + [dispatch] + ); + + const handleSortPress = useCallback( + (sortKey: string) => { + dispatch(setQueueSort({ sortKey })); + }, + [dispatch] + ); + + const handleTableOptionChange = useCallback( + (payload: TableOptionsChangePayload) => { + dispatch(setQueueTableOption(payload)); + + if (payload.pageSize) { + dispatch(gotoQueuePage({ page: 1 })); + } + }, + [dispatch] + ); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchQueue()); + } else { + dispatch(gotoQueuePage({ page: 1 })); + } + + return () => { + dispatch(clearQueue()); + }; + }, [requestCurrentPage, dispatch]); + + useEffect(() => { + const episodeIds = selectUniqueIds<QueueItem, number | undefined>( + items, + 'episodeId' + ); + + if (episodeIds.length) { + dispatch(fetchEpisodes({ episodeIds })); + } else { + dispatch(clearEpisodes()); + } + }, [items, dispatch]); + + useEffect(() => { + const repopulate = () => { + dispatch(fetchQueue()); + }; + + registerPagePopulator(repopulate); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [dispatch]); + + if (!shouldBlockRefresh.current) { + currentQueue.current = ( + <PageContentBody> + {isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null} + + {!isRefreshing && hasError ? ( + <Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert> + ) : null} + + {isAllPopulated && !hasError && !items.length ? ( + <Alert kind={kinds.INFO}> + {selectedFilterKey !== 'all' && count > 0 + ? translate('QueueFilterHasNoItems') + : translate('QueueIsEmpty')} + </Alert> + ) : null} + + {isAllPopulated && !hasError && !!items.length ? ( + <div> + <Table + selectAll={true} + allSelected={allSelected} + allUnselected={allUnselected} + columns={columns} + sortKey={sortKey} + sortDirection={sortDirection} + onTableOptionChange={handleTableOptionChange} + onSelectAllChange={handleSelectAllChange} + onSortPress={handleSortPress} + > + <TableBody> + {items.map((item) => { + return ( + <QueueRow + key={item.id} + episodeId={item.episodeId} + isSelected={selectedState[item.id]} + columns={columns} + {...item} + onSelectedChange={handleSelectedChange} + onQueueRowModalOpenOrClose={ + handleQueueRowModalOpenOrClose + } + /> + ); + })} + </TableBody> + </Table> + + <TablePager + page={page} + totalPages={totalPages} + totalRecords={totalRecords} + isFetching={isFetching} + onFirstPagePress={handleFirstPagePress} + onPreviousPagePress={handlePreviousPagePress} + onNextPagePress={handleNextPagePress} + onLastPagePress={handleLastPagePress} + onPageSelect={handlePageSelect} + /> + </div> + ) : null} + </PageContentBody> + ); + } + + return ( + <PageContent title={translate('Queue')}> + <PageToolbar> + <PageToolbarSection> + <PageToolbarButton + label="Refresh" + iconName={icons.REFRESH} + isSpinning={isRefreshing} + onPress={handleRefreshPress} + /> + + <PageToolbarSeparator /> + + <PageToolbarButton + label={translate('GrabSelected')} + iconName={icons.DOWNLOAD} + isDisabled={disableSelectedActions || !isPendingSelected} + isSpinning={isGrabbing} + onPress={handleGrabSelectedPress} + /> + + <PageToolbarButton + label={translate('RemoveSelected')} + iconName={icons.REMOVE} + isDisabled={disableSelectedActions} + isSpinning={isRemoving} + onPress={handleRemoveSelectedPress} + /> + </PageToolbarSection> + + <PageToolbarSection alignContent={align.RIGHT}> + <TableOptionsModalWrapper + columns={columns} + maxPageSize={200} + optionsComponent={QueueOptions} + onTableOptionChange={handleTableOptionChange} + > + <PageToolbarButton + label={translate('Options')} + iconName={icons.TABLE} + /> + </TableOptionsModalWrapper> + + <FilterMenu + alignMenu={align.RIGHT} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + filterModalConnectorComponent={QueueFilterModal} + onFilterSelect={handleFilterSelect} + /> + </PageToolbarSection> + </PageToolbar> + + {currentQueue.current} + + <RemoveQueueItemModal + isOpen={isConfirmRemoveModalOpen} + selectedCount={selectedCount} + canChangeCategory={ + isConfirmRemoveModalOpen && + selectedIds.every((id) => { + const item = items.find((i) => i.id === id); + + return !!(item && item.downloadClientHasPostImportCategory); + }) + } + canIgnore={ + isConfirmRemoveModalOpen && + selectedIds.every((id) => { + const item = items.find((i) => i.id === id); + + return !!(item && item.seriesId && item.episodeId); + }) + } + isPending={ + isConfirmRemoveModalOpen && + selectedIds.every((id) => { + const item = items.find((i) => i.id === id); + + if (!item) { + return false; + } + + return ( + item.status === 'delay' || + item.status === 'downloadClientUnavailable' + ); + }) + } + onRemovePress={handleRemoveSelectedConfirmed} + onModalClose={handleConfirmRemoveModalClose} + /> + </PageContent> + ); +} + +export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js deleted file mode 100644 index 178cb8e5f..000000000 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ /dev/null @@ -1,203 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import withCurrentPage from 'Components/withCurrentPage'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; -import * as queueActions from 'Store/Actions/queueActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; -import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; -import Queue from './Queue'; - -function createMapStateToProps() { - return createSelector( - (state) => state.episodes, - (state) => state.queue.options, - (state) => state.queue.paged, - (state) => state.queue.status.item, - createCustomFiltersSelector('queue'), - createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), - (episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => { - return { - count: options.includeUnknownSeriesItems ? status.totalCount : status.count, - isEpisodesFetching: episodes.isFetching, - isEpisodesPopulated: episodes.isPopulated, - episodesError: episodes.error, - customFilters, - isRefreshMonitoredDownloadsExecuting, - ...options, - ...queue - }; - } - ); -} - -const mapDispatchToProps = { - ...queueActions, - fetchEpisodes, - clearEpisodes, - executeCommand -}; - -class QueueConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - useCurrentPage, - fetchQueue, - fetchQueueStatus, - gotoQueueFirstPage - } = this.props; - - registerPagePopulator(this.repopulate); - - if (useCurrentPage) { - fetchQueue(); - } else { - gotoQueueFirstPage(); - } - - fetchQueueStatus(); - } - - componentDidUpdate(prevProps) { - if (hasDifferentItems(prevProps.items, this.props.items)) { - const episodeIds = selectUniqueIds(this.props.items, 'episodeId'); - - if (episodeIds.length) { - this.props.fetchEpisodes({ episodeIds }); - } else { - this.props.clearEpisodes(); - } - } - - if ( - this.props.includeUnknownSeriesItems !== - prevProps.includeUnknownSeriesItems - ) { - this.repopulate(); - } - } - - componentWillUnmount() { - unregisterPagePopulator(this.repopulate); - this.props.clearQueue(); - this.props.clearEpisodes(); - } - - // - // Control - - repopulate = () => { - this.props.fetchQueue(); - }; - - // - // Listeners - - onFirstPagePress = () => { - this.props.gotoQueueFirstPage(); - }; - - onPreviousPagePress = () => { - this.props.gotoQueuePreviousPage(); - }; - - onNextPagePress = () => { - this.props.gotoQueueNextPage(); - }; - - onLastPagePress = () => { - this.props.gotoQueueLastPage(); - }; - - onPageSelect = (page) => { - this.props.gotoQueuePage({ page }); - }; - - onSortPress = (sortKey) => { - this.props.setQueueSort({ sortKey }); - }; - - onFilterSelect = (selectedFilterKey) => { - this.props.setQueueFilter({ selectedFilterKey }); - }; - - onTableOptionChange = (payload) => { - this.props.setQueueTableOption(payload); - - if (payload.pageSize) { - this.props.gotoQueueFirstPage(); - } - }; - - onRefreshPress = () => { - this.props.executeCommand({ - name: commandNames.REFRESH_MONITORED_DOWNLOADS - }); - }; - - onGrabSelectedPress = (ids) => { - this.props.grabQueueItems({ ids }); - }; - - onRemoveSelectedPress = (payload) => { - this.props.removeQueueItems(payload); - }; - - // - // Render - - render() { - return ( - <Queue - onFirstPagePress={this.onFirstPagePress} - onPreviousPagePress={this.onPreviousPagePress} - onNextPagePress={this.onNextPagePress} - onLastPagePress={this.onLastPagePress} - onPageSelect={this.onPageSelect} - onSortPress={this.onSortPress} - onFilterSelect={this.onFilterSelect} - onTableOptionChange={this.onTableOptionChange} - onRefreshPress={this.onRefreshPress} - onGrabSelectedPress={this.onGrabSelectedPress} - onRemoveSelectedPress={this.onRemoveSelectedPress} - {...this.props} - /> - ); - } -} - -QueueConnector.propTypes = { - includeUnknownSeriesItems: PropTypes.bool.isRequired, - useCurrentPage: PropTypes.bool.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchQueue: PropTypes.func.isRequired, - fetchQueueStatus: PropTypes.func.isRequired, - gotoQueueFirstPage: PropTypes.func.isRequired, - gotoQueuePreviousPage: PropTypes.func.isRequired, - gotoQueueNextPage: PropTypes.func.isRequired, - gotoQueueLastPage: PropTypes.func.isRequired, - gotoQueuePage: PropTypes.func.isRequired, - setQueueSort: PropTypes.func.isRequired, - setQueueFilter: PropTypes.func.isRequired, - setQueueTableOption: PropTypes.func.isRequired, - clearQueue: PropTypes.func.isRequired, - grabQueueItems: PropTypes.func.isRequired, - removeQueueItems: PropTypes.func.isRequired, - fetchEpisodes: PropTypes.func.isRequired, - clearEpisodes: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired -}; - -export default withCurrentPage( - connect(createMapStateToProps, mapDispatchToProps)(QueueConnector) -); diff --git a/frontend/src/Activity/Queue/QueueDetails.js b/frontend/src/Activity/Queue/QueueDetails.tsx similarity index 60% rename from frontend/src/Activity/Queue/QueueDetails.js rename to frontend/src/Activity/Queue/QueueDetails.tsx index abc97b75c..be70ceead 100644 --- a/frontend/src/Activity/Queue/QueueDetails.js +++ b/frontend/src/Activity/Queue/QueueDetails.tsx @@ -1,36 +1,49 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, tooltipPositions } from 'Helpers/Props'; +import { + QueueTrackedDownloadState, + QueueTrackedDownloadStatus, + StatusMessage, +} from 'typings/Queue'; import translate from 'Utilities/String/translate'; import QueueStatus from './QueueStatus'; import styles from './QueueDetails.css'; -function QueueDetails(props) { +interface QueueDetailsProps { + title: string; + size: number; + sizeleft: number; + estimatedCompletionTime?: string; + status: string; + trackedDownloadState?: QueueTrackedDownloadState; + trackedDownloadStatus?: QueueTrackedDownloadStatus; + statusMessages?: StatusMessage[]; + errorMessage?: string; + progressBar: React.ReactNode; +} + +function QueueDetails(props: QueueDetailsProps) { const { title, size, sizeleft, status, - trackedDownloadState, - trackedDownloadStatus, + trackedDownloadState = 'downloading', + trackedDownloadStatus = 'ok', statusMessages, errorMessage, - progressBar + progressBar, } = props; - const progress = (100 - sizeleft / size * 100); + const progress = 100 - (sizeleft / size) * 100; const isDownloading = status === 'downloading'; const isPaused = status === 'paused'; const hasWarning = trackedDownloadStatus === 'warning'; const hasError = trackedDownloadStatus === 'error'; - if ( - (isDownloading || isPaused) && - !hasWarning && - !hasError - ) { + if ((isDownloading || isPaused) && !hasWarning && !hasError) { const state = isPaused ? translate('Paused') : translate('Downloading'); if (progress < 5) { @@ -45,11 +58,9 @@ function QueueDetails(props) { return ( <Popover className={styles.progressBarContainer} - anchor={progressBar} + anchor={progressBar!} title={`${state} - ${progress.toFixed(1)}%`} - body={ - <div>{title}</div> - } + body={<div>{title}</div>} position={tooltipPositions.LEFT} /> ); @@ -68,22 +79,4 @@ function QueueDetails(props) { ); } -QueueDetails.propTypes = { - title: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - sizeleft: PropTypes.number.isRequired, - estimatedCompletionTime: PropTypes.string, - status: PropTypes.string.isRequired, - trackedDownloadState: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string.isRequired, - statusMessages: PropTypes.arrayOf(PropTypes.object), - errorMessage: PropTypes.string, - progressBar: PropTypes.node.isRequired -}; - -QueueDetails.defaultProps = { - trackedDownloadStatus: 'ok', - trackedDownloadState: 'downloading' -}; - export default QueueDetails; diff --git a/frontend/src/Activity/Queue/QueueOptions.js b/frontend/src/Activity/Queue/QueueOptions.js deleted file mode 100644 index 573b3d9c2..000000000 --- a/frontend/src/Activity/Queue/QueueOptions.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -class QueueOptions extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - includeUnknownSeriesItems: props.includeUnknownSeriesItems - }; - } - - componentDidUpdate(prevProps) { - const { - includeUnknownSeriesItems - } = this.props; - - if (includeUnknownSeriesItems !== prevProps.includeUnknownSeriesItems) { - this.setState({ - includeUnknownSeriesItems - }); - } - } - - // - // Listeners - - onOptionChange = ({ name, value }) => { - this.setState({ - [name]: value - }, () => { - this.props.onOptionChange({ - [name]: value - }); - }); - }; - - // - // Render - - render() { - const { - includeUnknownSeriesItems - } = this.state; - - return ( - <Fragment> - <FormGroup> - <FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="includeUnknownSeriesItems" - value={includeUnknownSeriesItems} - helpText={translate('ShowUnknownSeriesItemsHelpText')} - onChange={this.onOptionChange} - /> - </FormGroup> - </Fragment> - ); - } -} - -QueueOptions.propTypes = { - includeUnknownSeriesItems: PropTypes.bool.isRequired, - onOptionChange: PropTypes.func.isRequired -}; - -export default QueueOptions; diff --git a/frontend/src/Activity/Queue/QueueOptions.tsx b/frontend/src/Activity/Queue/QueueOptions.tsx new file mode 100644 index 000000000..70bb0fdcf --- /dev/null +++ b/frontend/src/Activity/Queue/QueueOptions.tsx @@ -0,0 +1,46 @@ +import React, { Fragment, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import { inputTypes } from 'Helpers/Props'; +import { setQueueOption } from 'Store/Actions/queueActions'; +import { CheckInputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; + +function QueueOptions() { + const dispatch = useDispatch(); + const { includeUnknownSeriesItems } = useSelector( + (state: AppState) => state.queue.options + ); + + const handleOptionChange = useCallback( + ({ name, value }: CheckInputChanged) => { + dispatch( + setQueueOption({ + [name]: value, + }) + ); + }, + [dispatch] + ); + + return ( + <Fragment> + <FormGroup> + <FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="includeUnknownSeriesItems" + value={includeUnknownSeriesItems} + helpText={translate('ShowUnknownSeriesItemsHelpText')} + onChange={handleOptionChange} + /> + </FormGroup> + </Fragment> + ); +} + +export default QueueOptions; diff --git a/frontend/src/Activity/Queue/QueueOptionsConnector.js b/frontend/src/Activity/Queue/QueueOptionsConnector.js deleted file mode 100644 index b2c99511c..000000000 --- a/frontend/src/Activity/Queue/QueueOptionsConnector.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { setQueueOption } from 'Store/Actions/queueActions'; -import QueueOptions from './QueueOptions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.queue.options, - (options) => { - return options; - } - ); -} - -const mapDispatchToProps = { - onOptionChange: setQueueOption -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions); diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js deleted file mode 100644 index 523147078..000000000 --- a/frontend/src/Activity/Queue/QueueRow.js +++ /dev/null @@ -1,481 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; -import IconButton from 'Components/Link/IconButton'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import ProgressBar from 'Components/ProgressBar'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import TableRow from 'Components/Table/TableRow'; -import Tooltip from 'Components/Tooltip/Tooltip'; -import EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; -import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; -import SeriesTitleLink from 'Series/SeriesTitleLink'; -import formatBytes from 'Utilities/Number/formatBytes'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; -import QueueStatusCell from './QueueStatusCell'; -import RemoveQueueItemModal from './RemoveQueueItemModal'; -import TimeleftCell from './TimeleftCell'; -import styles from './QueueRow.css'; - -class QueueRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isRemoveQueueItemModalOpen: false, - isInteractiveImportModalOpen: false - }; - } - - // - // Listeners - - onRemoveQueueItemPress = () => { - this.setState({ isRemoveQueueItemModalOpen: true }); - }; - - onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => { - const { - onRemoveQueueItemPress, - onQueueRowModalOpenOrClose - } = this.props; - - onQueueRowModalOpenOrClose(false); - onRemoveQueueItemPress(blocklist, skipRedownload); - - this.setState({ isRemoveQueueItemModalOpen: false }); - }; - - onRemoveQueueItemModalClose = () => { - this.props.onQueueRowModalOpenOrClose(false); - - this.setState({ isRemoveQueueItemModalOpen: false }); - }; - - onInteractiveImportPress = () => { - this.props.onQueueRowModalOpenOrClose(true); - - this.setState({ isInteractiveImportModalOpen: true }); - }; - - onInteractiveImportModalClose = () => { - this.props.onQueueRowModalOpenOrClose(false); - - this.setState({ isInteractiveImportModalOpen: false }); - }; - - // - // Render - - render() { - const { - id, - downloadId, - title, - status, - trackedDownloadStatus, - trackedDownloadState, - statusMessages, - errorMessage, - series, - episode, - languages, - quality, - customFormats, - customFormatScore, - protocol, - indexer, - outputPath, - downloadClient, - downloadClientHasPostImportCategory, - estimatedCompletionTime, - added, - timeleft, - size, - sizeleft, - showRelativeDates, - shortDateFormat, - timeFormat, - isGrabbing, - grabError, - isRemoving, - isSelected, - columns, - onSelectedChange, - onGrabPress - } = this.props; - - const { - isRemoveQueueItemModalOpen, - isInteractiveImportModalOpen - } = this.state; - - const progress = 100 - (sizeleft / size * 100); - const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning'; - const isPending = status === 'delay' || status === 'downloadClientUnavailable'; - - return ( - <TableRow> - <TableSelectCell - id={id} - isSelected={isSelected} - onSelectedChange={onSelectedChange} - /> - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'status') { - return ( - <QueueStatusCell - key={name} - sourceTitle={title} - status={status} - trackedDownloadStatus={trackedDownloadStatus} - trackedDownloadState={trackedDownloadState} - statusMessages={statusMessages} - errorMessage={errorMessage} - /> - ); - } - - if (name === 'series.sortTitle') { - return ( - <TableRowCell key={name}> - { - series ? - <SeriesTitleLink - titleSlug={series.titleSlug} - title={series.title} - /> : - title - } - </TableRowCell> - ); - } - - if (name === 'episode') { - return ( - <TableRowCell key={name}> - { - episode ? - <SeasonEpisodeNumber - seasonNumber={episode.seasonNumber} - episodeNumber={episode.episodeNumber} - absoluteEpisodeNumber={episode.absoluteEpisodeNumber} - seriesType={series.seriesType} - alternateTitles={series.alternateTitles} - sceneSeasonNumber={episode.sceneSeasonNumber} - sceneEpisodeNumber={episode.sceneEpisodeNumber} - sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber} - unverifiedSceneNumbering={episode.unverifiedSceneNumbering} - /> : - '-' - } - </TableRowCell> - ); - } - - if (name === 'episodes.title') { - return ( - <TableRowCell key={name}> - { - episode ? - <EpisodeTitleLink - episodeId={episode.id} - seriesId={series.id} - episodeFileId={episode.episodeFileId} - episodeTitle={episode.title} - showOpenSeriesButton={true} - /> : - '-' - } - </TableRowCell> - ); - } - - if (name === 'episodes.airDateUtc') { - if (episode) { - return ( - <RelativeDateCell - key={name} - date={episode.airDateUtc} - /> - ); - } - - return ( - <TableRowCell key={name}> - - - </TableRowCell> - ); - } - - if (name === 'languages') { - return ( - <TableRowCell key={name}> - <EpisodeLanguages - languages={languages} - /> - </TableRowCell> - ); - } - - if (name === 'quality') { - return ( - <TableRowCell key={name}> - { - quality ? - <EpisodeQuality - quality={quality} - /> : - null - } - </TableRowCell> - ); - } - - if (name === 'customFormats') { - return ( - <TableRowCell key={name}> - <EpisodeFormats - formats={customFormats} - /> - </TableRowCell> - ); - } - - if (name === 'customFormatScore') { - return ( - <TableRowCell - key={name} - className={styles.customFormatScore} - > - <Tooltip - anchor={formatCustomFormatScore( - customFormatScore, - customFormats.length - )} - tooltip={<EpisodeFormats formats={customFormats} />} - position={tooltipPositions.BOTTOM} - /> - </TableRowCell> - ); - } - - if (name === 'protocol') { - return ( - <TableRowCell key={name}> - <ProtocolLabel - protocol={protocol} - /> - </TableRowCell> - ); - } - - if (name === 'indexer') { - return ( - <TableRowCell key={name}> - {indexer} - </TableRowCell> - ); - } - - if (name === 'downloadClient') { - return ( - <TableRowCell key={name}> - {downloadClient} - </TableRowCell> - ); - } - - if (name === 'title') { - return ( - <TableRowCell key={name}> - {title} - </TableRowCell> - ); - } - - if (name === 'size') { - return ( - <TableRowCell key={name}>{formatBytes(size)}</TableRowCell> - ); - } - - if (name === 'outputPath') { - return ( - <TableRowCell key={name}> - {outputPath} - </TableRowCell> - ); - } - - if (name === 'estimatedCompletionTime') { - return ( - <TimeleftCell - key={name} - status={status} - estimatedCompletionTime={estimatedCompletionTime} - timeleft={timeleft} - size={size} - sizeleft={sizeleft} - showRelativeDates={showRelativeDates} - shortDateFormat={shortDateFormat} - timeFormat={timeFormat} - /> - ); - } - - if (name === 'progress') { - return ( - <TableRowCell - key={name} - className={styles.progress} - > - { - !!progress && - <ProgressBar - progress={progress} - title={`${progress.toFixed(1)}%`} - /> - } - </TableRowCell> - ); - } - - if (name === 'added') { - return ( - <RelativeDateCell - key={name} - date={added} - /> - ); - } - - if (name === 'actions') { - return ( - <TableRowCell - key={name} - className={styles.actions} - > - { - showInteractiveImport && - <IconButton - name={icons.INTERACTIVE} - onPress={this.onInteractiveImportPress} - /> - } - - { - isPending && - <SpinnerIconButton - name={icons.DOWNLOAD} - kind={grabError ? kinds.DANGER : kinds.DEFAULT} - isSpinning={isGrabbing} - onPress={onGrabPress} - /> - } - - <SpinnerIconButton - title={translate('RemoveFromQueue')} - name={icons.REMOVE} - isSpinning={isRemoving} - onPress={this.onRemoveQueueItemPress} - /> - </TableRowCell> - ); - } - - return null; - }) - } - - <InteractiveImportModal - isOpen={isInteractiveImportModalOpen} - downloadId={downloadId} - title={title} - onModalClose={this.onInteractiveImportModalClose} - /> - - <RemoveQueueItemModal - isOpen={isRemoveQueueItemModalOpen} - sourceTitle={title} - canChangeCategory={!!downloadClientHasPostImportCategory} - canIgnore={!!series} - isPending={isPending} - onRemovePress={this.onRemoveQueueItemModalConfirmed} - onModalClose={this.onRemoveQueueItemModalClose} - /> - </TableRow> - ); - } - -} - -QueueRow.propTypes = { - id: PropTypes.number.isRequired, - downloadId: PropTypes.string, - title: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string, - trackedDownloadState: PropTypes.string, - statusMessages: PropTypes.arrayOf(PropTypes.object), - errorMessage: PropTypes.string, - series: PropTypes.object, - episode: PropTypes.object, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - quality: PropTypes.object.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - protocol: PropTypes.string.isRequired, - indexer: PropTypes.string, - outputPath: PropTypes.string, - downloadClient: PropTypes.string, - downloadClientHasPostImportCategory: PropTypes.bool, - estimatedCompletionTime: PropTypes.string, - added: PropTypes.string, - timeleft: PropTypes.string, - size: PropTypes.number, - sizeleft: PropTypes.number, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - isGrabbing: PropTypes.bool.isRequired, - grabError: PropTypes.object, - isRemoving: PropTypes.bool.isRequired, - isSelected: PropTypes.bool, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onSelectedChange: PropTypes.func.isRequired, - onGrabPress: PropTypes.func.isRequired, - onRemoveQueueItemPress: PropTypes.func.isRequired, - onQueueRowModalOpenOrClose: PropTypes.func.isRequired -}; - -QueueRow.defaultProps = { - customFormats: [], - isGrabbing: false, - isRemoving: false -}; - -export default QueueRow; diff --git a/frontend/src/Activity/Queue/QueueRow.tsx b/frontend/src/Activity/Queue/QueueRow.tsx new file mode 100644 index 000000000..25f5cb410 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRow.tsx @@ -0,0 +1,411 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import { Error } from 'App/State/AppSectionState'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import useEpisode from 'Episode/useEpisode'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import SeriesTitleLink from 'Series/SeriesTitleLink'; +import useSeries from 'Series/useSeries'; +import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CustomFormat from 'typings/CustomFormat'; +import { SelectStateInputProps } from 'typings/props'; +import { + QueueTrackedDownloadState, + QueueTrackedDownloadStatus, + StatusMessage, +} from 'typings/Queue'; +import formatBytes from 'Utilities/Number/formatBytes'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import translate from 'Utilities/String/translate'; +import QueueStatusCell from './QueueStatusCell'; +import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal'; +import TimeleftCell from './TimeleftCell'; +import styles from './QueueRow.css'; + +interface QueueRowProps { + id: number; + seriesId?: number; + episodeId?: number; + downloadId?: string; + title: string; + status: string; + trackedDownloadStatus?: QueueTrackedDownloadStatus; + trackedDownloadState?: QueueTrackedDownloadState; + statusMessages?: StatusMessage[]; + errorMessage?: string; + languages: Language[]; + quality: QualityModel; + customFormats?: CustomFormat[]; + customFormatScore: number; + protocol: DownloadProtocol; + indexer?: string; + outputPath?: string; + downloadClient?: string; + downloadClientHasPostImportCategory?: boolean; + estimatedCompletionTime?: string; + added?: string; + timeleft?: string; + size: number; + sizeleft: number; + isGrabbing?: boolean; + grabError?: Error; + isRemoving?: boolean; + isSelected?: boolean; + columns: Column[]; + onSelectedChange: (options: SelectStateInputProps) => void; + onQueueRowModalOpenOrClose: (isOpen: boolean) => void; +} + +function QueueRow(props: QueueRowProps) { + const { + id, + seriesId, + episodeId, + downloadId, + title, + status, + trackedDownloadStatus, + trackedDownloadState, + statusMessages, + errorMessage, + languages, + quality, + customFormats = [], + customFormatScore, + protocol, + indexer, + outputPath, + downloadClient, + downloadClientHasPostImportCategory, + estimatedCompletionTime, + added, + timeleft, + size, + sizeleft, + isGrabbing = false, + grabError, + isRemoving = false, + isSelected, + columns, + onSelectedChange, + onQueueRowModalOpenOrClose, + } = props; + + const dispatch = useDispatch(); + const series = useSeries(seriesId); + const episode = useEpisode(episodeId, 'episodes'); + const { showRelativeDates, shortDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + + const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] = + useState(false); + + const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] = + useState(false); + + const handleGrabPress = useCallback(() => { + dispatch(grabQueueItem({ id })); + }, [id, dispatch]); + + const handleInteractiveImportPress = useCallback(() => { + onQueueRowModalOpenOrClose(true); + setIsInteractiveImportModalOpen(true); + }, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]); + + const handleInteractiveImportModalClose = useCallback(() => { + onQueueRowModalOpenOrClose(false); + setIsInteractiveImportModalOpen(false); + }, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]); + + const handleRemoveQueueItemPress = useCallback(() => { + onQueueRowModalOpenOrClose(true); + setIsRemoveQueueItemModalOpen(true); + }, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]); + + const handleRemoveQueueItemModalConfirmed = useCallback( + (payload: RemovePressProps) => { + onQueueRowModalOpenOrClose(false); + dispatch(removeQueueItem({ id, ...payload })); + setIsRemoveQueueItemModalOpen(false); + }, + [id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch] + ); + + const handleRemoveQueueItemModalClose = useCallback(() => { + onQueueRowModalOpenOrClose(false); + setIsRemoveQueueItemModalOpen(false); + }, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]); + + const progress = 100 - (sizeleft / size) * 100; + const showInteractiveImport = + status === 'completed' && trackedDownloadStatus === 'warning'; + const isPending = + status === 'delay' || status === 'downloadClientUnavailable'; + + return ( + <TableRow> + <TableSelectCell + id={id} + isSelected={isSelected} + onSelectedChange={onSelectedChange} + /> + + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + <QueueStatusCell + key={name} + sourceTitle={title} + status={status} + trackedDownloadStatus={trackedDownloadStatus} + trackedDownloadState={trackedDownloadState} + statusMessages={statusMessages} + errorMessage={errorMessage} + /> + ); + } + + if (name === 'series.sortTitle') { + return ( + <TableRowCell key={name}> + {series ? ( + <SeriesTitleLink + titleSlug={series.titleSlug} + title={series.title} + /> + ) : ( + title + )} + </TableRowCell> + ); + } + + if (name === 'episode') { + return ( + <TableRowCell key={name}> + {episode ? ( + <SeasonEpisodeNumber + seasonNumber={episode.seasonNumber} + episodeNumber={episode.episodeNumber} + absoluteEpisodeNumber={episode.absoluteEpisodeNumber} + seriesType={series?.seriesType} + alternateTitles={series?.alternateTitles} + sceneSeasonNumber={episode.sceneSeasonNumber} + sceneEpisodeNumber={episode.sceneEpisodeNumber} + sceneAbsoluteEpisodeNumber={ + episode.sceneAbsoluteEpisodeNumber + } + unverifiedSceneNumbering={episode.unverifiedSceneNumbering} + /> + ) : ( + '-' + )} + </TableRowCell> + ); + } + + if (name === 'episodes.title') { + return ( + <TableRowCell key={name}> + {series && episode ? ( + <EpisodeTitleLink + episodeId={episode.id} + seriesId={series.id} + episodeTitle={episode.title} + episodeEntity="episodes" + showOpenSeriesButton={true} + /> + ) : ( + '-' + )} + </TableRowCell> + ); + } + + if (name === 'episodes.airDateUtc') { + if (episode) { + return <RelativeDateCell key={name} date={episode.airDateUtc} />; + } + + return <TableRowCell key={name}>-</TableRowCell>; + } + + if (name === 'languages') { + return ( + <TableRowCell key={name}> + <EpisodeLanguages languages={languages} /> + </TableRowCell> + ); + } + + if (name === 'quality') { + return ( + <TableRowCell key={name}> + {quality ? <EpisodeQuality quality={quality} /> : null} + </TableRowCell> + ); + } + + if (name === 'customFormats') { + return ( + <TableRowCell key={name}> + <EpisodeFormats formats={customFormats} /> + </TableRowCell> + ); + } + + if (name === 'customFormatScore') { + return ( + <TableRowCell key={name} className={styles.customFormatScore}> + <Tooltip + anchor={formatCustomFormatScore( + customFormatScore, + customFormats.length + )} + tooltip={<EpisodeFormats formats={customFormats} />} + position={tooltipPositions.BOTTOM} + /> + </TableRowCell> + ); + } + + if (name === 'protocol') { + return ( + <TableRowCell key={name}> + <ProtocolLabel protocol={protocol} /> + </TableRowCell> + ); + } + + if (name === 'indexer') { + return <TableRowCell key={name}>{indexer}</TableRowCell>; + } + + if (name === 'downloadClient') { + return <TableRowCell key={name}>{downloadClient}</TableRowCell>; + } + + if (name === 'title') { + return <TableRowCell key={name}>{title}</TableRowCell>; + } + + if (name === 'size') { + return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>; + } + + if (name === 'outputPath') { + return <TableRowCell key={name}>{outputPath}</TableRowCell>; + } + + if (name === 'estimatedCompletionTime') { + return ( + <TimeleftCell + key={name} + status={status} + estimatedCompletionTime={estimatedCompletionTime} + timeleft={timeleft} + size={size} + sizeleft={sizeleft} + showRelativeDates={showRelativeDates} + shortDateFormat={shortDateFormat} + timeFormat={timeFormat} + /> + ); + } + + if (name === 'progress') { + return ( + <TableRowCell key={name} className={styles.progress}> + {!!progress && ( + <ProgressBar + progress={progress} + title={`${progress.toFixed(1)}%`} + /> + )} + </TableRowCell> + ); + } + + if (name === 'added') { + return <RelativeDateCell key={name} date={added} />; + } + + if (name === 'actions') { + return ( + <TableRowCell key={name} className={styles.actions}> + {showInteractiveImport ? ( + <IconButton + name={icons.INTERACTIVE} + onPress={handleInteractiveImportPress} + /> + ) : null} + + {isPending ? ( + <SpinnerIconButton + name={icons.DOWNLOAD} + kind={grabError ? kinds.DANGER : kinds.DEFAULT} + isSpinning={isGrabbing} + onPress={handleGrabPress} + /> + ) : null} + + <SpinnerIconButton + title={translate('RemoveFromQueue')} + name={icons.REMOVE} + isSpinning={isRemoving} + onPress={handleRemoveQueueItemPress} + /> + </TableRowCell> + ); + } + + return null; + })} + + <InteractiveImportModal + isOpen={isInteractiveImportModalOpen} + downloadId={downloadId} + modalTitle={title} + onModalClose={handleInteractiveImportModalClose} + /> + + <RemoveQueueItemModal + isOpen={isRemoveQueueItemModalOpen} + sourceTitle={title} + canChangeCategory={!!downloadClientHasPostImportCategory} + canIgnore={!!series} + isPending={isPending} + onRemovePress={handleRemoveQueueItemModalConfirmed} + onModalClose={handleRemoveQueueItemModalClose} + /> + </TableRow> + ); +} + +export default QueueRow; diff --git a/frontend/src/Activity/Queue/QueueRowConnector.js b/frontend/src/Activity/Queue/QueueRowConnector.js deleted file mode 100644 index e1e469a70..000000000 --- a/frontend/src/Activity/Queue/QueueRowConnector.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions'; -import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import QueueRow from './QueueRow'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - createEpisodeSelector(), - createUISettingsSelector(), - (series, episode, uiSettings) => { - const result = { - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - timeFormat: uiSettings.timeFormat - }; - - result.series = series; - result.episode = episode; - - return result; - } - ); -} - -const mapDispatchToProps = { - grabQueueItem, - removeQueueItem -}; - -class QueueRowConnector extends Component { - - // - // Listeners - - onGrabPress = () => { - this.props.grabQueueItem({ id: this.props.id }); - }; - - onRemoveQueueItemPress = (payload) => { - this.props.removeQueueItem({ id: this.props.id, ...payload }); - }; - - // - // Render - - render() { - return ( - <QueueRow - {...this.props} - onGrabPress={this.onGrabPress} - onRemoveQueueItemPress={this.onRemoveQueueItemPress} - /> - ); - } -} - -QueueRowConnector.propTypes = { - id: PropTypes.number.isRequired, - episode: PropTypes.object, - grabQueueItem: PropTypes.func.isRequired, - removeQueueItem: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector); diff --git a/frontend/src/Activity/Queue/QueueStatus.js b/frontend/src/Activity/Queue/QueueStatus.tsx similarity index 63% rename from frontend/src/Activity/Queue/QueueStatus.js rename to frontend/src/Activity/Queue/QueueStatus.tsx index f7cab31ca..d7314baff 100644 --- a/frontend/src/Activity/Queue/QueueStatus.js +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -1,51 +1,59 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import { icons, kinds } from 'Helpers/Props'; +import TooltipPosition from 'Helpers/Props/TooltipPosition'; +import { + QueueTrackedDownloadState, + QueueTrackedDownloadStatus, + StatusMessage, +} from 'typings/Queue'; import translate from 'Utilities/String/translate'; import styles from './QueueStatus.css'; -function getDetailedPopoverBody(statusMessages) { +function getDetailedPopoverBody(statusMessages: StatusMessage[]) { return ( <div> - { - statusMessages.map(({ title, messages }) => { - return ( - <div - key={title} - className={messages.length ? undefined: styles.noMessages} - > - {title} - <ul> - { - messages.map((message) => { - return ( - <li key={message}> - {message} - </li> - ); - }) - } - </ul> - </div> - ); - }) - } + {statusMessages.map(({ title, messages }) => { + return ( + <div + key={title} + className={messages.length ? undefined : styles.noMessages} + > + {title} + <ul> + {messages.map((message) => { + return <li key={message}>{message}</li>; + })} + </ul> + </div> + ); + })} </div> ); } -function QueueStatus(props) { +interface QueueStatusProps { + sourceTitle: string; + status: string; + trackedDownloadStatus?: QueueTrackedDownloadStatus; + trackedDownloadState?: QueueTrackedDownloadState; + statusMessages?: StatusMessage[]; + errorMessage?: string; + position: TooltipPosition; + canFlip?: boolean; +} + +function QueueStatus(props: QueueStatusProps) { const { sourceTitle, status, - trackedDownloadStatus, - trackedDownloadState, - statusMessages, + trackedDownloadStatus = 'ok', + trackedDownloadState = 'downloading', + statusMessages = [], errorMessage, position, - canFlip + canFlip = false, } = props; const hasWarning = trackedDownloadStatus === 'warning'; @@ -115,7 +123,8 @@ function QueueStatus(props) { if (status === 'warning') { iconName = icons.DOWNLOADING; iconKind = kinds.WARNING; - const warningMessage = errorMessage || translate('CheckDownloadClientForDetails'); + const warningMessage = + errorMessage || translate('CheckDownloadClientForDetails'); title = translate('DownloadWarning', { warningMessage }); } @@ -133,35 +142,23 @@ function QueueStatus(props) { return ( <Popover - anchor={ - <Icon - name={iconName} - kind={iconKind} - /> - } + anchor={<Icon name={iconName} kind={iconKind} />} title={title} - body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle} + body={ + hasWarning || hasError + ? getDetailedPopoverBody(statusMessages) + : sourceTitle + } position={position} canFlip={canFlip} /> ); } -QueueStatus.propTypes = { - sourceTitle: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string.isRequired, - trackedDownloadState: PropTypes.string.isRequired, - statusMessages: PropTypes.arrayOf(PropTypes.object), - errorMessage: PropTypes.string, - position: PropTypes.oneOf(tooltipPositions.all).isRequired, - canFlip: PropTypes.bool.isRequired -}; - QueueStatus.defaultProps = { trackedDownloadStatus: 'ok', trackedDownloadState: 'downloading', - canFlip: false + canFlip: false, }; export default QueueStatus; diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js deleted file mode 100644 index 4e8b9658c..000000000 --- a/frontend/src/Activity/Queue/QueueStatusCell.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import { tooltipPositions } from 'Helpers/Props'; -import QueueStatus from './QueueStatus'; -import styles from './QueueStatusCell.css'; - -function QueueStatusCell(props) { - const { - sourceTitle, - status, - trackedDownloadStatus, - trackedDownloadState, - statusMessages, - errorMessage - } = props; - - return ( - <TableRowCell className={styles.status}> - <QueueStatus - sourceTitle={sourceTitle} - status={status} - trackedDownloadStatus={trackedDownloadStatus} - trackedDownloadState={trackedDownloadState} - statusMessages={statusMessages} - errorMessage={errorMessage} - position={tooltipPositions.RIGHT} - /> - </TableRowCell> - ); -} - -QueueStatusCell.propTypes = { - sourceTitle: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string.isRequired, - trackedDownloadState: PropTypes.string.isRequired, - statusMessages: PropTypes.arrayOf(PropTypes.object), - errorMessage: PropTypes.string -}; - -QueueStatusCell.defaultProps = { - trackedDownloadStatus: 'ok', - trackedDownloadState: 'downloading' -}; - -export default QueueStatusCell; diff --git a/frontend/src/Activity/Queue/QueueStatusCell.tsx b/frontend/src/Activity/Queue/QueueStatusCell.tsx new file mode 100644 index 000000000..634e33164 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatusCell.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import { + QueueTrackedDownloadState, + QueueTrackedDownloadStatus, + StatusMessage, +} from 'typings/Queue'; +import QueueStatus from './QueueStatus'; +import styles from './QueueStatusCell.css'; + +interface QueueStatusCellProps { + sourceTitle: string; + status: string; + trackedDownloadStatus?: QueueTrackedDownloadStatus; + trackedDownloadState?: QueueTrackedDownloadState; + statusMessages?: StatusMessage[]; + errorMessage?: string; +} + +function QueueStatusCell(props: QueueStatusCellProps) { + const { + sourceTitle, + status, + trackedDownloadStatus = 'ok', + trackedDownloadState = 'downloading', + statusMessages, + errorMessage, + } = props; + + return ( + <TableRowCell className={styles.status}> + <QueueStatus + sourceTitle={sourceTitle} + status={status} + trackedDownloadStatus={trackedDownloadStatus} + trackedDownloadState={trackedDownloadState} + statusMessages={statusMessages} + errorMessage={errorMessage} + position="right" + /> + </TableRowCell> + ); +} + +export default QueueStatusCell; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx index 255c8a562..461fa57ad 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx @@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './RemoveQueueItemModal.css'; -interface RemovePressProps { +export interface RemovePressProps { remove: boolean; changeCategory: boolean; blocklist: boolean; @@ -21,7 +21,7 @@ interface RemovePressProps { interface RemoveQueueItemModalProps { isOpen: boolean; - sourceTitle: string; + sourceTitle?: string; canChangeCategory: boolean; canIgnore: boolean; isPending: boolean; @@ -39,7 +39,7 @@ type BlocklistMethod = function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { const { isOpen, - sourceTitle, + sourceTitle = '', canIgnore, canChangeCategory, isPending, diff --git a/frontend/src/Activity/Queue/Status/QueueStatus.tsx b/frontend/src/Activity/Queue/Status/QueueStatus.tsx new file mode 100644 index 000000000..894434e07 --- /dev/null +++ b/frontend/src/Activity/Queue/Status/QueueStatus.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { fetchQueueStatus } from 'Store/Actions/queueActions'; +import createQueueStatusSelector from './createQueueStatusSelector'; + +function QueueStatus() { + const dispatch = useDispatch(); + const { isConnected, isReconnecting } = useSelector( + (state: AppState) => state.app + ); + const { isPopulated, count, errors, warnings } = useSelector( + createQueueStatusSelector() + ); + + const wasReconnecting = usePrevious(isReconnecting); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchQueueStatus()); + } + }, [isPopulated, dispatch]); + + useEffect(() => { + if (isConnected && wasReconnecting) { + dispatch(fetchQueueStatus()); + } + }, [isConnected, wasReconnecting, dispatch]); + + return ( + <PageSidebarStatus count={count} errors={errors} warnings={warnings} /> + ); +} + +export default QueueStatus; diff --git a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js deleted file mode 100644 index 9412d7952..000000000 --- a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js +++ /dev/null @@ -1,76 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; -import { fetchQueueStatus } from 'Store/Actions/queueActions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app, - (state) => state.queue.status, - (state) => state.queue.options.includeUnknownSeriesItems, - (app, status, includeUnknownSeriesItems) => { - const { - errors, - warnings, - unknownErrors, - unknownWarnings, - count, - totalCount - } = status.item; - - return { - isConnected: app.isConnected, - isReconnecting: app.isReconnecting, - isPopulated: status.isPopulated, - ...status.item, - count: includeUnknownSeriesItems ? totalCount : count, - errors: includeUnknownSeriesItems ? errors || unknownErrors : errors, - warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings - }; - } - ); -} - -const mapDispatchToProps = { - fetchQueueStatus -}; - -class QueueStatusConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.fetchQueueStatus(); - } - } - - componentDidUpdate(prevProps) { - if (this.props.isConnected && prevProps.isReconnecting) { - this.props.fetchQueueStatus(); - } - } - - // - // Render - - render() { - return ( - <PageSidebarStatus - {...this.props} - /> - ); - } -} - -QueueStatusConnector.propTypes = { - isConnected: PropTypes.bool.isRequired, - isReconnecting: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - fetchQueueStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector); diff --git a/frontend/src/Activity/Queue/Status/createQueueStatusSelector.ts b/frontend/src/Activity/Queue/Status/createQueueStatusSelector.ts new file mode 100644 index 000000000..4fd37b28c --- /dev/null +++ b/frontend/src/Activity/Queue/Status/createQueueStatusSelector.ts @@ -0,0 +1,32 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createQueueStatusSelector() { + return createSelector( + (state: AppState) => state.queue.status.isPopulated, + (state: AppState) => state.queue.status.item, + (state: AppState) => state.queue.options.includeUnknownSeriesItems, + (isPopulated, status, includeUnknownSeriesItems) => { + const { + errors, + warnings, + unknownErrors, + unknownWarnings, + count, + totalCount, + } = status; + + return { + ...status, + isPopulated, + count: includeUnknownSeriesItems ? totalCount : count, + errors: includeUnknownSeriesItems ? errors || unknownErrors : errors, + warnings: includeUnknownSeriesItems + ? warnings || unknownWarnings + : warnings, + }; + } + ); +} + +export default createQueueStatusSelector; diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.tsx similarity index 76% rename from frontend/src/Activity/Queue/TimeleftCell.js rename to frontend/src/Activity/Queue/TimeleftCell.tsx index 0a39b7edc..917a6ad0d 100644 --- a/frontend/src/Activity/Queue/TimeleftCell.js +++ b/frontend/src/Activity/Queue/TimeleftCell.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import styles from './TimeleftCell.css'; -function TimeleftCell(props) { +interface TimeleftCellProps { + estimatedCompletionTime?: string; + timeleft?: string; + status: string; + size: number; + sizeleft: number; + showRelativeDates: boolean; + shortDateFormat: string; + timeFormat: string; +} + +function TimeleftCell(props: TimeleftCellProps) { const { estimatedCompletionTime, timeleft, @@ -20,16 +30,18 @@ function TimeleftCell(props) { sizeleft, showRelativeDates, shortDateFormat, - timeFormat + timeFormat, } = props; if (status === 'delay') { const date = getRelativeDate({ date: estimatedCompletionTime, shortDateFormat, - showRelativeDates + showRelativeDates, + }); + const time = formatTime(estimatedCompletionTime, timeFormat, { + includeMinuteZero: true, }); - const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( <TableRowCell className={styles.timeleft}> @@ -47,9 +59,11 @@ function TimeleftCell(props) { const date = getRelativeDate({ date: estimatedCompletionTime, shortDateFormat, - showRelativeDates + showRelativeDates, + }); + const time = formatTime(estimatedCompletionTime, timeFormat, { + includeMinuteZero: true, }); - const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( <TableRowCell className={styles.timeleft}> @@ -64,11 +78,7 @@ function TimeleftCell(props) { } if (!timeleft || status === 'completed' || status === 'failed') { - return ( - <TableRowCell className={styles.timeleft}> - - - </TableRowCell> - ); + return <TableRowCell className={styles.timeleft}>-</TableRowCell>; } const totalSize = formatBytes(size); @@ -84,15 +94,4 @@ function TimeleftCell(props) { ); } -TimeleftCell.propTypes = { - estimatedCompletionTime: PropTypes.string, - timeleft: PropTypes.string, - status: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - sizeleft: PropTypes.number.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired -}; - export default TimeleftCell; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 4a8330a6c..a5bb0b33c 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -3,7 +3,7 @@ import React from 'react'; import { Redirect, Route } from 'react-router-dom'; import Blocklist from 'Activity/Blocklist/Blocklist'; import History from 'Activity/History/History'; -import QueueConnector from 'Activity/Queue/QueueConnector'; +import Queue from 'Activity/Queue/Queue'; import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector'; import ImportSeries from 'AddSeries/ImportSeries/ImportSeries'; import CalendarPageConnector from 'Calendar/CalendarPageConnector'; @@ -130,7 +130,7 @@ function AppRoutes(props) { <Route path="/activity/queue" - component={QueueConnector} + component={Queue} /> <Route diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index b72e95760..6e0893926 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -46,6 +46,8 @@ export interface CustomFilter { } export interface AppSectionState { + isConnected: boolean; + isReconnecting: boolean; version: string; dimensions: { isSmallScreen: boolean; diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts index 05d74acac..954d649a2 100644 --- a/frontend/src/App/State/QueueAppState.ts +++ b/frontend/src/App/State/QueueAppState.ts @@ -3,15 +3,29 @@ import AppSectionState, { AppSectionFilterState, AppSectionItemState, Error, + PagedAppSectionState, + TableAppSectionState, } from './AppSectionState'; +export interface QueueStatus { + totalCount: number; + count: number; + unknownCount: number; + errors: boolean; + warnings: boolean; + unknownErrors: boolean; + unknownWarnings: boolean; +} + export interface QueueDetailsAppState extends AppSectionState<Queue> { params: unknown; } export interface QueuePagedAppState extends AppSectionState<Queue>, - AppSectionFilterState<Queue> { + AppSectionFilterState<Queue>, + PagedAppSectionState, + TableAppSectionState { isGrabbing: boolean; grabError: Error; isRemoving: boolean; @@ -19,9 +33,12 @@ export interface QueuePagedAppState } interface QueueAppState { - status: AppSectionItemState<Queue>; + status: AppSectionItemState<QueueStatus>; details: QueueDetailsAppState; paged: QueuePagedAppState; + options: { + includeUnknownSeriesItems: boolean; + }; } export default QueueAppState; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 614281c26..bf618a87d 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector'; +import QueueStatus from 'Activity/Queue/Status/QueueStatus'; import OverlayScroller from 'Components/Scroller/OverlayScroller'; import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; @@ -50,7 +50,7 @@ const links = [ { title: () => translate('Queue'), to: '/activity/queue', - statusComponent: QueueStatusConnector + statusComponent: QueueStatus }, { title: () => translate('History'), diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts index 4f32ffe7f..7f80ae1b3 100644 --- a/frontend/src/Episode/useEpisode.ts +++ b/frontend/src/Episode/useEpisode.ts @@ -9,7 +9,7 @@ export type EpisodeEntities = | 'cutoffUnmet' | 'missing'; -function createEpisodeSelector(episodeId: number) { +function createEpisodeSelector(episodeId?: number) { return createSelector( (state: AppState) => state.episodes.items, (episodes) => { @@ -18,7 +18,7 @@ function createEpisodeSelector(episodeId: number) { ); } -function createCalendarEpisodeSelector(episodeId: number) { +function createCalendarEpisodeSelector(episodeId?: number) { return createSelector( (state: AppState) => state.calendar.items, (episodes) => { @@ -27,7 +27,10 @@ function createCalendarEpisodeSelector(episodeId: number) { ); } -function useEpisode(episodeId: number, episodeEntity: EpisodeEntities) { +function useEpisode( + episodeId: number | undefined, + episodeEntity: EpisodeEntities +) { let selector = createEpisodeSelector; switch (episodeEntity) { diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts index 7a9351ac6..885c73470 100644 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ b/frontend/src/Helpers/Props/TooltipPosition.ts @@ -1,8 +1,3 @@ -enum TooltipPosition { - Top = 'top', - Right = 'right', - Bottom = 'bottom', - Left = 'left', -} +type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; export default TooltipPosition; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 6120c86af..58a98c86d 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -137,14 +137,13 @@ function getInfoRowProps( date: formatDateTime(previousAiring, longDateFormat, timeFormat), }), iconName: icons.CALENDAR, - label: - getRelativeDate({ - date: previousAiring, - shortDateFormat, - showRelativeDates, - timeFormat, - timeForToday: true, - }) ?? '', + label: getRelativeDate({ + date: previousAiring, + shortDateFormat, + showRelativeDates, + timeFormat, + timeForToday: true, + }), }; } diff --git a/frontend/src/Series/Index/Table/SeasonsCell.tsx b/frontend/src/Series/Index/Table/SeasonsCell.tsx index fdf12c2ed..1078a622d 100644 --- a/frontend/src/Series/Index/Table/SeasonsCell.tsx +++ b/frontend/src/Series/Index/Table/SeasonsCell.tsx @@ -1,7 +1,6 @@ import React from 'react'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import Popover from 'Components/Tooltip/Popover'; -import TooltipPosition from 'Helpers/Props/TooltipPosition'; import SeasonDetails from 'Series/Index/Select/SeasonPass/SeasonDetails'; import { Season } from 'Series/Series'; import translate from 'Utilities/String/translate'; @@ -33,7 +32,7 @@ function SeasonsCell(props: SeriesStatusCellProps) { anchor={seasonCount} title={translate('SeasonDetails')} body={<SeasonDetails seriesId={seriesId} seasons={seasons} />} - position={TooltipPosition.Left} + position="left" /> ) : ( seasonCount diff --git a/frontend/src/Utilities/Date/getRelativeDate.tsx b/frontend/src/Utilities/Date/getRelativeDate.ts similarity index 99% rename from frontend/src/Utilities/Date/getRelativeDate.tsx rename to frontend/src/Utilities/Date/getRelativeDate.ts index 3a7f4f143..509f1f39d 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.tsx +++ b/frontend/src/Utilities/Date/getRelativeDate.ts @@ -27,7 +27,7 @@ function getRelativeDate({ includeTime = false, }: GetRelativeDateOptions) { if (!date) { - return null; + return ''; } if ((includeTime || timeForToday) && !timeFormat) { diff --git a/frontend/src/typings/Queue.ts b/frontend/src/typings/Queue.ts index 8fe114489..dd266d132 100644 --- a/frontend/src/typings/Queue.ts +++ b/frontend/src/typings/Queue.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; @@ -7,6 +8,7 @@ export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error'; export type QueueTrackedDownloadState = | 'downloading' + | 'importBlocked' | 'importPending' | 'importing' | 'imported' @@ -23,6 +25,7 @@ interface Queue extends ModelBase { languages: Language[]; quality: QualityModel; customFormats: CustomFormat[]; + customFormatScore: number; size: number; title: string; sizeleft: number; @@ -35,13 +38,14 @@ interface Queue extends ModelBase { statusMessages: StatusMessage[]; errorMessage: string; downloadId: string; - protocol: string; + protocol: DownloadProtocol; downloadClient: string; outputPath: string; episodeHasFile: boolean; seriesId?: number; episodeId?: number; seasonNumber?: number; + downloadClientHasPostImportCategory: boolean; } export default Queue; From d46f4b215483e3a902454aa706336f3869328b3e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 23 Jul 2024 15:52:44 -0700 Subject: [PATCH 414/762] Convert Utilities to TypeScript --- .vscode/settings.json | 3 + frontend/src/App/State/AppSectionState.ts | 7 ++ frontend/src/App/State/AppState.ts | 4 +- frontend/src/App/State/SettingsAppState.ts | 4 +- frontend/src/Commands/Command.ts | 16 +++- .../Quality/SelectQualityModalContent.tsx | 12 +-- frontend/src/Series/Series.ts | 24 ++++++ .../createServerSideCollectionHandlers.js | 4 +- ...reateSetServerSideCollectionPageHandler.js | 2 +- .../Actions/Settings/importListExclusions.js | 2 +- .../src/Store/Actions/blocklistActions.js | 2 +- frontend/src/Store/Actions/historyActions.js | 2 +- frontend/src/Store/Actions/queueActions.js | 2 +- frontend/src/Store/Actions/systemActions.js | 2 +- frontend/src/Store/Actions/wantedActions.js | 2 +- .../createCommandExecutingSelector.ts | 2 +- .../Array/getIndexOfFirstCharacter.js | 11 --- .../Array/getIndexOfFirstCharacter.ts | 18 ++++ .../{findCommand.js => findCommand.ts} | 3 +- .../Utilities/Command/{index.js => index.ts} | 0 .../Utilities/Command/isCommandComplete.js | 9 -- .../Utilities/Command/isCommandComplete.ts | 11 +++ ...mandExecuting.js => isCommandExecuting.ts} | 4 +- .../src/Utilities/Command/isCommandFailed.js | 12 --- .../src/Utilities/Command/isCommandFailed.ts | 16 ++++ .../src/Utilities/Command/isSameCommand.js | 24 ------ .../src/Utilities/Command/isSameCommand.ts | 50 +++++++++++ .../Constants/{keyCodes.js => keyCodes.ts} | 0 .../{updateEpisodes.js => updateEpisodes.ts} | 13 ++- .../Utilities/Filter/findSelectedFilters.js | 19 ----- .../Utilities/Filter/findSelectedFilters.ts | 27 ++++++ .../{getFilterValue.js => getFilterValue.ts} | 9 +- .../{convertToBytes.js => convertToBytes.ts} | 3 +- frontend/src/Utilities/Number/formatAge.js | 19 ----- frontend/src/Utilities/Number/formatAge.ts | 33 ++++++++ .../Number/{formatBytes.js => formatBytes.ts} | 8 +- frontend/src/Utilities/Number/padNumber.js | 10 --- frontend/src/Utilities/Number/padNumber.ts | 13 +++ .../Number/{roundNumber.js => roundNumber.ts} | 2 +- ...{getErrorMessage.js => getErrorMessage.ts} | 10 ++- .../src/Utilities/Object/getRemovedItems.js | 15 ---- ...DifferentItems.js => hasDifferentItems.ts} | 8 +- ...OrOrder.js => hasDifferentItemsOrOrder.ts} | 8 +- .../src/Utilities/Quality/getQualities.js | 16 ---- .../src/Utilities/Quality/getQualities.ts | 26 ++++++ frontend/src/Utilities/ResolutionUtility.js | 26 ------ frontend/src/Utilities/ResolutionUtility.ts | 24 ++++++ .../Utilities/Series/filterAlternateTitles.js | 53 ------------ .../Utilities/Series/filterAlternateTitles.ts | 83 +++++++++++++++++++ .../{getNewSeries.js => getNewSeries.ts} | 23 ++++- ...msOptions.js => monitorNewItemsOptions.ts} | 6 +- .../{monitorOptions.js => monitorOptions.ts} | 24 +++--- .../Series/{seriesTypes.js => seriesTypes.ts} | 0 frontend/src/Utilities/State/getNextId.js | 5 -- frontend/src/Utilities/State/getNextId.ts | 7 ++ frontend/src/Utilities/{ => State}/pages.js | 0 .../serverSideCollectionHandlers.js | 0 frontend/src/Utilities/String/combinePath.js | 5 -- frontend/src/Utilities/String/combinePath.ts | 9 ++ .../src/Utilities/String/generateUUIDv4.js | 6 -- .../String/{isString.js => isString.ts} | 2 +- ...aturalExpansion.js => naturalExpansion.ts} | 2 +- .../String/{parseUrl.js => parseUrl.ts} | 8 +- frontend/src/Utilities/String/split.js | 17 ---- frontend/src/Utilities/String/split.ts | 15 ++++ .../String/{titleCase.js => titleCase.ts} | 2 +- .../src/Utilities/Table/areAllSelected.js | 17 ---- .../src/Utilities/Table/areAllSelected.ts | 19 +++++ .../src/Utilities/Table/getToggledRange.js | 23 ----- .../src/Utilities/Table/getToggledRange.ts | 27 ++++++ .../Utilities/Table/removeOldSelectedState.js | 16 ---- .../Utilities/Table/removeOldSelectedState.ts | 21 +++++ frontend/src/Utilities/Table/selectAll.js | 17 ---- frontend/src/Utilities/Table/selectAll.ts | 19 +++++ .../{toggleSelected.js => toggleSelected.ts} | 18 ++-- .../src/Utilities/{browser.js => browser.ts} | 1 - frontend/src/Utilities/getPathWithUrlBase.js | 3 - frontend/src/Utilities/getPathWithUrlBase.ts | 3 + ...iqueElementId.js => getUniqueElementId.ts} | 0 .../{pagePopulator.js => pagePopulator.ts} | 15 ++-- frontend/src/Utilities/requestAction.js | 9 +- .../{scrollLock.js => scrollLock.ts} | 2 +- frontend/src/Utilities/sectionTypes.js | 6 -- package.json | 1 + yarn.lock | 5 ++ 85 files changed, 614 insertions(+), 412 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 frontend/src/Utilities/Array/getIndexOfFirstCharacter.js create mode 100644 frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts rename frontend/src/Utilities/Command/{findCommand.js => findCommand.ts} (60%) rename frontend/src/Utilities/Command/{index.js => index.ts} (100%) delete mode 100644 frontend/src/Utilities/Command/isCommandComplete.js create mode 100644 frontend/src/Utilities/Command/isCommandComplete.ts rename frontend/src/Utilities/Command/{isCommandExecuting.js => isCommandExecuting.ts} (62%) delete mode 100644 frontend/src/Utilities/Command/isCommandFailed.js create mode 100644 frontend/src/Utilities/Command/isCommandFailed.ts delete mode 100644 frontend/src/Utilities/Command/isSameCommand.js create mode 100644 frontend/src/Utilities/Command/isSameCommand.ts rename frontend/src/Utilities/Constants/{keyCodes.js => keyCodes.ts} (100%) rename frontend/src/Utilities/Episode/{updateEpisodes.js => updateEpisodes.ts} (54%) delete mode 100644 frontend/src/Utilities/Filter/findSelectedFilters.js create mode 100644 frontend/src/Utilities/Filter/findSelectedFilters.ts rename frontend/src/Utilities/Filter/{getFilterValue.js => getFilterValue.ts} (53%) rename frontend/src/Utilities/Number/{convertToBytes.js => convertToBytes.ts} (76%) delete mode 100644 frontend/src/Utilities/Number/formatAge.js create mode 100644 frontend/src/Utilities/Number/formatAge.ts rename frontend/src/Utilities/Number/{formatBytes.js => formatBytes.ts} (66%) delete mode 100644 frontend/src/Utilities/Number/padNumber.js create mode 100644 frontend/src/Utilities/Number/padNumber.ts rename frontend/src/Utilities/Number/{roundNumber.js => roundNumber.ts} (59%) rename frontend/src/Utilities/Object/{getErrorMessage.js => getErrorMessage.ts} (53%) delete mode 100644 frontend/src/Utilities/Object/getRemovedItems.js rename frontend/src/Utilities/Object/{hasDifferentItems.js => hasDifferentItems.ts} (70%) rename frontend/src/Utilities/Object/{hasDifferentItemsOrOrder.js => hasDifferentItemsOrOrder.ts} (67%) delete mode 100644 frontend/src/Utilities/Quality/getQualities.js create mode 100644 frontend/src/Utilities/Quality/getQualities.ts delete mode 100644 frontend/src/Utilities/ResolutionUtility.js create mode 100644 frontend/src/Utilities/ResolutionUtility.ts delete mode 100644 frontend/src/Utilities/Series/filterAlternateTitles.js create mode 100644 frontend/src/Utilities/Series/filterAlternateTitles.ts rename frontend/src/Utilities/Series/{getNewSeries.js => getNewSeries.ts} (52%) rename frontend/src/Utilities/Series/{monitorNewItemsOptions.js => monitorNewItemsOptions.ts} (94%) rename frontend/src/Utilities/Series/{monitorOptions.js => monitorOptions.ts} (93%) rename frontend/src/Utilities/Series/{seriesTypes.js => seriesTypes.ts} (100%) delete mode 100644 frontend/src/Utilities/State/getNextId.js create mode 100644 frontend/src/Utilities/State/getNextId.ts rename frontend/src/Utilities/{ => State}/pages.js (100%) rename frontend/src/Utilities/{ => State}/serverSideCollectionHandlers.js (100%) delete mode 100644 frontend/src/Utilities/String/combinePath.js create mode 100644 frontend/src/Utilities/String/combinePath.ts delete mode 100644 frontend/src/Utilities/String/generateUUIDv4.js rename frontend/src/Utilities/String/{isString.js => isString.ts} (58%) rename frontend/src/Utilities/String/{naturalExpansion.js => naturalExpansion.ts} (78%) rename frontend/src/Utilities/String/{parseUrl.js => parseUrl.ts} (72%) delete mode 100644 frontend/src/Utilities/String/split.js create mode 100644 frontend/src/Utilities/String/split.ts rename frontend/src/Utilities/String/{titleCase.js => titleCase.ts} (81%) delete mode 100644 frontend/src/Utilities/Table/areAllSelected.js create mode 100644 frontend/src/Utilities/Table/areAllSelected.ts delete mode 100644 frontend/src/Utilities/Table/getToggledRange.js create mode 100644 frontend/src/Utilities/Table/getToggledRange.ts delete mode 100644 frontend/src/Utilities/Table/removeOldSelectedState.js create mode 100644 frontend/src/Utilities/Table/removeOldSelectedState.ts delete mode 100644 frontend/src/Utilities/Table/selectAll.js create mode 100644 frontend/src/Utilities/Table/selectAll.ts rename frontend/src/Utilities/Table/{toggleSelected.js => toggleSelected.ts} (57%) rename frontend/src/Utilities/{browser.js => browser.ts} (99%) delete mode 100644 frontend/src/Utilities/getPathWithUrlBase.js create mode 100644 frontend/src/Utilities/getPathWithUrlBase.ts rename frontend/src/Utilities/{getUniqueElementId.js => getUniqueElementId.ts} (100%) rename frontend/src/Utilities/{pagePopulator.js => pagePopulator.ts} (51%) rename frontend/src/Utilities/{scrollLock.js => scrollLock.ts} (85%) delete mode 100644 frontend/src/Utilities/sectionTypes.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..44aeb4060 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..18f0adf50 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -43,6 +43,13 @@ export interface AppSectionSchemaState<T> { }; } +export interface AppSectionItemSchemaState<T> { + isSchemaFetching: boolean; + isSchemaPopulated: boolean; + schemaError: Error; + schema: T; +} + export interface AppSectionItemState<T> { isFetching: boolean; isPopulated: boolean; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 6e0893926..744f11f31 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -35,14 +35,14 @@ export interface PropertyFilter { export interface Filter { key: string; label: string; - filers: PropertyFilter[]; + filters: PropertyFilter[]; } export interface CustomFilter { id: number; type: string; label: string; - filers: PropertyFilter[]; + filters: PropertyFilter[]; } export interface AppSectionState { diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index d6624ff74..0959e99c5 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -1,8 +1,8 @@ import AppSectionState, { AppSectionDeleteState, + AppSectionItemSchemaState, AppSectionItemState, AppSectionSaveState, - AppSectionSchemaState, PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; @@ -40,7 +40,7 @@ export interface NotificationAppState export interface QualityProfilesAppState extends AppSectionState<QualityProfile>, - AppSectionSchemaState<QualityProfile> {} + AppSectionItemSchemaState<QualityProfile> {} export interface ImportListOptionsSettingsAppState extends AppSectionItemState<ImportListOptionsSettings>, diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index 0830fd34b..d0797a79a 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -1,5 +1,16 @@ import ModelBase from 'App/ModelBase'; +export type CommandStatus = + | 'queued' + | 'started' + | 'completed' + | 'failed' + | 'aborted' + | 'cancelled' + | 'orphaned'; + +export type CommandResult = 'unknown' | 'successful' | 'unsuccessful'; + export interface CommandBody { sendUpdatesToClient: boolean; updateScheduledTask: boolean; @@ -15,6 +26,7 @@ export interface CommandBody { seriesId?: number; seriesIds?: number[]; seasonNumber?: number; + [key: string]: string | number | boolean | undefined | number[] | undefined; } interface Command extends ModelBase { @@ -23,8 +35,8 @@ interface Command extends ModelBase { message: string; body: CommandBody; priority: string; - status: string; - result: string; + status: CommandStatus; + result: CommandResult; queued: string; started: string; ended: string; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx index edb65663c..5d4d561b0 100644 --- a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import { Error } from 'App/State/AppSectionState'; import AppState from 'App/State/AppState'; import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; @@ -21,21 +20,14 @@ import { CheckInputChanged } from 'typings/inputs'; import getQualities from 'Utilities/Quality/getQualities'; import translate from 'Utilities/String/translate'; -interface QualitySchemaState { - isFetching: boolean; - isPopulated: boolean; - error: Error; - items: Quality[]; -} - function createQualitySchemaSelector() { return createSelector( (state: AppState) => state.settings.qualityProfiles, - (qualityProfiles): QualitySchemaState => { + (qualityProfiles) => { const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = qualityProfiles; - const items = getQualities(schema.items) as Quality[]; + const items = getQualities(schema.items); return { isFetching: isSchemaFetching, diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index 321fc7378..c93ccf3ff 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -2,6 +2,20 @@ import ModelBase from 'App/ModelBase'; import Language from 'Language/Language'; export type SeriesType = 'anime' | 'daily' | 'standard'; +export type SeriesMonitor = + | 'all' + | 'future' + | 'missing' + | 'existing' + | 'recent' + | 'pilot' + | 'firstSeason' + | 'lastSeason' + | 'monitorSpecials' + | 'unmonitorSpecials' + | 'none'; + +export type MonitorNewItems = 'all' | 'none'; export interface Image { coverType: string; @@ -34,7 +48,15 @@ export interface Ratings { export interface AlternateTitle { seasonNumber: number; + sceneSeasonNumber?: number; title: string; + sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb'; +} + +export interface SeriesAddOptions { + monitor: SeriesMonitor; + searchForMissingEpisodes: boolean; + searchForCutoffUnmetEpisodes: boolean; } interface Series extends ModelBase { @@ -48,6 +70,7 @@ interface Series extends ModelBase { images: Image[]; imdbId: string; monitored: boolean; + monitorNewItems: MonitorNewItems; network: string; originalLanguage: Language; overview: string; @@ -74,6 +97,7 @@ interface Series extends ModelBase { useSceneNumbering: boolean; year: number; isSaving?: boolean; + addOptions: SeriesAddOptions; } export default Series; diff --git a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js index 8b4697377..9d0615509 100644 --- a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js +++ b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js @@ -1,5 +1,5 @@ -import pages from 'Utilities/pages'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import pages from 'Utilities/State/pages'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler'; import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler'; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js index 12b21bb0d..069de8d5b 100644 --- a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js @@ -1,5 +1,5 @@ -import pages from 'Utilities/pages'; import getSectionState from 'Utilities/State/getSectionState'; +import pages from 'Utilities/State/pages'; function createSetServerSideCollectionPageHandler(section, page, fetchHandler) { return function(getState, payload, dispatch) { diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index d6371946f..3af8bf9ec 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -5,7 +5,7 @@ import createServerSideCollectionHandlers from 'Store/Actions/Creators/createSer import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import { createThunk, handleThunks } from 'Store/thunks'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; // // Variables diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js index 87ffe7f7c..e188a0380 100644 --- a/frontend/src/Store/Actions/blocklistActions.js +++ b/frontend/src/Store/Actions/blocklistActions.js @@ -3,7 +3,7 @@ import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import translate from 'Utilities/String/translate'; import { set, updateItem } from './baseActions'; import createHandleActions from './Creators/createHandleActions'; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 3e773eca8..45d858249 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -4,7 +4,7 @@ import Icon from 'Components/Icon'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import translate from 'Utilities/String/translate'; import { updateItem } from './baseActions'; import createHandleActions from './Creators/createHandleActions'; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index dff490d12..5f91318ad 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -6,7 +6,7 @@ import Icon from 'Components/Icon'; import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import translate from 'Utilities/String/translate'; import { set, updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 92360b589..0f2410846 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -3,7 +3,7 @@ import { filterTypes, sortDirections } from 'Helpers/Props'; import { setAppValue } from 'Store/Actions/appActions'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import translate from 'Utilities/String/translate'; import { pingServer } from './appActions'; import { set } from './baseActions'; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 21bfcd3c0..bb39416aa 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -1,7 +1,7 @@ import { createAction } from 'redux-actions'; import { filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; import translate from 'Utilities/String/translate'; import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler'; import createHandleActions from './Creators/createHandleActions'; diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts index 6a80e172b..dd5071b9d 100644 --- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts @@ -4,7 +4,7 @@ import createCommandSelector from './createCommandSelector'; function createCommandExecutingSelector(name: string, contraints = {}) { return createSelector(createCommandSelector(name, contraints), (command) => { - return isCommandExecuting(command); + return command ? isCommandExecuting(command) : false; }); } diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js deleted file mode 100644 index 5cbb30085..000000000 --- a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function getIndexOfFirstCharacter(items, character) { - return items.findIndex((item) => { - const firstCharacter = item.sortTitle.charAt(0); - - if (character === '#') { - return !isNaN(firstCharacter); - } - - return firstCharacter === character; - }); -} diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts new file mode 100644 index 000000000..8e4c1f308 --- /dev/null +++ b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts @@ -0,0 +1,18 @@ +import Series from 'Series/Series'; + +const STARTS_WITH_NUMBER_REGEX = /^\d/; + +export default function getIndexOfFirstCharacter( + items: Series[], + character: string +) { + return items.findIndex((item) => { + const firstCharacter = item.sortTitle.charAt(0); + + if (character === '#') { + return STARTS_WITH_NUMBER_REGEX.test(firstCharacter); + } + + return firstCharacter === character; + }); +} diff --git a/frontend/src/Utilities/Command/findCommand.js b/frontend/src/Utilities/Command/findCommand.ts similarity index 60% rename from frontend/src/Utilities/Command/findCommand.js rename to frontend/src/Utilities/Command/findCommand.ts index cf7d5444a..fad9e59fe 100644 --- a/frontend/src/Utilities/Command/findCommand.js +++ b/frontend/src/Utilities/Command/findCommand.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; +import Command, { CommandBody } from 'Commands/Command'; import isSameCommand from './isSameCommand'; -function findCommand(commands, options) { +function findCommand(commands: Command[], options: Partial<CommandBody>) { return _.findLast(commands, (command) => { return isSameCommand(command.body, options); }); diff --git a/frontend/src/Utilities/Command/index.js b/frontend/src/Utilities/Command/index.ts similarity index 100% rename from frontend/src/Utilities/Command/index.js rename to frontend/src/Utilities/Command/index.ts diff --git a/frontend/src/Utilities/Command/isCommandComplete.js b/frontend/src/Utilities/Command/isCommandComplete.js deleted file mode 100644 index 558ab801b..000000000 --- a/frontend/src/Utilities/Command/isCommandComplete.js +++ /dev/null @@ -1,9 +0,0 @@ -function isCommandComplete(command) { - if (!command) { - return false; - } - - return command.status === 'complete'; -} - -export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandComplete.ts b/frontend/src/Utilities/Command/isCommandComplete.ts new file mode 100644 index 000000000..678023737 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandComplete.ts @@ -0,0 +1,11 @@ +import Command from 'Commands/Command'; + +function isCommandComplete(command: Command) { + if (!command) { + return false; + } + + return command.status === 'completed'; +} + +export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandExecuting.js b/frontend/src/Utilities/Command/isCommandExecuting.ts similarity index 62% rename from frontend/src/Utilities/Command/isCommandExecuting.js rename to frontend/src/Utilities/Command/isCommandExecuting.ts index 8e637704e..7da31bdee 100644 --- a/frontend/src/Utilities/Command/isCommandExecuting.js +++ b/frontend/src/Utilities/Command/isCommandExecuting.ts @@ -1,4 +1,6 @@ -function isCommandExecuting(command) { +import Command from 'Commands/Command'; + +function isCommandExecuting(command?: Command) { if (!command) { return false; } diff --git a/frontend/src/Utilities/Command/isCommandFailed.js b/frontend/src/Utilities/Command/isCommandFailed.js deleted file mode 100644 index 00e5ccdf2..000000000 --- a/frontend/src/Utilities/Command/isCommandFailed.js +++ /dev/null @@ -1,12 +0,0 @@ -function isCommandFailed(command) { - if (!command) { - return false; - } - - return command.status === 'failed' || - command.status === 'aborted' || - command.status === 'cancelled' || - command.status === 'orphaned'; -} - -export default isCommandFailed; diff --git a/frontend/src/Utilities/Command/isCommandFailed.ts b/frontend/src/Utilities/Command/isCommandFailed.ts new file mode 100644 index 000000000..4e88b95c9 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandFailed.ts @@ -0,0 +1,16 @@ +import Command from 'Commands/Command'; + +function isCommandFailed(command: Command) { + if (!command) { + return false; + } + + return ( + command.status === 'failed' || + command.status === 'aborted' || + command.status === 'cancelled' || + command.status === 'orphaned' + ); +} + +export default isCommandFailed; diff --git a/frontend/src/Utilities/Command/isSameCommand.js b/frontend/src/Utilities/Command/isSameCommand.js deleted file mode 100644 index d0acb24b5..000000000 --- a/frontend/src/Utilities/Command/isSameCommand.js +++ /dev/null @@ -1,24 +0,0 @@ -import _ from 'lodash'; - -function isSameCommand(commandA, commandB) { - if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) { - return false; - } - - for (const key in commandB) { - if (key !== 'name') { - const value = commandB[key]; - if (Array.isArray(value)) { - if (_.difference(value, commandA[key]).length > 0) { - return false; - } - } else if (value !== commandA[key]) { - return false; - } - } - } - - return true; -} - -export default isSameCommand; diff --git a/frontend/src/Utilities/Command/isSameCommand.ts b/frontend/src/Utilities/Command/isSameCommand.ts new file mode 100644 index 000000000..cbe18aa8f --- /dev/null +++ b/frontend/src/Utilities/Command/isSameCommand.ts @@ -0,0 +1,50 @@ +import { CommandBody } from 'Commands/Command'; + +function isSameCommand( + commandA: Partial<CommandBody>, + commandB: Partial<CommandBody> +) { + if ( + commandA.name?.toLocaleLowerCase() !== commandB.name?.toLocaleLowerCase() + ) { + return false; + } + + for (const key in commandB) { + if (key !== 'name') { + const value = commandB[key]; + + if (Array.isArray(value)) { + const sortedB = [...value].sort((a, b) => a - b); + const commandAProp = commandA[key]; + const sortedA = Array.isArray(commandAProp) + ? [...commandAProp].sort((a, b) => a - b) + : []; + + if (sortedA === sortedB) { + return true; + } + + if (sortedA == null || sortedB == null) { + return false; + } + + if (sortedA.length !== sortedB.length) { + return false; + } + + for (let i = 0; i < sortedB.length; ++i) { + if (sortedB[i] !== sortedA[i]) { + return false; + } + } + } else if (value !== commandA[key]) { + return false; + } + } + } + + return true; +} + +export default isSameCommand; diff --git a/frontend/src/Utilities/Constants/keyCodes.js b/frontend/src/Utilities/Constants/keyCodes.ts similarity index 100% rename from frontend/src/Utilities/Constants/keyCodes.js rename to frontend/src/Utilities/Constants/keyCodes.ts diff --git a/frontend/src/Utilities/Episode/updateEpisodes.js b/frontend/src/Utilities/Episode/updateEpisodes.ts similarity index 54% rename from frontend/src/Utilities/Episode/updateEpisodes.js rename to frontend/src/Utilities/Episode/updateEpisodes.ts index 80890b53f..d6a9e9eb4 100644 --- a/frontend/src/Utilities/Episode/updateEpisodes.js +++ b/frontend/src/Utilities/Episode/updateEpisodes.ts @@ -1,12 +1,17 @@ -import _ from 'lodash'; +import Episode from 'Episode/Episode'; import { update } from 'Store/Actions/baseActions'; -function updateEpisodes(section, episodes, episodeIds, options) { - const data = _.reduce(episodes, (result, item) => { +function updateEpisodes( + section: string, + episodes: Episode[], + episodeIds: number[], + options: Partial<Episode> +) { + const data = episodes.reduce<Episode[]>((result, item) => { if (episodeIds.indexOf(item.id) > -1) { result.push({ ...item, - ...options + ...options, }); } else { result.push(item); diff --git a/frontend/src/Utilities/Filter/findSelectedFilters.js b/frontend/src/Utilities/Filter/findSelectedFilters.js deleted file mode 100644 index 1c104073c..000000000 --- a/frontend/src/Utilities/Filter/findSelectedFilters.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function findSelectedFilters(selectedFilterKey, filters = [], customFilters = []) { - if (!selectedFilterKey) { - return []; - } - - let selectedFilter = filters.find((f) => f.key === selectedFilterKey); - - if (!selectedFilter) { - selectedFilter = customFilters.find((f) => f.id === selectedFilterKey); - } - - if (!selectedFilter) { - // TODO: throw in dev - console.error('Matching filter not found'); - return []; - } - - return selectedFilter.filters; -} diff --git a/frontend/src/Utilities/Filter/findSelectedFilters.ts b/frontend/src/Utilities/Filter/findSelectedFilters.ts new file mode 100644 index 000000000..89211f628 --- /dev/null +++ b/frontend/src/Utilities/Filter/findSelectedFilters.ts @@ -0,0 +1,27 @@ +import { CustomFilter, Filter } from 'App/State/AppState'; + +export default function findSelectedFilters( + selectedFilterKey: string | number, + filters: Filter[] = [], + customFilters: CustomFilter[] = [] +) { + if (!selectedFilterKey) { + return []; + } + + let selectedFilter: Filter | CustomFilter | undefined = filters.find( + (f) => f.key === selectedFilterKey + ); + + if (!selectedFilter) { + selectedFilter = customFilters.find((f) => f.id === selectedFilterKey); + } + + if (!selectedFilter) { + // TODO: throw in dev + console.error('Matching filter not found'); + return []; + } + + return selectedFilter.filters; +} diff --git a/frontend/src/Utilities/Filter/getFilterValue.js b/frontend/src/Utilities/Filter/getFilterValue.ts similarity index 53% rename from frontend/src/Utilities/Filter/getFilterValue.js rename to frontend/src/Utilities/Filter/getFilterValue.ts index 70b0b51f1..95c078e48 100644 --- a/frontend/src/Utilities/Filter/getFilterValue.js +++ b/frontend/src/Utilities/Filter/getFilterValue.ts @@ -1,4 +1,11 @@ -export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) { +import { Filter } from 'App/State/AppState'; + +export default function getFilterValue( + filters: Filter[], + filterKey: string | number, + filterValueKey: string, + defaultValue: string | number | boolean +) { const filter = filters.find((f) => f.key === filterKey); if (!filter) { diff --git a/frontend/src/Utilities/Number/convertToBytes.js b/frontend/src/Utilities/Number/convertToBytes.ts similarity index 76% rename from frontend/src/Utilities/Number/convertToBytes.js rename to frontend/src/Utilities/Number/convertToBytes.ts index 6c63fb117..53dbc27dd 100644 --- a/frontend/src/Utilities/Number/convertToBytes.js +++ b/frontend/src/Utilities/Number/convertToBytes.ts @@ -1,5 +1,4 @@ - -function convertToBytes(input, power, binaryPrefix) { +function convertToBytes(input: number, power: number, binaryPrefix: boolean) { const size = Number(input); if (isNaN(size)) { diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js deleted file mode 100644 index a8f0e9f65..000000000 --- a/frontend/src/Utilities/Number/formatAge.js +++ /dev/null @@ -1,19 +0,0 @@ -import translate from 'Utilities/String/translate'; - -function formatAge(age, ageHours, ageMinutes) { - age = Math.round(age); - ageHours = parseFloat(ageHours); - ageMinutes = ageMinutes && parseFloat(ageMinutes); - - if (age < 2 && ageHours) { - if (ageHours < 2 && !!ageMinutes) { - return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? translate('FormatAgeMinute') : translate('FormatAgeMinutes')}`; - } - - return `${ageHours.toFixed(1)} ${ageHours === 1 ? translate('FormatAgeHour') : translate('FormatAgeHours')}`; - } - - return `${age} ${age === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays')}`; -} - -export default formatAge; diff --git a/frontend/src/Utilities/Number/formatAge.ts b/frontend/src/Utilities/Number/formatAge.ts new file mode 100644 index 000000000..e114cd9b4 --- /dev/null +++ b/frontend/src/Utilities/Number/formatAge.ts @@ -0,0 +1,33 @@ +import translate from 'Utilities/String/translate'; + +function formatAge( + age: string | number, + ageHours: string | number, + ageMinutes: string | number +) { + const ageRounded = Math.round(Number(age)); + const ageHoursFloat = parseFloat(String(ageHours)); + const ageMinutesFloat = ageMinutes && parseFloat(String(ageMinutes)); + + if (ageRounded < 2 && ageHoursFloat) { + if (ageHoursFloat < 2 && !!ageMinutesFloat) { + return `${ageMinutesFloat.toFixed(0)} ${ + ageHoursFloat === 1 + ? translate('FormatAgeMinute') + : translate('FormatAgeMinutes') + }`; + } + + return `${ageHoursFloat.toFixed(1)} ${ + ageHoursFloat === 1 + ? translate('FormatAgeHour') + : translate('FormatAgeHours') + }`; + } + + return `${ageRounded} ${ + ageRounded === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays') + }`; +} + +export default formatAge; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.ts similarity index 66% rename from frontend/src/Utilities/Number/formatBytes.js rename to frontend/src/Utilities/Number/formatBytes.ts index d4d389357..bccf7435a 100644 --- a/frontend/src/Utilities/Number/formatBytes.js +++ b/frontend/src/Utilities/Number/formatBytes.ts @@ -1,6 +1,10 @@ import { filesize } from 'filesize'; -function formatBytes(input) { +function formatBytes(input?: string | number) { + if (!input) { + return ''; + } + const size = Number(input); if (isNaN(size)) { @@ -9,7 +13,7 @@ function formatBytes(input) { return `${filesize(size, { base: 2, - round: 1 + round: 1, })}`; } diff --git a/frontend/src/Utilities/Number/padNumber.js b/frontend/src/Utilities/Number/padNumber.js deleted file mode 100644 index 53ae69cac..000000000 --- a/frontend/src/Utilities/Number/padNumber.js +++ /dev/null @@ -1,10 +0,0 @@ -function padNumber(input, width, paddingCharacter = 0) { - if (input == null) { - return ''; - } - - input = `${input}`; - return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input; -} - -export default padNumber; diff --git a/frontend/src/Utilities/Number/padNumber.ts b/frontend/src/Utilities/Number/padNumber.ts new file mode 100644 index 000000000..8646e28d8 --- /dev/null +++ b/frontend/src/Utilities/Number/padNumber.ts @@ -0,0 +1,13 @@ +function padNumber(input: number, width: number, paddingCharacter = '0') { + if (input == null) { + return ''; + } + + const result = `${input}`; + + return result.length >= width + ? result + : new Array(width - result.length + 1).join(paddingCharacter) + result; +} + +export default padNumber; diff --git a/frontend/src/Utilities/Number/roundNumber.js b/frontend/src/Utilities/Number/roundNumber.ts similarity index 59% rename from frontend/src/Utilities/Number/roundNumber.js rename to frontend/src/Utilities/Number/roundNumber.ts index e1a19018f..2035e11cc 100644 --- a/frontend/src/Utilities/Number/roundNumber.js +++ b/frontend/src/Utilities/Number/roundNumber.ts @@ -1,4 +1,4 @@ -export default function roundNumber(input, decimalPlaces = 1) { +export default function roundNumber(input: number, decimalPlaces = 1) { const multiplier = Math.pow(10, decimalPlaces); return Math.round(input * multiplier) / multiplier; diff --git a/frontend/src/Utilities/Object/getErrorMessage.js b/frontend/src/Utilities/Object/getErrorMessage.ts similarity index 53% rename from frontend/src/Utilities/Object/getErrorMessage.js rename to frontend/src/Utilities/Object/getErrorMessage.ts index 1ba874660..d757ceec3 100644 --- a/frontend/src/Utilities/Object/getErrorMessage.js +++ b/frontend/src/Utilities/Object/getErrorMessage.ts @@ -1,4 +1,12 @@ -function getErrorMessage(xhr, fallbackErrorMessage) { +interface AjaxResponse { + responseJSON: + | { + message: string | undefined; + } + | undefined; +} + +function getErrorMessage(xhr: AjaxResponse, fallbackErrorMessage?: string) { if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) { return fallbackErrorMessage; } diff --git a/frontend/src/Utilities/Object/getRemovedItems.js b/frontend/src/Utilities/Object/getRemovedItems.js deleted file mode 100644 index df7ada3a8..000000000 --- a/frontend/src/Utilities/Object/getRemovedItems.js +++ /dev/null @@ -1,15 +0,0 @@ -function getRemovedItems(prevItems, currentItems, idProp = 'id') { - if (prevItems === currentItems) { - return []; - } - - const currentItemIds = new Set(); - - currentItems.forEach((currentItem) => { - currentItemIds.add(currentItem[idProp]); - }); - - return prevItems.filter((prevItem) => !currentItemIds.has(prevItem[idProp])); -} - -export default getRemovedItems; diff --git a/frontend/src/Utilities/Object/hasDifferentItems.js b/frontend/src/Utilities/Object/hasDifferentItems.ts similarity index 70% rename from frontend/src/Utilities/Object/hasDifferentItems.js rename to frontend/src/Utilities/Object/hasDifferentItems.ts index d3c0046e4..0fd832769 100644 --- a/frontend/src/Utilities/Object/hasDifferentItems.js +++ b/frontend/src/Utilities/Object/hasDifferentItems.ts @@ -1,4 +1,10 @@ -function hasDifferentItems(prevItems, currentItems, idProp = 'id') { +import ModelBase from 'App/ModelBase'; + +function hasDifferentItems<T extends ModelBase>( + prevItems: T[], + currentItems: T[], + idProp: keyof T = 'id' +) { if (prevItems === currentItems) { return false; } diff --git a/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts similarity index 67% rename from frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js rename to frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts index e2acbc5c0..1235d4732 100644 --- a/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js +++ b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts @@ -1,4 +1,10 @@ -function hasDifferentItemsOrOrder(prevItems, currentItems, idProp = 'id') { +import ModelBase from 'App/ModelBase'; + +function hasDifferentItemsOrOrder<T extends ModelBase>( + prevItems: T[], + currentItems: T[], + idProp: keyof T = 'id' +) { if (prevItems === currentItems) { return false; } diff --git a/frontend/src/Utilities/Quality/getQualities.js b/frontend/src/Utilities/Quality/getQualities.js deleted file mode 100644 index da09851ea..000000000 --- a/frontend/src/Utilities/Quality/getQualities.js +++ /dev/null @@ -1,16 +0,0 @@ -export default function getQualities(qualities) { - if (!qualities) { - return []; - } - - return qualities.reduce((acc, item) => { - if (item.quality) { - acc.push(item.quality); - } else { - const groupQualities = item.items.map((i) => i.quality); - acc.push(...groupQualities); - } - - return acc; - }, []); -} diff --git a/frontend/src/Utilities/Quality/getQualities.ts b/frontend/src/Utilities/Quality/getQualities.ts new file mode 100644 index 000000000..cf35b7992 --- /dev/null +++ b/frontend/src/Utilities/Quality/getQualities.ts @@ -0,0 +1,26 @@ +import Quality from 'Quality/Quality'; +import { QualityProfileQualityItem } from 'typings/QualityProfile'; + +export default function getQualities(qualities?: QualityProfileQualityItem[]) { + if (!qualities) { + return []; + } + + return qualities.reduce<Quality[]>((acc, item) => { + if (item.quality) { + acc.push(item.quality); + } else { + const groupQualities = item.items.reduce<Quality[]>((acc, i) => { + if (i.quality) { + acc.push(i.quality); + } + + return acc; + }, []); + + acc.push(...groupQualities); + } + + return acc; + }, []); +} diff --git a/frontend/src/Utilities/ResolutionUtility.js b/frontend/src/Utilities/ResolutionUtility.js deleted file mode 100644 index 358448ca9..000000000 --- a/frontend/src/Utilities/ResolutionUtility.js +++ /dev/null @@ -1,26 +0,0 @@ -import $ from 'jquery'; - -module.exports = { - resolutions: { - desktopLarge: 1200, - desktop: 992, - tablet: 768, - mobile: 480 - }, - - isDesktopLarge() { - return $(window).width() < this.resolutions.desktopLarge; - }, - - isDesktop() { - return $(window).width() < this.resolutions.desktop; - }, - - isTablet() { - return $(window).width() < this.resolutions.tablet; - }, - - isMobile() { - return $(window).width() < this.resolutions.mobile; - } -}; diff --git a/frontend/src/Utilities/ResolutionUtility.ts b/frontend/src/Utilities/ResolutionUtility.ts new file mode 100644 index 000000000..4b0d58419 --- /dev/null +++ b/frontend/src/Utilities/ResolutionUtility.ts @@ -0,0 +1,24 @@ +module.exports = { + resolutions: { + desktopLarge: 1200, + desktop: 992, + tablet: 768, + mobile: 480, + }, + + isDesktopLarge() { + return window.innerWidth < this.resolutions.desktopLarge; + }, + + isDesktop() { + return window.innerWidth < this.resolutions.desktop; + }, + + isTablet() { + return window.innerWidth < this.resolutions.tablet; + }, + + isMobile() { + return window.innerWidth < this.resolutions.mobile; + }, +}; diff --git a/frontend/src/Utilities/Series/filterAlternateTitles.js b/frontend/src/Utilities/Series/filterAlternateTitles.js deleted file mode 100644 index 52a6723c1..000000000 --- a/frontend/src/Utilities/Series/filterAlternateTitles.js +++ /dev/null @@ -1,53 +0,0 @@ - -function filterAlternateTitles(alternateTitles, seriesTitle, useSceneNumbering, seasonNumber, sceneSeasonNumber) { - const globalTitles = []; - const seasonTitles = []; - - if (alternateTitles) { - alternateTitles.forEach((alternateTitle) => { - if (alternateTitle.sceneOrigin === 'unknown' || alternateTitle.sceneOrigin === 'unknown:tvdb') { - return; - } - - if (alternateTitle.sceneOrigin === 'mixed') { - // For now filter out 'mixed' from the UI, the user will get an rejection during manual search. - return; - } - - const hasAltSeasonNumber = (alternateTitle.seasonNumber !== -1 && alternateTitle.seasonNumber !== undefined); - const hasAltSceneSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined); - - // Global alias that should be displayed global - if (!hasAltSeasonNumber && !hasAltSceneSeasonNumber && - (alternateTitle.title !== seriesTitle) && - (!alternateTitle.sceneOrigin || !useSceneNumbering)) { - globalTitles.push(alternateTitle); - return; - } - - // Global alias that should be displayed per episode - if (!hasAltSeasonNumber && !hasAltSceneSeasonNumber && alternateTitle.sceneOrigin && useSceneNumbering) { - seasonTitles.push(alternateTitle); - return; - } - - // Apply the alternative mapping (release to scene) - const mappedAltSeasonNumber = hasAltSeasonNumber ? alternateTitle.seasonNumber : alternateTitle.sceneSeasonNumber; - // Select scene or tvdb on the episode - const mappedSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : sceneSeasonNumber; - - if (mappedSeasonNumber !== undefined && mappedSeasonNumber === mappedAltSeasonNumber) { - seasonTitles.push(alternateTitle); - return; - } - }); - } - - if (seasonNumber === undefined) { - return globalTitles; - } - - return seasonTitles; -} - -export default filterAlternateTitles; diff --git a/frontend/src/Utilities/Series/filterAlternateTitles.ts b/frontend/src/Utilities/Series/filterAlternateTitles.ts new file mode 100644 index 000000000..a8dfad702 --- /dev/null +++ b/frontend/src/Utilities/Series/filterAlternateTitles.ts @@ -0,0 +1,83 @@ +import { AlternateTitle } from 'Series/Series'; + +function filterAlternateTitles( + alternateTitles: AlternateTitle[], + seriesTitle: string | null, + useSceneNumbering: boolean, + seasonNumber?: number, + sceneSeasonNumber?: number +) { + const globalTitles: AlternateTitle[] = []; + const seasonTitles: AlternateTitle[] = []; + + if (alternateTitles) { + alternateTitles.forEach((alternateTitle) => { + if ( + alternateTitle.sceneOrigin === 'unknown' || + alternateTitle.sceneOrigin === 'unknown:tvdb' + ) { + return; + } + + if (alternateTitle.sceneOrigin === 'mixed') { + // For now filter out 'mixed' from the UI, the user will get an rejection during manual search. + return; + } + + const hasAltSeasonNumber = + alternateTitle.seasonNumber !== -1 && + alternateTitle.seasonNumber !== undefined; + const hasAltSceneSeasonNumber = + alternateTitle.sceneSeasonNumber !== -1 && + alternateTitle.sceneSeasonNumber !== undefined; + + // Global alias that should be displayed global + if ( + !hasAltSeasonNumber && + !hasAltSceneSeasonNumber && + alternateTitle.title !== seriesTitle && + (!alternateTitle.sceneOrigin || !useSceneNumbering) + ) { + globalTitles.push(alternateTitle); + return; + } + + // Global alias that should be displayed per episode + if ( + !hasAltSeasonNumber && + !hasAltSceneSeasonNumber && + alternateTitle.sceneOrigin && + useSceneNumbering + ) { + seasonTitles.push(alternateTitle); + return; + } + + // Apply the alternative mapping (release to scene) + const mappedAltSeasonNumber = hasAltSeasonNumber + ? alternateTitle.seasonNumber + : alternateTitle.sceneSeasonNumber; + // Select scene or tvdb on the episode + const mappedSeasonNumber = + alternateTitle.sceneOrigin === 'tvdb' + ? seasonNumber + : sceneSeasonNumber; + + if ( + mappedSeasonNumber !== undefined && + mappedSeasonNumber === mappedAltSeasonNumber + ) { + seasonTitles.push(alternateTitle); + return; + } + }); + } + + if (seasonNumber === undefined) { + return globalTitles; + } + + return seasonTitles; +} + +export default filterAlternateTitles; diff --git a/frontend/src/Utilities/Series/getNewSeries.js b/frontend/src/Utilities/Series/getNewSeries.ts similarity index 52% rename from frontend/src/Utilities/Series/getNewSeries.js rename to frontend/src/Utilities/Series/getNewSeries.ts index 0acbe93d7..67c0cd20c 100644 --- a/frontend/src/Utilities/Series/getNewSeries.js +++ b/frontend/src/Utilities/Series/getNewSeries.ts @@ -1,5 +1,22 @@ +import Series, { + MonitorNewItems, + SeriesMonitor, + SeriesType, +} from 'Series/Series'; -function getNewSeries(series, payload) { +interface NewSeriesPayload { + rootFolderPath: string; + monitor: SeriesMonitor; + monitorNewItems: MonitorNewItems; + qualityProfileId: number; + seriesType: SeriesType; + seasonFolder: boolean; + tags: number[]; + searchForMissingEpisodes?: boolean; + searchForCutoffUnmetEpisodes?: boolean; +} + +function getNewSeries(series: Series, payload: NewSeriesPayload) { const { rootFolderPath, monitor, @@ -9,13 +26,13 @@ function getNewSeries(series, payload) { seasonFolder, tags, searchForMissingEpisodes = false, - searchForCutoffUnmetEpisodes = false + searchForCutoffUnmetEpisodes = false, } = payload; const addOptions = { monitor, searchForMissingEpisodes, - searchForCutoffUnmetEpisodes + searchForCutoffUnmetEpisodes, }; series.addOptions = addOptions; diff --git a/frontend/src/Utilities/Series/monitorNewItemsOptions.js b/frontend/src/Utilities/Series/monitorNewItemsOptions.ts similarity index 94% rename from frontend/src/Utilities/Series/monitorNewItemsOptions.js rename to frontend/src/Utilities/Series/monitorNewItemsOptions.ts index 49c948c7f..be49fd60b 100644 --- a/frontend/src/Utilities/Series/monitorNewItemsOptions.js +++ b/frontend/src/Utilities/Series/monitorNewItemsOptions.ts @@ -5,14 +5,14 @@ const monitorNewItemsOptions = [ key: 'all', get value() { return translate('MonitorAllSeasons'); - } + }, }, { key: 'none', get value() { return translate('MonitorNoNewSeasons'); - } - } + }, + }, ]; export default monitorNewItemsOptions; diff --git a/frontend/src/Utilities/Series/monitorOptions.js b/frontend/src/Utilities/Series/monitorOptions.ts similarity index 93% rename from frontend/src/Utilities/Series/monitorOptions.js rename to frontend/src/Utilities/Series/monitorOptions.ts index 6616cf7c4..5efcc51f4 100644 --- a/frontend/src/Utilities/Series/monitorOptions.js +++ b/frontend/src/Utilities/Series/monitorOptions.ts @@ -5,68 +5,68 @@ const monitorOptions = [ key: 'all', get value() { return translate('MonitorAllEpisodes'); - } + }, }, { key: 'future', get value() { return translate('MonitorFutureEpisodes'); - } + }, }, { key: 'missing', get value() { return translate('MonitorMissingEpisodes'); - } + }, }, { key: 'existing', get value() { return translate('MonitorExistingEpisodes'); - } + }, }, { key: 'recent', get value() { return translate('MonitorRecentEpisodes'); - } + }, }, { key: 'pilot', get value() { return translate('MonitorPilotEpisode'); - } + }, }, { key: 'firstSeason', get value() { return translate('MonitorFirstSeason'); - } + }, }, { key: 'lastSeason', get value() { return translate('MonitorLastSeason'); - } + }, }, { key: 'monitorSpecials', get value() { return translate('MonitorSpecialEpisodes'); - } + }, }, { key: 'unmonitorSpecials', get value() { return translate('UnmonitorSpecialEpisodes'); - } + }, }, { key: 'none', get value() { return translate('MonitorNoEpisodes'); - } - } + }, + }, ]; export default monitorOptions; diff --git a/frontend/src/Utilities/Series/seriesTypes.js b/frontend/src/Utilities/Series/seriesTypes.ts similarity index 100% rename from frontend/src/Utilities/Series/seriesTypes.js rename to frontend/src/Utilities/Series/seriesTypes.ts diff --git a/frontend/src/Utilities/State/getNextId.js b/frontend/src/Utilities/State/getNextId.js deleted file mode 100644 index 204aac95a..000000000 --- a/frontend/src/Utilities/State/getNextId.js +++ /dev/null @@ -1,5 +0,0 @@ -function getNextId(items) { - return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; -} - -export default getNextId; diff --git a/frontend/src/Utilities/State/getNextId.ts b/frontend/src/Utilities/State/getNextId.ts new file mode 100644 index 000000000..c0cbdec97 --- /dev/null +++ b/frontend/src/Utilities/State/getNextId.ts @@ -0,0 +1,7 @@ +import ModelBase from 'App/ModelBase'; + +function getNextId<T extends ModelBase>(items: T[]) { + return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; +} + +export default getNextId; diff --git a/frontend/src/Utilities/pages.js b/frontend/src/Utilities/State/pages.js similarity index 100% rename from frontend/src/Utilities/pages.js rename to frontend/src/Utilities/State/pages.js diff --git a/frontend/src/Utilities/serverSideCollectionHandlers.js b/frontend/src/Utilities/State/serverSideCollectionHandlers.js similarity index 100% rename from frontend/src/Utilities/serverSideCollectionHandlers.js rename to frontend/src/Utilities/State/serverSideCollectionHandlers.js diff --git a/frontend/src/Utilities/String/combinePath.js b/frontend/src/Utilities/String/combinePath.js deleted file mode 100644 index 9e4e9abe8..000000000 --- a/frontend/src/Utilities/String/combinePath.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function combinePath(isWindows, basePath, paths = []) { - const slash = isWindows ? '\\' : '/'; - - return `${basePath}${slash}${paths.join(slash)}`; -} diff --git a/frontend/src/Utilities/String/combinePath.ts b/frontend/src/Utilities/String/combinePath.ts new file mode 100644 index 000000000..d62b71628 --- /dev/null +++ b/frontend/src/Utilities/String/combinePath.ts @@ -0,0 +1,9 @@ +export default function combinePath( + isWindows: boolean, + basePath: string, + paths: string[] = [] +) { + const slash = isWindows ? '\\' : '/'; + + return `${basePath}${slash}${paths.join(slash)}`; +} diff --git a/frontend/src/Utilities/String/generateUUIDv4.js b/frontend/src/Utilities/String/generateUUIDv4.js deleted file mode 100644 index 51b15ec60..000000000 --- a/frontend/src/Utilities/String/generateUUIDv4.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function generateUUIDv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => - // eslint-disable-next-line no-bitwise - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); -} diff --git a/frontend/src/Utilities/String/isString.js b/frontend/src/Utilities/String/isString.ts similarity index 58% rename from frontend/src/Utilities/String/isString.js rename to frontend/src/Utilities/String/isString.ts index 1e7c3dff8..f67d10683 100644 --- a/frontend/src/Utilities/String/isString.js +++ b/frontend/src/Utilities/String/isString.ts @@ -1,3 +1,3 @@ -export default function isString(possibleString) { +export default function isString(possibleString: unknown) { return typeof possibleString === 'string' || possibleString instanceof String; } diff --git a/frontend/src/Utilities/String/naturalExpansion.js b/frontend/src/Utilities/String/naturalExpansion.ts similarity index 78% rename from frontend/src/Utilities/String/naturalExpansion.js rename to frontend/src/Utilities/String/naturalExpansion.ts index 2cdd69b86..3b7422933 100644 --- a/frontend/src/Utilities/String/naturalExpansion.js +++ b/frontend/src/Utilities/String/naturalExpansion.ts @@ -1,6 +1,6 @@ const regex = /\d+/g; -function naturalExpansion(input) { +function naturalExpansion(input: string) { if (!input) { return ''; } diff --git a/frontend/src/Utilities/String/parseUrl.js b/frontend/src/Utilities/String/parseUrl.ts similarity index 72% rename from frontend/src/Utilities/String/parseUrl.js rename to frontend/src/Utilities/String/parseUrl.ts index 93341f85f..47d6c5d98 100644 --- a/frontend/src/Utilities/String/parseUrl.js +++ b/frontend/src/Utilities/String/parseUrl.ts @@ -4,13 +4,13 @@ import qs from 'qs'; // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils const anchor = document.createElement('a'); -export default function parseUrl(url) { +export default function parseUrl(url: string) { anchor.href = url; // The `origin`, `password`, and `username` properties are unavailable in // Opera Presto. We synthesize `origin` if it's not present. While `password` // and `username` are ignored intentionally. - const properties = _.pick( + const properties: Record<string, string | number | boolean | object> = _.pick( anchor, 'hash', 'host', @@ -23,11 +23,11 @@ export default function parseUrl(url) { 'search' ); - properties.isAbsolute = (/^[\w:]*\/\//).test(url); + properties.isAbsolute = /^[\w:]*\/\//.test(url); if (properties.search) { // Remove leading ? from querystring before parsing. - properties.params = qs.parse(properties.search.substring(1)); + properties.params = qs.parse((properties.search as string).substring(1)); } else { properties.params = {}; } diff --git a/frontend/src/Utilities/String/split.js b/frontend/src/Utilities/String/split.js deleted file mode 100644 index 0e57e7545..000000000 --- a/frontend/src/Utilities/String/split.js +++ /dev/null @@ -1,17 +0,0 @@ -import _ from 'lodash'; - -function split(input, separator = ',') { - if (!input) { - return []; - } - - return _.reduce(input.split(separator), (result, s) => { - if (s) { - result.push(s); - } - - return result; - }, []); -} - -export default split; diff --git a/frontend/src/Utilities/String/split.ts b/frontend/src/Utilities/String/split.ts new file mode 100644 index 000000000..2f6af7605 --- /dev/null +++ b/frontend/src/Utilities/String/split.ts @@ -0,0 +1,15 @@ +function split(input: string, separator = ',') { + if (!input) { + return []; + } + + return input.split(separator).reduce<string[]>((acc, s) => { + if (s) { + acc.push(s); + } + + return acc; + }, []); +} + +export default split; diff --git a/frontend/src/Utilities/String/titleCase.js b/frontend/src/Utilities/String/titleCase.ts similarity index 81% rename from frontend/src/Utilities/String/titleCase.js rename to frontend/src/Utilities/String/titleCase.ts index 03573b9e3..72513cd09 100644 --- a/frontend/src/Utilities/String/titleCase.js +++ b/frontend/src/Utilities/String/titleCase.ts @@ -1,6 +1,6 @@ const regex = /\b\w+/g; -function titleCase(input) { +function titleCase(input: string | undefined) { if (!input) { return ''; } diff --git a/frontend/src/Utilities/Table/areAllSelected.js b/frontend/src/Utilities/Table/areAllSelected.js deleted file mode 100644 index 26102f89b..000000000 --- a/frontend/src/Utilities/Table/areAllSelected.js +++ /dev/null @@ -1,17 +0,0 @@ -export default function areAllSelected(selectedState) { - let allSelected = true; - let allUnselected = true; - - Object.keys(selectedState).forEach((key) => { - if (selectedState[key]) { - allUnselected = false; - } else { - allSelected = false; - } - }); - - return { - allSelected, - allUnselected - }; -} diff --git a/frontend/src/Utilities/Table/areAllSelected.ts b/frontend/src/Utilities/Table/areAllSelected.ts new file mode 100644 index 000000000..ffb791ed1 --- /dev/null +++ b/frontend/src/Utilities/Table/areAllSelected.ts @@ -0,0 +1,19 @@ +import { SelectedState } from 'Helpers/Hooks/useSelectState'; + +export default function areAllSelected(selectedState: SelectedState) { + let allSelected = true; + let allUnselected = true; + + Object.values(selectedState).forEach((value) => { + if (value) { + allUnselected = false; + } else { + allSelected = false; + } + }); + + return { + allSelected, + allUnselected, + }; +} diff --git a/frontend/src/Utilities/Table/getToggledRange.js b/frontend/src/Utilities/Table/getToggledRange.js deleted file mode 100644 index c0cc44fe5..000000000 --- a/frontend/src/Utilities/Table/getToggledRange.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'lodash'; - -function getToggledRange(items, id, lastToggled) { - const lastToggledIndex = _.findIndex(items, { id: lastToggled }); - const changedIndex = _.findIndex(items, { id }); - let lower = 0; - let upper = 0; - - if (lastToggledIndex > changedIndex) { - lower = changedIndex; - upper = lastToggledIndex + 1; - } else { - lower = lastToggledIndex; - upper = changedIndex; - } - - return { - lower, - upper - }; -} - -export default getToggledRange; diff --git a/frontend/src/Utilities/Table/getToggledRange.ts b/frontend/src/Utilities/Table/getToggledRange.ts new file mode 100644 index 000000000..59a098e17 --- /dev/null +++ b/frontend/src/Utilities/Table/getToggledRange.ts @@ -0,0 +1,27 @@ +import ModelBase from 'App/ModelBase'; + +function getToggledRange<T extends ModelBase>( + items: T[], + id: number, + lastToggled: number +) { + const lastToggledIndex = items.findIndex((item) => item.id === lastToggled); + const changedIndex = items.findIndex((item) => item.id === id); + let lower = 0; + let upper = 0; + + if (lastToggledIndex > changedIndex) { + lower = changedIndex; + upper = lastToggledIndex + 1; + } else { + lower = lastToggledIndex; + upper = changedIndex; + } + + return { + lower, + upper, + }; +} + +export default getToggledRange; diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.js b/frontend/src/Utilities/Table/removeOldSelectedState.js deleted file mode 100644 index ff3a4fe11..000000000 --- a/frontend/src/Utilities/Table/removeOldSelectedState.js +++ /dev/null @@ -1,16 +0,0 @@ -import areAllSelected from './areAllSelected'; - -export default function removeOldSelectedState(state, prevItems) { - const selectedState = { - ...state.selectedState - }; - - prevItems.forEach((item) => { - delete selectedState[item.id]; - }); - - return { - ...areAllSelected(selectedState), - selectedState - }; -} diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.ts b/frontend/src/Utilities/Table/removeOldSelectedState.ts new file mode 100644 index 000000000..8edb9e4dc --- /dev/null +++ b/frontend/src/Utilities/Table/removeOldSelectedState.ts @@ -0,0 +1,21 @@ +import ModelBase from 'App/ModelBase'; +import { SelectState } from 'Helpers/Hooks/useSelectState'; +import areAllSelected from './areAllSelected'; + +export default function removeOldSelectedState<T extends ModelBase>( + state: SelectState, + prevItems: T[] +) { + const selectedState = { + ...state.selectedState, + }; + + prevItems.forEach((item) => { + delete selectedState[item.id]; + }); + + return { + ...areAllSelected(selectedState), + selectedState, + }; +} diff --git a/frontend/src/Utilities/Table/selectAll.js b/frontend/src/Utilities/Table/selectAll.js deleted file mode 100644 index ffaaeaddf..000000000 --- a/frontend/src/Utilities/Table/selectAll.js +++ /dev/null @@ -1,17 +0,0 @@ -import _ from 'lodash'; - -function selectAll(selectedState, selected) { - const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => { - result[item] = selected; - return result; - }, {}); - - return { - allSelected: selected, - allUnselected: !selected, - lastToggled: null, - selectedState: newSelectedState - }; -} - -export default selectAll; diff --git a/frontend/src/Utilities/Table/selectAll.ts b/frontend/src/Utilities/Table/selectAll.ts new file mode 100644 index 000000000..bc7f8de8c --- /dev/null +++ b/frontend/src/Utilities/Table/selectAll.ts @@ -0,0 +1,19 @@ +import { SelectedState } from 'Helpers/Hooks/useSelectState'; + +function selectAll(selectedState: SelectedState, selected: boolean) { + const newSelectedState = Object.keys(selectedState).reduce< + Record<number, boolean> + >((acc, item) => { + acc[Number(item)] = selected; + return acc; + }, {}); + + return { + allSelected: selected, + allUnselected: !selected, + lastToggled: null, + selectedState: newSelectedState, + }; +} + +export default selectAll; diff --git a/frontend/src/Utilities/Table/toggleSelected.js b/frontend/src/Utilities/Table/toggleSelected.ts similarity index 57% rename from frontend/src/Utilities/Table/toggleSelected.js rename to frontend/src/Utilities/Table/toggleSelected.ts index ec8870b0b..e3510572a 100644 --- a/frontend/src/Utilities/Table/toggleSelected.js +++ b/frontend/src/Utilities/Table/toggleSelected.ts @@ -1,11 +1,19 @@ +import ModelBase from 'App/ModelBase'; +import { SelectState } from 'Helpers/Hooks/useSelectState'; import areAllSelected from './areAllSelected'; import getToggledRange from './getToggledRange'; -function toggleSelected(selectedState, items, id, selected, shiftKey) { - const lastToggled = selectedState.lastToggled; +function toggleSelected<T extends ModelBase>( + selectState: SelectState, + items: T[], + id: number, + selected: boolean, + shiftKey: boolean +) { + const lastToggled = selectState.lastToggled; const nextSelectedState = { - ...selectedState.selectedState, - [id]: selected + ...selectState.selectedState, + [id]: selected, }; if (selected == null) { @@ -23,7 +31,7 @@ function toggleSelected(selectedState, items, id, selected, shiftKey) { return { ...areAllSelected(nextSelectedState), lastToggled: id, - selectedState: nextSelectedState + selectedState: nextSelectedState, }; } diff --git a/frontend/src/Utilities/browser.js b/frontend/src/Utilities/browser.ts similarity index 99% rename from frontend/src/Utilities/browser.js rename to frontend/src/Utilities/browser.ts index ff896e801..2ec0481f3 100644 --- a/frontend/src/Utilities/browser.js +++ b/frontend/src/Utilities/browser.ts @@ -3,7 +3,6 @@ import MobileDetect from 'mobile-detect'; const mobileDetect = new MobileDetect(window.navigator.userAgent); export function isMobile() { - return mobileDetect.mobile() != null; } diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js deleted file mode 100644 index 60533d3d3..000000000 --- a/frontend/src/Utilities/getPathWithUrlBase.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getPathWithUrlBase(path) { - return `${window.Sonarr.urlBase}${path}`; -} diff --git a/frontend/src/Utilities/getPathWithUrlBase.ts b/frontend/src/Utilities/getPathWithUrlBase.ts new file mode 100644 index 000000000..c8335b899 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.ts @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path: string) { + return `${window.Sonarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.ts similarity index 100% rename from frontend/src/Utilities/getUniqueElementId.js rename to frontend/src/Utilities/getUniqueElementId.ts diff --git a/frontend/src/Utilities/pagePopulator.js b/frontend/src/Utilities/pagePopulator.ts similarity index 51% rename from frontend/src/Utilities/pagePopulator.js rename to frontend/src/Utilities/pagePopulator.ts index f58dbe803..45689f63a 100644 --- a/frontend/src/Utilities/pagePopulator.js +++ b/frontend/src/Utilities/pagePopulator.ts @@ -1,19 +1,24 @@ -let currentPopulator = null; -let currentReasons = []; +type Populator = () => void; -export function registerPagePopulator(populator, reasons = []) { +let currentPopulator: Populator | null = null; +let currentReasons: string[] = []; + +export function registerPagePopulator( + populator: Populator, + reasons: string[] = [] +) { currentPopulator = populator; currentReasons = reasons; } -export function unregisterPagePopulator(populator) { +export function unregisterPagePopulator(populator: Populator) { if (currentPopulator === populator) { currentPopulator = null; currentReasons = []; } } -export function repopulatePage(reason) { +export function repopulatePage(reason: string) { if (!currentPopulator) { return; } diff --git a/frontend/src/Utilities/requestAction.js b/frontend/src/Utilities/requestAction.js index ed69cf5ad..86b17f695 100644 --- a/frontend/src/Utilities/requestAction.js +++ b/frontend/src/Utilities/requestAction.js @@ -1,18 +1,17 @@ import $ from 'jquery'; -import _ from 'lodash'; import createAjaxRequest from './createAjaxRequest'; function flattenProviderData(providerData) { - return _.reduce(Object.keys(providerData), (result, key) => { + return Object.keys(providerData).reduce((acc, key) => { const property = providerData[key]; if (key === 'fields') { - result[key] = property; + acc[key] = property; } else { - result[key] = property.value; + acc[key] = property.value; } - return result; + return acc; }, {}); } diff --git a/frontend/src/Utilities/scrollLock.js b/frontend/src/Utilities/scrollLock.ts similarity index 85% rename from frontend/src/Utilities/scrollLock.js rename to frontend/src/Utilities/scrollLock.ts index cff50a34b..c63e8ff87 100644 --- a/frontend/src/Utilities/scrollLock.js +++ b/frontend/src/Utilities/scrollLock.ts @@ -8,6 +8,6 @@ export function isLocked() { return scrollLock; } -export function setScrollLock(locked) { +export function setScrollLock(locked: boolean) { scrollLock = locked; } diff --git a/frontend/src/Utilities/sectionTypes.js b/frontend/src/Utilities/sectionTypes.js deleted file mode 100644 index 5479b32b9..000000000 --- a/frontend/src/Utilities/sectionTypes.js +++ /dev/null @@ -1,6 +0,0 @@ -const sectionTypes = { - COLLECTION: 'collection', - MODEL: 'model' -}; - -export default sectionTypes; diff --git a/package.json b/package.json index c86ab70c5..cc608795a 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@babel/preset-react": "7.24.1", "@babel/preset-typescript": "7.24.1", "@types/lodash": "4.14.194", + "@types/qs": "6.9.15", "@types/react-lazyload": "3.2.0", "@types/react-router-dom": "5.3.3", "@types/react-text-truncate": "0.14.1", diff --git a/yarn.lock b/yarn.lock index 5cf57bfc8..55ee05650 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1458,6 +1458,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/qs@6.9.15": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + "@types/react-dom@18.2.25": version "18.2.25" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" From d6d90a64a39d3b9d3a95fb6b265517693a70fdd7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 24 Jul 2024 16:39:01 -0700 Subject: [PATCH 415/762] Convert App to TypeScript --- frontend/src/App/{App.js => App.tsx} | 12 +- .../src/App/{AppRoutes.js => AppRoutes.tsx} | 182 ++++-------------- frontend/src/App/AppUpdatedModal.js | 30 --- frontend/src/App/AppUpdatedModal.tsx | 28 +++ frontend/src/App/AppUpdatedModalConnector.js | 12 -- frontend/src/App/AppUpdatedModalContent.js | 139 ------------- frontend/src/App/AppUpdatedModalContent.tsx | 145 ++++++++++++++ .../App/AppUpdatedModalContentConnector.js | 78 -------- frontend/src/App/ApplyTheme.tsx | 10 +- ...iredContext.js => ColorImpairedContext.ts} | 0 ...onLostModal.js => ConnectionLostModal.tsx} | 43 ++--- .../src/App/ConnectionLostModalConnector.js | 12 -- frontend/src/App/State/AppState.ts | 1 + frontend/src/Components/Page/Page.js | 8 +- frontend/src/System/Updates/Updates.tsx | 4 +- frontend/src/typings/Update.ts | 2 +- package.json | 1 + yarn.lock | 7 + 18 files changed, 258 insertions(+), 456 deletions(-) rename frontend/src/App/{App.js => App.tsx} (71%) rename frontend/src/App/{AppRoutes.js => AppRoutes.tsx} (52%) delete mode 100644 frontend/src/App/AppUpdatedModal.js create mode 100644 frontend/src/App/AppUpdatedModal.tsx delete mode 100644 frontend/src/App/AppUpdatedModalConnector.js delete mode 100644 frontend/src/App/AppUpdatedModalContent.js create mode 100644 frontend/src/App/AppUpdatedModalContent.tsx delete mode 100644 frontend/src/App/AppUpdatedModalContentConnector.js rename frontend/src/App/{ColorImpairedContext.js => ColorImpairedContext.ts} (100%) rename frontend/src/App/{ConnectionLostModal.js => ConnectionLostModal.tsx} (54%) delete mode 100644 frontend/src/App/ConnectionLostModalConnector.js diff --git a/frontend/src/App/App.js b/frontend/src/App/App.tsx similarity index 71% rename from frontend/src/App/App.js rename to frontend/src/App/App.tsx index 754c75035..6c2d799f3 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.tsx @@ -1,13 +1,19 @@ -import { ConnectedRouter } from 'connected-react-router'; +import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; import PropTypes from 'prop-types'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; +import { Store } from 'redux'; import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history }) { +interface AppProps { + store: Store; + history: ConnectedRouterProps['history']; +} + +function App({ store, history }: AppProps) { return ( <DocumentTitle title={window.Sonarr.instanceName}> <Provider store={store}> @@ -24,7 +30,7 @@ function App({ store, history }) { App.propTypes = { store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired + history: PropTypes.object.isRequired, }; export default App; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.tsx similarity index 52% rename from frontend/src/App/AppRoutes.js rename to frontend/src/App/AppRoutes.tsx index a5bb0b33c..f66a4df40 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.tsx @@ -35,60 +35,37 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector'; -function AppRoutes(props) { - const { - app - } = props; - +function AppRoutes() { return ( <Switch> {/* Series */} - <Route - exact={true} - path="/" - component={SeriesIndex} - /> + <Route exact={true} path="/" component={SeriesIndex} /> - { - window.Sonarr.urlBase && - <Route - exact={true} - path="/" - addUrlBase={false} - render={() => { - return ( - <Redirect - to={getPathWithUrlBase('/')} - component={app} - /> - ); - }} - /> - } + {window.Sonarr.urlBase && ( + <Route + exact={true} + path="/" + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + addUrlBase={false} + render={() => { + return <Redirect to={getPathWithUrlBase('/')} />; + }} + /> + )} - <Route - path="/add/new" - component={AddNewSeriesConnector} - /> + <Route path="/add/new" component={AddNewSeriesConnector} /> - <Route - path="/add/import" - component={ImportSeries} - /> + <Route path="/add/import" component={ImportSeries} /> <Route path="/serieseditor" exact={true} render={() => { - return ( - <Redirect - to={getPathWithUrlBase('/')} - component={app} - /> - ); + return <Redirect to={getPathWithUrlBase('/')} />; }} /> @@ -96,96 +73,57 @@ function AppRoutes(props) { path="/seasonpass" exact={true} render={() => { - return ( - <Redirect - to={getPathWithUrlBase('/')} - component={app} - /> - ); + return <Redirect to={getPathWithUrlBase('/')} />; }} /> - <Route - path="/series/:titleSlug" - component={SeriesDetailsPageConnector} - /> + <Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} /> {/* Calendar */} - <Route - path="/calendar" - component={CalendarPageConnector} - /> + <Route path="/calendar" component={CalendarPageConnector} /> {/* Activity */} - <Route - path="/activity/history" - component={History} - /> + <Route path="/activity/history" component={History} /> - <Route - path="/activity/queue" - component={Queue} - /> + <Route path="/activity/queue" component={Queue} /> - <Route - path="/activity/blocklist" - component={Blocklist} - /> + <Route path="/activity/blocklist" component={Blocklist} /> {/* Wanted */} - <Route - path="/wanted/missing" - component={MissingConnector} - /> + <Route path="/wanted/missing" component={MissingConnector} /> - <Route - path="/wanted/cutoffunmet" - component={CutoffUnmetConnector} - /> + <Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} /> {/* Settings */} - <Route - exact={true} - path="/settings" - component={Settings} - /> + <Route exact={true} path="/settings" component={Settings} /> <Route path="/settings/mediamanagement" component={MediaManagementConnector} /> - <Route - path="/settings/profiles" - component={Profiles} - /> + <Route path="/settings/profiles" component={Profiles} /> - <Route - path="/settings/quality" - component={QualityConnector} - /> + <Route path="/settings/quality" component={QualityConnector} /> <Route path="/settings/customformats" component={CustomFormatSettingsPage} /> - <Route - path="/settings/indexers" - component={IndexerSettingsConnector} - /> + <Route path="/settings/indexers" component={IndexerSettingsConnector} /> <Route path="/settings/downloadclients" @@ -197,84 +135,48 @@ function AppRoutes(props) { component={ImportListSettingsConnector} /> - <Route - path="/settings/connect" - component={NotificationSettings} - /> + <Route path="/settings/connect" component={NotificationSettings} /> - <Route - path="/settings/metadata" - component={MetadataSettings} - /> + <Route path="/settings/metadata" component={MetadataSettings} /> <Route path="/settings/metadatasource" component={MetadataSourceSettings} /> - <Route - path="/settings/tags" - component={TagSettings} - /> + <Route path="/settings/tags" component={TagSettings} /> - <Route - path="/settings/general" - component={GeneralSettingsConnector} - /> + <Route path="/settings/general" component={GeneralSettingsConnector} /> - <Route - path="/settings/ui" - component={UISettingsConnector} - /> + <Route path="/settings/ui" component={UISettingsConnector} /> {/* System */} - <Route - path="/system/status" - component={Status} - /> + <Route path="/system/status" component={Status} /> - <Route - path="/system/tasks" - component={Tasks} - /> + <Route path="/system/tasks" component={Tasks} /> - <Route - path="/system/backup" - component={BackupsConnector} - /> + <Route path="/system/backup" component={BackupsConnector} /> - <Route - path="/system/updates" - component={Updates} - /> + <Route path="/system/updates" component={Updates} /> - <Route - path="/system/events" - component={LogsTableConnector} - /> + <Route path="/system/events" component={LogsTableConnector} /> - <Route - path="/system/logs/files" - component={Logs} - /> + <Route path="/system/logs/files" component={Logs} /> {/* Not Found */} - <Route - path="*" - component={NotFound} - /> + <Route path="*" component={NotFound} /> </Switch> ); } AppRoutes.propTypes = { - app: PropTypes.func.isRequired + app: PropTypes.func.isRequired, }; export default AppRoutes; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js deleted file mode 100644 index abc7f8832..000000000 --- a/frontend/src/App/AppUpdatedModal.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; - -function AppUpdatedModal(props) { - const { - isOpen, - onModalClose - } = props; - - return ( - <Modal - isOpen={isOpen} - closeOnBackgroundClick={false} - onModalClose={onModalClose} - > - <AppUpdatedModalContentConnector - onModalClose={onModalClose} - /> - </Modal> - ); -} - -AppUpdatedModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx new file mode 100644 index 000000000..696d36fb2 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +interface AppUpdatedModalProps { + isOpen: boolean; + onModalClose: (...args: unknown[]) => unknown; +} + +function AppUpdatedModal(props: AppUpdatedModalProps) { + const { isOpen, onModalClose } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); + + return ( + <Modal + isOpen={isOpen} + closeOnBackgroundClick={false} + onModalClose={onModalClose} + > + <AppUpdatedModalContent onModalClose={handleModalClose} /> + </Modal> + ); +} + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js deleted file mode 100644 index a21afbc5a..000000000 --- a/frontend/src/App/AppUpdatedModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import AppUpdatedModal from './AppUpdatedModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js deleted file mode 100644 index 8cce1bc16..000000000 --- a/frontend/src/App/AppUpdatedModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -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 { kinds } from 'Helpers/Props'; -import UpdateChanges from 'System/Updates/UpdateChanges'; -import translate from 'Utilities/String/translate'; -import styles from './AppUpdatedModalContent.css'; - -function mergeUpdates(items, version, prevVersion) { - let installedIndex = items.findIndex((u) => u.version === version); - let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); - - if (installedIndex === -1) { - installedIndex = 0; - } - - if (installedPreviouslyIndex === -1) { - installedPreviouslyIndex = items.length; - } else if (installedPreviouslyIndex === installedIndex && items.length) { - installedPreviouslyIndex += 1; - } - - const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); - - if (!appliedUpdates.length) { - return null; - } - - const appliedChanges = { new: [], fixed: [] }; - appliedUpdates.forEach((u) => { - if (u.changes) { - appliedChanges.new.push(... u.changes.new); - appliedChanges.fixed.push(... u.changes.fixed); - } - }); - - const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); - - if (!appliedChanges.new.length && !appliedChanges.fixed.length) { - mergedUpdate.changes = null; - } - - return mergedUpdate; -} - -function AppUpdatedModalContent(props) { - const { - version, - prevVersion, - isPopulated, - error, - items, - onSeeChangesPress, - onModalClose - } = props; - - const update = mergeUpdates(items, version, prevVersion); - - return ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('AppUpdated')} - </ModalHeader> - - <ModalBody> - <div> - <InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} /> - </div> - - { - isPopulated && !error && !!update && - <div> - { - !update.changes && - <div className={styles.maintenance}>{translate('MaintenanceRelease')}</div> - } - - { - !!update.changes && - <div> - <div className={styles.changes}> - {translate('WhatsNew')} - </div> - - <UpdateChanges - title={translate('New')} - changes={update.changes.new} - /> - - <UpdateChanges - title={translate('Fixed')} - changes={update.changes.fixed} - /> - </div> - } - </div> - } - - { - !isPopulated && !error && - <LoadingIndicator /> - } - </ModalBody> - - <ModalFooter> - <Button - onPress={onSeeChangesPress} - > - {translate('RecentChanges')} - </Button> - - <Button - kind={kinds.PRIMARY} - onPress={onModalClose} - > - {translate('Reload')} - </Button> - </ModalFooter> - </ModalContent> - ); -} - -AppUpdatedModalContent.propTypes = { - version: PropTypes.string.isRequired, - prevVersion: PropTypes.string, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onSeeChangesPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx new file mode 100644 index 000000000..6553d6270 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +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 { kinds } from 'Helpers/Props'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import Update from 'typings/Update'; +import translate from 'Utilities/String/translate'; +import AppState from './State/AppState'; +import styles from './AppUpdatedModalContent.css'; + +function mergeUpdates(items: Update[], version: string, prevVersion?: string) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex( + (u) => u.version === prevVersion + ); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.length) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges: Update['changes'] = { new: [], fixed: [] }; + + appliedUpdates.forEach((u: Update) => { + if (u.changes) { + appliedChanges.new.push(...u.changes.new); + appliedChanges.fixed.push(...u.changes.fixed); + } + }); + + const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], { + changes: appliedChanges, + }); + + if (!appliedChanges.new.length && !appliedChanges.fixed.length) { + mergedUpdate.changes = null; + } + + return mergedUpdate; +} + +interface AppUpdatedModalContentProps { + onModalClose: () => void; +} + +function AppUpdatedModalContent(props: AppUpdatedModalContentProps) { + const dispatch = useDispatch(); + const { version, prevVersion } = useSelector((state: AppState) => state.app); + const { isPopulated, error, items } = useSelector( + (state: AppState) => state.system.updates + ); + const previousVersion = usePrevious(version); + + const { onModalClose } = props; + + const update = mergeUpdates(items, version, prevVersion); + + const handleSeeChangesPress = useCallback(() => { + window.location.href = `${window.Sonarr.urlBase}/system/updates`; + }, []); + + useEffect(() => { + dispatch(fetchUpdates()); + }, [dispatch]); + + useEffect(() => { + if (version !== previousVersion) { + dispatch(fetchUpdates()); + } + }, [version, previousVersion, dispatch]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('AppUpdated')}</ModalHeader> + + <ModalBody> + <div> + <InlineMarkdown + data={translate('AppUpdatedVersion', { version })} + blockClassName={styles.version} + /> + </div> + + {isPopulated && !error && !!update ? ( + <div> + {update.changes ? ( + <div className={styles.maintenance}> + {translate('MaintenanceRelease')} + </div> + ) : null} + + {update.changes ? ( + <div> + <div className={styles.changes}>{translate('WhatsNew')}</div> + + <UpdateChanges + title={translate('New')} + changes={update.changes.new} + /> + + <UpdateChanges + title={translate('Fixed')} + changes={update.changes.fixed} + /> + </div> + ) : null} + </div> + ) : null} + + {!isPopulated && !error ? <LoadingIndicator /> : null} + </ModalBody> + + <ModalFooter> + <Button onPress={handleSeeChangesPress}> + {translate('RecentChanges')} + </Button> + + <Button kind={kinds.PRIMARY} onPress={onModalClose}> + {translate('Reload')} + </Button> + </ModalFooter> + </ModalContent> + ); +} + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js deleted file mode 100644 index 4100ee674..000000000 --- a/frontend/src/App/AppUpdatedModalContentConnector.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import AppUpdatedModalContent from './AppUpdatedModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - (state) => state.app.prevVersion, - (state) => state.system.updates, - (version, prevVersion, updates) => { - const { - isPopulated, - error, - items - } = updates; - - return { - version, - prevVersion, - isPopulated, - error, - items - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchFetchUpdates() { - dispatch(fetchUpdates()); - }, - - onSeeChangesPress() { - window.location = `${window.Sonarr.urlBase}/system/updates`; - } - }; -} - -class AppUpdatedModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - } - - componentDidUpdate(prevProps) { - if (prevProps.version !== this.props.version) { - this.props.dispatchFetchUpdates(); - } - } - - // - // Render - - render() { - const { - dispatchFetchUpdates, - ...otherProps - } = this.props; - - return ( - <AppUpdatedModalContent {...otherProps} /> - ); - } -} - -AppUpdatedModalContentConnector.propTypes = { - version: PropTypes.string.isRequired, - dispatchFetchUpdates: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx index ad3ad69e9..ce598f2dc 100644 --- a/frontend/src/App/ApplyTheme.tsx +++ b/frontend/src/App/ApplyTheme.tsx @@ -1,13 +1,9 @@ -import React, { Fragment, ReactNode, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import themes from 'Styles/Themes'; import AppState from './State/AppState'; -interface ApplyThemeProps { - children: ReactNode; -} - function createThemeSelector() { return createSelector( (state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme, @@ -17,7 +13,7 @@ function createThemeSelector() { ); } -function ApplyTheme({ children }: ApplyThemeProps) { +function ApplyTheme() { const theme = useSelector(createThemeSelector()); const updateCSSVariables = useCallback(() => { @@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) { updateCSSVariables(); }, [updateCSSVariables, theme]); - return <Fragment>{children}</Fragment>; + return null; } export default ApplyTheme; diff --git a/frontend/src/App/ColorImpairedContext.js b/frontend/src/App/ColorImpairedContext.ts similarity index 100% rename from frontend/src/App/ColorImpairedContext.js rename to frontend/src/App/ColorImpairedContext.ts diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.tsx similarity index 54% rename from frontend/src/App/ConnectionLostModal.js rename to frontend/src/App/ConnectionLostModal.tsx index 5c08f491f..f08f2c0e2 100644 --- a/frontend/src/App/ConnectionLostModal.js +++ b/frontend/src/App/ConnectionLostModal.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback } from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -function ConnectionLostModal(props) { - const { - isOpen, - onModalClose - } = props; +interface ConnectionLostModalProps { + isOpen: boolean; +} + +function ConnectionLostModal(props: ConnectionLostModalProps) { + const { isOpen } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('ConnectionLost')} - </ModalHeader> + <Modal isOpen={isOpen} onModalClose={handleModalClose}> + <ModalContent onModalClose={handleModalClose}> + <ModalHeader>{translate('ConnectionLost')}</ModalHeader> <ModalBody> - <div> - {translate('ConnectionLostToBackend')} - </div> + <div>{translate('ConnectionLostToBackend')}</div> <div className={styles.automatic}> {translate('ConnectionLostReconnect')} </div> </ModalBody> <ModalFooter> - <Button - kind={kinds.PRIMARY} - onPress={onModalClose} - > + <Button kind={kinds.PRIMARY} onPress={handleModalClose}> {translate('Reload')} </Button> </ModalFooter> @@ -48,9 +42,4 @@ function ConnectionLostModal(props) { ); } -ConnectionLostModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js deleted file mode 100644 index 8ab8e3cd0..000000000 --- a/frontend/src/App/ConnectionLostModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import ConnectionLostModal from './ConnectionLostModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 744f11f31..212a24ad1 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -49,6 +49,7 @@ export interface AppSectionState { isConnected: boolean; isReconnecting: boolean; version: string; + prevVersion?: string; dimensions: { isSmallScreen: boolean; width: number; diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index 4b24a8231..1386865e8 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; +import AppUpdatedModal from 'App/AppUpdatedModal'; import ColorImpairedContext from 'App/ColorImpairedContext'; -import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; +import ConnectionLostModal from 'App/ConnectionLostModal'; import SignalRConnector from 'Components/SignalRConnector'; import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import locationShape from 'Helpers/Props/Shapes/locationShape'; @@ -101,12 +101,12 @@ class Page extends Component { {children} </div> - <AppUpdatedModalConnector + <AppUpdatedModal isOpen={this.state.isUpdatedModalOpen} onModalClose={this.onUpdatedModalClose} /> - <ConnectionLostModalConnector + <ConnectionLostModal isOpen={this.state.isConnectionLostModalOpen} onModalClose={this.onConnectionLostModalClose} /> diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index e3a3076c1..c0a5fb882 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -198,8 +198,6 @@ function Updates() { {hasUpdates && ( <div> {items.map((update) => { - const hasChanges = !!update.changes; - return ( <div key={update.version} className={styles.update}> <div className={styles.info}> @@ -249,7 +247,7 @@ function Updates() { ) : null} </div> - {hasChanges ? ( + {update.changes ? ( <div> <UpdateChanges title={translate('New')} diff --git a/frontend/src/typings/Update.ts b/frontend/src/typings/Update.ts index 1e1ff652b..448b1728d 100644 --- a/frontend/src/typings/Update.ts +++ b/frontend/src/typings/Update.ts @@ -13,7 +13,7 @@ interface Update { installedOn: string; installable: boolean; latest: boolean; - changes: Changes; + changes: Changes | null; hash: string; } diff --git a/package.json b/package.json index cc608795a..20f467965 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@babel/preset-typescript": "7.24.1", "@types/lodash": "4.14.194", "@types/qs": "6.9.15", + "@types/react-document-title": "2.0.9", "@types/react-lazyload": "3.2.0", "@types/react-router-dom": "5.3.3", "@types/react-text-truncate": "0.14.1", diff --git a/yarn.lock b/yarn.lock index 55ee05650..3353b7d14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1463,6 +1463,13 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== +"@types/react-document-title@2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.9.tgz#65fd57e6086ef8ced5ad45ac8a439dd878ca71d6" + integrity sha512-Q8bSnESgyVoMCo0vAKJj2N4wD/yl7EnutaFntKpaL/edUUo4kTNH8M6A5iCoje9sknRdLx7cfB39cpdTNr5Z+Q== + dependencies: + "@types/react" "*" + "@types/react-dom@18.2.25": version "18.2.25" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" From ebc5cdb33586110eb365fbf848f6ca3d8b9e626e Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 29 Jul 2024 00:00:19 +0000 Subject: [PATCH 416/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 1 - src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 1 - src/NzbDrone.Core/Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 1 - src/NzbDrone.Core/Localization/Core/tr.json | 1 - src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 9 files changed, 9 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index b0936c991..c6f2c7158 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -114,7 +114,6 @@ "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Activa la gestió de baixades completades si és possible (no admet diversos ordinadors)", "InfoUrl": "URL d'informació", "NoDownloadClientsFound": "No s'han trobat clients de baixada", - "NoHistoryBlocklist": "Sense historial de llistes de bloqueig", "NoHistoryFound": "No s'ha trobat cap historial", "NoIndexersFound": "No s'han trobat indexadors", "NoImportListsFound": "No s'han trobat llistes d'importació", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 24a36fa6a..d593248e8 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -276,7 +276,6 @@ "MonitorSeries": "Monitorizar Series", "NoHistory": "Sin historial", "NoHistoryFound": "No se encontró historial", - "NoHistoryBlocklist": "Sin historial de la lista de bloqueos", "QueueIsEmpty": "La cola está vacía", "Quality": "Calidad", "RefreshAndScanTooltip": "Actualizar información y escanear disco", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index fbe83bee3..4d4e22902 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -767,7 +767,6 @@ "New": "Uutta", "NoHistoryFound": "Historiaa ei löytynyt", "NoEpisodesInThisSeason": "Kaudelle ei ole jaksoja", - "NoHistoryBlocklist": "Estohistoriaa ei ole.", "NoBackupsAreAvailable": "Varmuuskopioita ei ole käytettävissä", "OrganizeNothingToRename": "Valmis! Tuöni on tehty, eikä nimettäviä tiedostoja ole.", "OrganizeModalHeaderSeason": "Järjestellään ja uudelleennimetään - {season}", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 14de69518..9dcf80578 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -226,7 +226,6 @@ "MinimumFreeSpace": "Espace libre minimum", "Monitored": "Surveillé", "NoHistoryFound": "Aucun historique n'a été trouvé", - "NoHistoryBlocklist": "Pas d'historique de liste noire", "Period": "Période", "QualityDefinitionsLoadError": "Impossible de charger les définitions de qualité", "RemoveSelected": "Enlever la sélection", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index f3a6bc1c2..e1e241b77 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -573,7 +573,6 @@ "MoveSeriesFoldersDontMoveFiles": "Nem, én magam mozgatom át a fájlokat", "MoveSeriesFoldersMoveFiles": "Igen, mozgassa át a fájlokat", "MoveSeriesFoldersToNewPath": "Szeretné áthelyezni a sorozatfájlokat a(z) „{originalPath}” helyről a „{destinationPath}” címre?", - "NoHistoryBlocklist": "Nincs előzmény a tiltólistán", "NoHistoryFound": "Nem található előzmény", "NotificationsDiscordSettingsAvatarHelpText": "Módosítsa az integrációból származó üzenetekhez használt avatart", "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Módosítsa a „kézi interakcióról” értesítéshez átadott mezőket", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 305a82bf6..a23e33456 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1075,7 +1075,6 @@ "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Agenda", "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", - "NoHistoryBlocklist": "Não há lista de bloqueio no histórico", "QualityCutoffNotMet": "Corte da Qualidade ainda não foi alcançado", "QueueLoadError": "Falha ao carregar a fila", "RemoveQueueItem": "Remover - {sourceTitle}", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index cbdca46c2..b69123122 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -43,7 +43,6 @@ "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {notificationNames}", "CountIndexersSelected": "{count} выбранных индексаторов", "EditAutoTag": "Редактировать автоматическую маркировку", - "NoHistoryBlocklist": "Нет истории блокировок", "ManageDownloadClients": "Менеджер клиентов загрузки", "ManageImportLists": "Управление списками импорта", "ManageLists": "Управление листами", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 06a8fd01d..fc6919dce 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -445,7 +445,6 @@ "ManageLists": "Listeleri Yönet", "MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.", "Never": "Asla", - "NoHistoryBlocklist": "Geçmiş engellenenler listesi yok", "NoIndexersFound": "Dizinleyici bulunamadı", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.", "NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 0d07d0da6..fcd8dff8d 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1011,7 +1011,6 @@ "NoEpisodesFoundForSelectedSeason": "未找到所选季的集", "NoEpisodesInThisSeason": "本季没有集", "NoHistory": "无历史记录", - "NoHistoryBlocklist": "没有历史黑名单", "NoHistoryFound": "未发现历史记录", "NoLimitForAnyRuntime": "不限制任何运行环境", "NoLinks": "无链接", From 72db8099e0f4abc3176e397f8dda3b2b69026daf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 26 Jul 2024 23:16:03 -0700 Subject: [PATCH 417/762] Convert System to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 8 +- frontend/src/App/State/SystemAppState.ts | 9 + .../Components/Page/Sidebar/PageSidebar.js | 4 +- frontend/src/Components/Table/Column.ts | 1 + frontend/src/System/Status/About/About.js | 135 ---------- frontend/src/System/Status/About/About.tsx | 103 ++++++++ .../src/System/Status/About/AboutConnector.js | 52 ---- frontend/src/System/Status/About/StartTime.js | 93 ------- .../src/System/Status/About/StartTime.tsx | 44 ++++ .../src/System/Status/DiskSpace/DiskSpace.js | 121 --------- .../src/System/Status/DiskSpace/DiskSpace.tsx | 111 ++++++++ .../Status/DiskSpace/DiskSpaceConnector.js | 54 ---- frontend/src/System/Status/Health/Health.js | 242 ------------------ frontend/src/System/Status/Health/Health.tsx | 174 +++++++++++++ .../System/Status/Health/HealthConnector.js | 68 ----- .../System/Status/Health/HealthItemLink.tsx | 65 +++++ .../src/System/Status/Health/HealthStatus.tsx | 56 ++++ .../Status/Health/HealthStatusConnector.js | 79 ------ .../Status/Health/createHealthSelector.ts | 13 + .../src/System/Status/MoreInfo/MoreInfo.js | 101 -------- .../src/System/Status/MoreInfo/MoreInfo.tsx | 92 +++++++ .../System/Status/{Status.js => Status.tsx} | 14 +- .../Tasks/Scheduled/ScheduledTaskRow.js | 203 --------------- .../Tasks/Scheduled/ScheduledTaskRow.tsx | 170 ++++++++++++ .../Scheduled/ScheduledTaskRowConnector.js | 92 ------- .../System/Tasks/Scheduled/ScheduledTasks.js | 85 ------ .../System/Tasks/Scheduled/ScheduledTasks.tsx | 73 ++++++ .../Scheduled/ScheduledTasksConnector.js | 46 ---- .../src/System/Tasks/{Tasks.js => Tasks.tsx} | 4 +- frontend/src/typings/DiskSpace.ts | 8 + frontend/src/typings/Health.ts | 8 + frontend/src/typings/SystemStatus.ts | 4 + frontend/src/typings/Task.ts | 13 + 33 files changed, 960 insertions(+), 1385 deletions(-) delete mode 100644 frontend/src/System/Status/About/About.js create mode 100644 frontend/src/System/Status/About/About.tsx delete mode 100644 frontend/src/System/Status/About/AboutConnector.js delete mode 100644 frontend/src/System/Status/About/StartTime.js create mode 100644 frontend/src/System/Status/About/StartTime.tsx delete mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.js create mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.tsx delete mode 100644 frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js delete mode 100644 frontend/src/System/Status/Health/Health.js create mode 100644 frontend/src/System/Status/Health/Health.tsx delete mode 100644 frontend/src/System/Status/Health/HealthConnector.js create mode 100644 frontend/src/System/Status/Health/HealthItemLink.tsx create mode 100644 frontend/src/System/Status/Health/HealthStatus.tsx delete mode 100644 frontend/src/System/Status/Health/HealthStatusConnector.js create mode 100644 frontend/src/System/Status/Health/createHealthSelector.ts delete mode 100644 frontend/src/System/Status/MoreInfo/MoreInfo.js create mode 100644 frontend/src/System/Status/MoreInfo/MoreInfo.tsx rename frontend/src/System/Status/{Status.js => Status.tsx} (65%) delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js create mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasks.js create mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js rename frontend/src/System/Tasks/{Tasks.js => Tasks.tsx} (79%) create mode 100644 frontend/src/typings/DiskSpace.ts create mode 100644 frontend/src/typings/Health.ts create mode 100644 frontend/src/typings/Task.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 0959e99c5..ddca5b2ba 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -20,7 +20,9 @@ import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState extends AppSectionState<DownloadClient>, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export type GeneralAppState = AppSectionItemState<General>; @@ -32,7 +34,9 @@ export interface ImportListAppState export interface IndexerAppState extends AppSectionState<Indexer>, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export interface NotificationAppState extends AppSectionState<Notification>, diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index 3c150fcfb..d20dacc51 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,13 +1,22 @@ +import DiskSpace from 'typings/DiskSpace'; +import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; +import Task from 'typings/Task'; import Update from 'typings/Update'; import AppSectionState, { AppSectionItemState } from './AppSectionState'; +export type DiskSpaceAppState = AppSectionState<DiskSpace>; +export type HealthAppState = AppSectionState<Health>; export type SystemStatusAppState = AppSectionItemState<SystemStatus>; export type UpdateAppState = AppSectionState<Update>; +export type TaskAppState = AppSectionState<Task>; interface SystemAppState { + diskSpace: DiskSpaceAppState; + health: HealthAppState; updates: UpdateAppState; status: SystemStatusAppState; + tasks: TaskAppState; } export default SystemAppState; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index bf618a87d..0bb1f4e06 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -9,7 +9,7 @@ import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; -import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import HealthStatus from 'System/Status/Health/HealthStatus'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -147,7 +147,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatusConnector + statusComponent: HealthStatus }, { title: () => translate('Tasks'), diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index f5644357b..24674c3fc 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -6,6 +6,7 @@ type PropertyFunction<T> = () => T; interface Column { name: string; label: string | PropertyFunction<string> | React.ReactNode; + className?: string; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js deleted file mode 100644 index 84114b0dc..000000000 --- a/frontend/src/System/Status/About/About.js +++ /dev/null @@ -1,135 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import FieldSet from 'Components/FieldSet'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import StartTime from './StartTime'; -import styles from './About.css'; - -class About extends Component { - - // - // Render - - render() { - const { - version, - packageVersion, - packageAuthor, - isNetCore, - isDocker, - runtimeVersion, - databaseVersion, - databaseType, - migrationVersion, - appData, - startupPath, - mode, - startTime, - timeFormat, - longDateFormat - } = this.props; - - return ( - <FieldSet legend={translate('About')}> - <DescriptionList className={styles.descriptionList}> - <DescriptionListItem - title={translate('Version')} - data={version} - /> - - { - packageVersion && - <DescriptionListItem - title={translate('PackageVersion')} - data={(packageAuthor ? - <InlineMarkdown data={translate('PackageVersionInfo', { - packageVersion, - packageAuthor - })} - /> : - packageVersion - )} - /> - } - - { - isNetCore && - <DescriptionListItem - title={translate('DotNetVersion')} - data={`Yes (${runtimeVersion})`} - /> - } - - { - isDocker && - <DescriptionListItem - title={translate('Docker')} - data={'Yes'} - /> - } - - <DescriptionListItem - title={translate('Database')} - data={`${titleCase(databaseType)} ${databaseVersion}`} - /> - - <DescriptionListItem - title={translate('DatabaseMigration')} - data={migrationVersion} - /> - - <DescriptionListItem - title={translate('AppDataDirectory')} - data={appData} - /> - - <DescriptionListItem - title={translate('StartupDirectory')} - data={startupPath} - /> - - <DescriptionListItem - title={translate('Mode')} - data={titleCase(mode)} - /> - - <DescriptionListItem - title={translate('Uptime')} - data={ - <StartTime - startTime={startTime} - timeFormat={timeFormat} - longDateFormat={longDateFormat} - /> - } - /> - </DescriptionList> - </FieldSet> - ); - } - -} - -About.propTypes = { - version: PropTypes.string.isRequired, - packageVersion: PropTypes.string, - packageAuthor: PropTypes.string, - isNetCore: PropTypes.bool.isRequired, - runtimeVersion: PropTypes.string.isRequired, - isDocker: PropTypes.bool.isRequired, - databaseType: PropTypes.string.isRequired, - databaseVersion: PropTypes.string.isRequired, - migrationVersion: PropTypes.number.isRequired, - appData: PropTypes.string.isRequired, - startupPath: PropTypes.string.isRequired, - mode: PropTypes.string.isRequired, - startTime: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired -}; - -export default About; diff --git a/frontend/src/System/Status/About/About.tsx b/frontend/src/System/Status/About/About.tsx new file mode 100644 index 000000000..1480318ee --- /dev/null +++ b/frontend/src/System/Status/About/About.tsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import FieldSet from 'Components/FieldSet'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import StartTime from './StartTime'; +import styles from './About.css'; + +function About() { + const dispatch = useDispatch(); + const { item } = useSelector((state: AppState) => state.system.status); + + const { + version, + packageVersion, + packageAuthor, + isNetCore, + isDocker, + runtimeVersion, + databaseVersion, + databaseType, + migrationVersion, + appData, + startupPath, + mode, + startTime, + } = item; + + useEffect(() => { + dispatch(fetchStatus()); + }, [dispatch]); + + return ( + <FieldSet legend={translate('About')}> + <DescriptionList className={styles.descriptionList}> + <DescriptionListItem title={translate('Version')} data={version} /> + + {packageVersion && ( + <DescriptionListItem + title={translate('PackageVersion')} + data={ + packageAuthor ? ( + <InlineMarkdown + data={translate('PackageVersionInfo', { + packageVersion, + packageAuthor, + })} + /> + ) : ( + packageVersion + ) + } + /> + )} + + {isNetCore ? ( + <DescriptionListItem + title={translate('DotNetVersion')} + data={`Yes (${runtimeVersion})`} + /> + ) : null} + + {isDocker ? ( + <DescriptionListItem title={translate('Docker')} data={'Yes'} /> + ) : null} + + <DescriptionListItem + title={translate('Database')} + data={`${titleCase(databaseType)} ${databaseVersion}`} + /> + + <DescriptionListItem + title={translate('DatabaseMigration')} + data={migrationVersion} + /> + + <DescriptionListItem + title={translate('AppDataDirectory')} + data={appData} + /> + + <DescriptionListItem + title={translate('StartupDirectory')} + data={startupPath} + /> + + <DescriptionListItem title={translate('Mode')} data={titleCase(mode)} /> + + <DescriptionListItem + title={translate('Uptime')} + data={<StartTime startTime={startTime} />} + /> + </DescriptionList> + </FieldSet> + ); +} + +export default About; diff --git a/frontend/src/System/Status/About/AboutConnector.js b/frontend/src/System/Status/About/AboutConnector.js deleted file mode 100644 index 475d9778b..000000000 --- a/frontend/src/System/Status/About/AboutConnector.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import About from './About'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.status, - createUISettingsSelector(), - (status, uiSettings) => { - return { - ...status.item, - timeFormat: uiSettings.timeFormat, - longDateFormat: uiSettings.longDateFormat - }; - } - ); -} - -const mapDispatchToProps = { - fetchStatus -}; - -class AboutConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchStatus(); - } - - // - // Render - - render() { - return ( - <About - {...this.props} - /> - ); - } -} - -AboutConnector.propTypes = { - fetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector); diff --git a/frontend/src/System/Status/About/StartTime.js b/frontend/src/System/Status/About/StartTime.js deleted file mode 100644 index 08c820add..000000000 --- a/frontend/src/System/Status/About/StartTime.js +++ /dev/null @@ -1,93 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; - -function getUptime(startTime) { - return formatTimeSpan(moment().diff(startTime)); -} - -class StartTime extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - startTime, - timeFormat, - longDateFormat - } = props; - - this._timeoutId = null; - - this.state = { - uptime: getUptime(startTime), - startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true }) - }; - } - - componentDidMount() { - this._timeoutId = setTimeout(this.onTimeout, 1000); - } - - componentDidUpdate(prevProps) { - const { - startTime, - timeFormat, - longDateFormat - } = this.props; - - if ( - startTime !== prevProps.startTime || - timeFormat !== prevProps.timeFormat || - longDateFormat !== prevProps.longDateFormat - ) { - this.setState({ - uptime: getUptime(startTime), - startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true }) - }); - } - } - - componentWillUnmount() { - if (this._timeoutId) { - this._timeoutId = clearTimeout(this._timeoutId); - } - } - - // - // Listeners - - onTimeout = () => { - this.setState({ uptime: getUptime(this.props.startTime) }); - this._timeoutId = setTimeout(this.onTimeout, 1000); - }; - - // - // Render - - render() { - const { - uptime, - startTime - } = this.state; - - return ( - <span title={startTime}> - {uptime} - </span> - ); - } -} - -StartTime.propTypes = { - startTime: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired -}; - -export default StartTime; diff --git a/frontend/src/System/Status/About/StartTime.tsx b/frontend/src/System/Status/About/StartTime.tsx new file mode 100644 index 000000000..0fca7806b --- /dev/null +++ b/frontend/src/System/Status/About/StartTime.tsx @@ -0,0 +1,44 @@ +import moment from 'moment'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; + +interface StartTimeProps { + startTime: string; +} + +function StartTime(props: StartTimeProps) { + const { startTime } = props; + const { timeFormat, longDateFormat } = useSelector( + createUISettingsSelector() + ); + const [time, setTime] = useState(Date.now()); + + const { formattedStartTime, uptime } = useMemo(() => { + return { + uptime: formatTimeSpan(moment(time).diff(startTime)), + formattedStartTime: formatDateTime( + startTime, + longDateFormat, + timeFormat, + { + includeSeconds: true, + } + ), + }; + }, [startTime, time, longDateFormat, timeFormat]); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + + return () => { + clearInterval(interval); + }; + }, [setTime]); + + return <span title={formattedStartTime}>{uptime}</span>; +} + +export default StartTime; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.js b/frontend/src/System/Status/DiskSpace/DiskSpace.js deleted file mode 100644 index d287fed07..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.js +++ /dev/null @@ -1,121 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ProgressBar from 'Components/ProgressBar'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; -import { kinds, sizes } from 'Helpers/Props'; -import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; -import styles from './DiskSpace.css'; - -const columns = [ - { - name: 'path', - label: () => translate('Location'), - isVisible: true - }, - { - name: 'freeSpace', - label: () => translate('FreeSpace'), - isVisible: true - }, - { - name: 'totalSpace', - label: () => translate('TotalSpace'), - isVisible: true - }, - { - name: 'progress', - isVisible: true - } -]; - -class DiskSpace extends Component { - - // - // Render - - render() { - const { - isFetching, - items - } = this.props; - - return ( - <FieldSet legend={translate('DiskSpace')}> - { - isFetching && - <LoadingIndicator /> - } - - { - !isFetching && - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - const { - freeSpace, - totalSpace - } = item; - - const diskUsage = (100 - freeSpace / totalSpace * 100); - let diskUsageKind = kinds.PRIMARY; - - if (diskUsage > 90) { - diskUsageKind = kinds.DANGER; - } else if (diskUsage > 80) { - diskUsageKind = kinds.WARNING; - } - - return ( - <TableRow key={item.path}> - <TableRowCell> - {item.path} - - { - item.label && - ` (${item.label})` - } - </TableRowCell> - - <TableRowCell className={styles.space}> - {formatBytes(freeSpace)} - </TableRowCell> - - <TableRowCell className={styles.space}> - {formatBytes(totalSpace)} - </TableRowCell> - - <TableRowCell className={styles.space}> - <ProgressBar - progress={diskUsage} - kind={diskUsageKind} - size={sizes.MEDIUM} - /> - </TableRowCell> - </TableRow> - ); - }) - } - </TableBody> - </Table> - } - </FieldSet> - ); - } - -} - -DiskSpace.propTypes = { - isFetching: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx new file mode 100644 index 000000000..4a19cf1c9 --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx @@ -0,0 +1,111 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ProgressBar from 'Components/ProgressBar'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import { kinds, sizes } from 'Helpers/Props'; +import { fetchDiskSpace } from 'Store/Actions/systemActions'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import styles from './DiskSpace.css'; + +const columns: Column[] = [ + { + name: 'path', + label: () => translate('Location'), + isVisible: true, + }, + { + name: 'freeSpace', + label: () => translate('FreeSpace'), + isVisible: true, + }, + { + name: 'totalSpace', + label: () => translate('TotalSpace'), + isVisible: true, + }, + { + name: 'progress', + label: '', + isVisible: true, + }, +]; + +function createDiskSpaceSelector() { + return createSelector( + (state: AppState) => state.system.diskSpace, + (diskSpace) => { + return diskSpace; + } + ); +} + +function DiskSpace() { + const dispatch = useDispatch(); + const { isFetching, items } = useSelector(createDiskSpaceSelector()); + + useEffect(() => { + dispatch(fetchDiskSpace()); + }, [dispatch]); + + return ( + <FieldSet legend={translate('DiskSpace')}> + {isFetching ? <LoadingIndicator /> : null} + + {isFetching ? null : ( + <Table columns={columns}> + <TableBody> + {items.map((item) => { + const { freeSpace, totalSpace } = item; + + const diskUsage = 100 - (freeSpace / totalSpace) * 100; + let diskUsageKind = kinds.PRIMARY; + + if (diskUsage > 90) { + diskUsageKind = kinds.DANGER; + } else if (diskUsage > 80) { + diskUsageKind = kinds.WARNING; + } + + return ( + <TableRow key={item.path}> + <TableRowCell> + {item.path} + + {item.label && ` (${item.label})`} + </TableRowCell> + + <TableRowCell className={styles.space}> + {formatBytes(freeSpace)} + </TableRowCell> + + <TableRowCell className={styles.space}> + {formatBytes(totalSpace)} + </TableRowCell> + + <TableRowCell className={styles.space}> + <ProgressBar + progress={diskUsage} + kind={diskUsageKind} + size={sizes.MEDIUM} + /> + </TableRowCell> + </TableRow> + ); + })} + </TableBody> + </Table> + )} + </FieldSet> + ); +} + +export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js deleted file mode 100644 index 3049b2ead..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchDiskSpace } from 'Store/Actions/systemActions'; -import DiskSpace from './DiskSpace'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.diskSpace, - (diskSpace) => { - const { - isFetching, - items - } = diskSpace; - - return { - isFetching, - items - }; - } - ); -} - -const mapDispatchToProps = { - fetchDiskSpace -}; - -class DiskSpaceConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchDiskSpace(); - } - - // - // Render - - render() { - return ( - <DiskSpace - {...this.props} - /> - ); - } -} - -DiskSpaceConnector.propTypes = { - fetchDiskSpace: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector); diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js deleted file mode 100644 index 0a8a2e5a9..000000000 --- a/frontend/src/System/Status/Health/Health.js +++ /dev/null @@ -1,242 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './Health.css'; - -function getInternalLink(source) { - switch (source) { - case 'IndexerRssCheck': - case 'IndexerSearchCheck': - case 'IndexerStatusCheck': - case 'IndexerJackettAllCheck': - case 'IndexerLongTermStatusCheck': - return ( - <IconButton - name={icons.SETTINGS} - title={translate('Settings')} - to="/settings/indexers" - /> - ); - case 'DownloadClientCheck': - case 'DownloadClientStatusCheck': - case 'ImportMechanismCheck': - return ( - <IconButton - name={icons.SETTINGS} - title={translate('Settings')} - to="/settings/downloadclients" - /> - ); - case 'NotificationStatusCheck': - return ( - <IconButton - name={icons.SETTINGS} - title={translate('Settings')} - to="/settings/connect" - /> - ); - case 'RootFolderCheck': - return ( - <IconButton - name={icons.SERIES_CONTINUING} - title={translate('SeriesEditor')} - to="/serieseditor" - /> - ); - case 'UpdateCheck': - return ( - <IconButton - name={icons.UPDATE} - title={translate('Updates')} - to="/system/updates" - /> - ); - default: - return; - } -} - -function getTestLink(source, props) { - switch (source) { - case 'IndexerStatusCheck': - case 'IndexerLongTermStatusCheck': - return ( - <SpinnerIconButton - name={icons.TEST} - title={translate('TestAll')} - isSpinning={props.isTestingAllIndexers} - onPress={props.dispatchTestAllIndexers} - /> - ); - case 'DownloadClientCheck': - case 'DownloadClientStatusCheck': - return ( - <SpinnerIconButton - name={icons.TEST} - title={translate('TestAll')} - isSpinning={props.isTestingAllDownloadClients} - onPress={props.dispatchTestAllDownloadClients} - /> - ); - - default: - break; - } -} - -const columns = [ - { - className: styles.status, - name: 'type', - isVisible: true - }, - { - name: 'message', - label: () => translate('Message'), - isVisible: true - }, - { - name: 'actions', - label: () => translate('Actions'), - isVisible: true - } -]; - -class Health extends Component { - - // - // Render - - render() { - const { - isFetching, - isPopulated, - items - } = this.props; - - const healthIssues = !!items.length; - - return ( - <FieldSet - legend={ - <div className={styles.legend}> - {translate('Health')} - - { - isFetching && isPopulated && - <LoadingIndicator - className={styles.loading} - size={20} - /> - } - </div> - } - > - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - !healthIssues && - <div className={styles.healthOk}> - {translate('NoIssuesWithYourConfiguration')} - </div> - } - - { - healthIssues && - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - const internalLink = getInternalLink(item.source); - const testLink = getTestLink(item.source, this.props); - - let kind = kinds.WARNING; - switch (item.type.toLowerCase()) { - case 'error': - kind = kinds.DANGER; - break; - default: - case 'warning': - kind = kinds.WARNING; - break; - case 'notice': - kind = kinds.INFO; - break; - } - - return ( - <TableRow key={`health${item.message}`}> - <TableRowCell> - <Icon - name={icons.DANGER} - kind={kind} - title={titleCase(item.type)} - /> - </TableRowCell> - - <TableRowCell>{item.message}</TableRowCell> - - <TableRowCell> - <IconButton - name={icons.WIKI} - to={item.wikiUrl} - title={translate('ReadTheWikiForMoreInformation')} - /> - - { - internalLink - } - - { - !!testLink && - testLink - } - </TableRowCell> - </TableRow> - ); - }) - } - </TableBody> - </Table> - } - { - healthIssues && - <Alert kind={kinds.INFO}> - <InlineMarkdown data={translate('HealthMessagesInfoBox', { link: '/system/logs/files' })} /> - </Alert> - } - </FieldSet> - ); - } - -} - -Health.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - isTestingAllDownloadClients: PropTypes.bool.isRequired, - isTestingAllIndexers: PropTypes.bool.isRequired, - dispatchTestAllDownloadClients: PropTypes.func.isRequired, - dispatchTestAllIndexers: PropTypes.func.isRequired -}; - -export default Health; diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx new file mode 100644 index 000000000..281d95ac6 --- /dev/null +++ b/frontend/src/System/Status/Health/Health.tsx @@ -0,0 +1,174 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import { icons, kinds } from 'Helpers/Props'; +import { + testAllDownloadClients, + testAllIndexers, +} from 'Store/Actions/settingsActions'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import createHealthSelector from './createHealthSelector'; +import HealthItemLink from './HealthItemLink'; +import styles from './Health.css'; + +const columns: Column[] = [ + { + className: styles.status, + name: 'type', + label: '', + isVisible: true, + }, + { + name: 'message', + label: () => translate('Message'), + isVisible: true, + }, + { + name: 'actions', + label: () => translate('Actions'), + isVisible: true, + }, +]; + +function Health() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + createHealthSelector() + ); + const isTestingAllDownloadClients = useSelector( + (state: AppState) => state.settings.downloadClients.isTestingAll + ); + const isTestingAllIndexers = useSelector( + (state: AppState) => state.settings.indexers.isTestingAll + ); + + const healthIssues = !!items.length; + + const handleTestAllDownloadClientsPress = useCallback(() => { + dispatch(testAllDownloadClients()); + }, [dispatch]); + + const handleTestAllIndexersPress = useCallback(() => { + dispatch(testAllIndexers()); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchHealth()); + }, [dispatch]); + + return ( + <FieldSet + legend={ + <div className={styles.legend}> + {translate('Health')} + + {isFetching && isPopulated ? ( + <LoadingIndicator className={styles.loading} size={20} /> + ) : null} + </div> + } + > + {isFetching && !isPopulated ? <LoadingIndicator /> : null} + + {isPopulated && !healthIssues ? ( + <div className={styles.healthOk}> + {translate('NoIssuesWithYourConfiguration')} + </div> + ) : null} + + {healthIssues ? ( + <> + <Table columns={columns}> + <TableBody> + {items.map((item) => { + const source = item.source; + + let kind = kinds.WARNING; + switch (item.type.toLowerCase()) { + case 'error': + kind = kinds.DANGER; + break; + default: + case 'warning': + kind = kinds.WARNING; + break; + case 'notice': + kind = kinds.INFO; + break; + } + + return ( + <TableRow key={`health${item.message}`}> + <TableRowCell> + <Icon + name={icons.DANGER} + kind={kind} + title={titleCase(item.type)} + /> + </TableRowCell> + + <TableRowCell>{item.message}</TableRowCell> + + <TableRowCell> + <IconButton + name={icons.WIKI} + to={item.wikiUrl} + title={translate('ReadTheWikiForMoreInformation')} + /> + + <HealthItemLink source={source} /> + + {source === 'IndexerStatusCheck' || + source === 'IndexerLongTermStatusCheck' ? ( + <SpinnerIconButton + name={icons.TEST} + title={translate('TestAll')} + isSpinning={isTestingAllIndexers} + onPress={handleTestAllIndexersPress} + /> + ) : null} + + {source === 'DownloadClientCheck' || + source === 'DownloadClientStatusCheck' ? ( + <SpinnerIconButton + name={icons.TEST} + title={translate('TestAll')} + isSpinning={isTestingAllDownloadClients} + onPress={handleTestAllDownloadClientsPress} + /> + ) : null} + </TableRowCell> + </TableRow> + ); + })} + </TableBody> + </Table> + + <Alert kind={kinds.INFO}> + <InlineMarkdown + data={translate('HealthMessagesInfoBox', { + link: '/system/logs/files', + })} + /> + </Alert> + </> + ) : null} + </FieldSet> + ); +} + +export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js deleted file mode 100644 index 8165f3e3b..000000000 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ /dev/null @@ -1,68 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { testAllDownloadClients, testAllIndexers } from 'Store/Actions/settingsActions'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import Health from './Health'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.health, - (state) => state.settings.downloadClients.isTestingAll, - (state) => state.settings.indexers.isTestingAll, - (health, isTestingAllDownloadClients, isTestingAllIndexers) => { - const { - isFetching, - isPopulated, - items - } = health; - - return { - isFetching, - isPopulated, - items, - isTestingAllDownloadClients, - isTestingAllIndexers - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchHealth: fetchHealth, - dispatchTestAllDownloadClients: testAllDownloadClients, - dispatchTestAllIndexers: testAllIndexers -}; - -class HealthConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchHealth(); - } - - // - // Render - - render() { - const { - dispatchFetchHealth, - ...otherProps - } = this.props; - - return ( - <Health - {...otherProps} - /> - ); - } -} - -HealthConnector.propTypes = { - dispatchFetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthItemLink.tsx b/frontend/src/System/Status/Health/HealthItemLink.tsx new file mode 100644 index 000000000..ac3bafade --- /dev/null +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +interface HealthItemLinkProps { + source: string; +} + +function HealthItemLink(props: HealthItemLinkProps) { + const { source } = props; + + switch (source) { + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + case 'IndexerJackettAllCheck': + case 'IndexerLongTermStatusCheck': + return ( + <IconButton + name={icons.SETTINGS} + title={translate('Settings')} + to="/settings/indexers" + /> + ); + case 'DownloadClientCheck': + case 'DownloadClientStatusCheck': + case 'ImportMechanismCheck': + return ( + <IconButton + name={icons.SETTINGS} + title={translate('Settings')} + to="/settings/downloadclients" + /> + ); + case 'NotificationStatusCheck': + return ( + <IconButton + name={icons.SETTINGS} + title={translate('Settings')} + to="/settings/connect" + /> + ); + case 'RootFolderCheck': + return ( + <IconButton + name={icons.SERIES_CONTINUING} + title={translate('SeriesEditor')} + to="/serieseditor" + /> + ); + case 'UpdateCheck': + return ( + <IconButton + name={icons.UPDATE} + title={translate('Updates')} + to="/system/updates" + /> + ); + default: + return null; + } +} + +export default HealthItemLink; diff --git a/frontend/src/System/Status/Health/HealthStatus.tsx b/frontend/src/System/Status/Health/HealthStatus.tsx new file mode 100644 index 000000000..b12fd3ebb --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatus.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import createHealthSelector from './createHealthSelector'; + +function HealthStatus() { + const dispatch = useDispatch(); + const { isConnected, isReconnecting } = useSelector( + (state: AppState) => state.app + ); + const { isPopulated, items } = useSelector(createHealthSelector()); + + const wasReconnecting = usePrevious(isReconnecting); + + const { count, errors, warnings } = useMemo(() => { + let errors = false; + let warnings = false; + + items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + count: items.length, + errors, + warnings, + }; + }, [items]); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchHealth()); + } + }, [isPopulated, dispatch]); + + useEffect(() => { + if (isConnected && wasReconnecting) { + dispatch(fetchHealth()); + } + }, [isConnected, wasReconnecting, dispatch]); + + return ( + <PageSidebarStatus count={count} errors={errors} warnings={warnings} /> + ); +} + +export default HealthStatus; diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js deleted file mode 100644 index 765baa351..000000000 --- a/frontend/src/System/Status/Health/HealthStatusConnector.js +++ /dev/null @@ -1,79 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; -import { fetchHealth } from 'Store/Actions/systemActions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app, - (state) => state.system.health, - (app, health) => { - const count = health.items.length; - let errors = false; - let warnings = false; - - health.items.forEach((item) => { - if (item.type === 'error') { - errors = true; - } - - if (item.type === 'warning') { - warnings = true; - } - }); - - return { - isConnected: app.isConnected, - isReconnecting: app.isReconnecting, - isPopulated: health.isPopulated, - count, - errors, - warnings - }; - } - ); -} - -const mapDispatchToProps = { - fetchHealth -}; - -class HealthStatusConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.fetchHealth(); - } - } - - componentDidUpdate(prevProps) { - if (this.props.isConnected && prevProps.isReconnecting) { - this.props.fetchHealth(); - } - } - - // - // Render - - render() { - return ( - <PageSidebarStatus - {...this.props} - /> - ); - } -} - -HealthStatusConnector.propTypes = { - isConnected: PropTypes.bool.isRequired, - isReconnecting: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - fetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/Health/createHealthSelector.ts b/frontend/src/System/Status/Health/createHealthSelector.ts new file mode 100644 index 000000000..f38e3fe88 --- /dev/null +++ b/frontend/src/System/Status/Health/createHealthSelector.ts @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createHealthSelector() { + return createSelector( + (state: AppState) => state.system.health, + (health) => { + return health; + } + ); +} + +export default createHealthSelector; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js deleted file mode 100644 index 95d384fef..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ /dev/null @@ -1,101 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; -import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; -import FieldSet from 'Components/FieldSet'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; - -class MoreInfo extends Component { - - // - // Render - - render() { - return ( - <FieldSet legend={translate('MoreInfo')}> - <DescriptionList> - <DescriptionListItemTitle> - {translate('HomePage')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://sonarr.tv/">sonarr.tv</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Wiki')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://wiki.servarr.com/sonarr">wiki.servarr.com/sonarr</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Forums')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Twitter')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://twitter.com/sonarrtv">@sonarrtv</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Discord')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://discord.sonarr.tv/">discord.sonarr.tv</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('IRC')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="irc://irc.libera.chat/#sonarr"> - {translate('IRCLinkText')} - </Link> - </DescriptionListItemDescription> - <DescriptionListItemDescription> - <Link to="https://web.libera.chat/?channels=#sonarr"> - {translate('LiberaWebchat')} - </Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Donations')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://sonarr.tv/donate">sonarr.tv/donate</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('Source')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://github.com/Sonarr/Sonarr/">github.com/Sonarr/Sonarr</Link> - </DescriptionListItemDescription> - - <DescriptionListItemTitle> - {translate('FeatureRequests')} - </DescriptionListItemTitle> - <DescriptionListItemDescription> - <Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link> - </DescriptionListItemDescription> - <DescriptionListItemDescription> - <Link to="https://github.com/Sonarr/Sonarr/issues">github.com/Sonarr/Sonarr/issues</Link> - </DescriptionListItemDescription> - - </DescriptionList> - </FieldSet> - ); - } -} - -MoreInfo.propTypes = { - -}; - -export default MoreInfo; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx new file mode 100644 index 000000000..c4ec06575 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import FieldSet from 'Components/FieldSet'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; + +function MoreInfo() { + return ( + <FieldSet legend={translate('MoreInfo')}> + <DescriptionList> + <DescriptionListItemTitle> + {translate('HomePage')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://sonarr.tv/">sonarr.tv</Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://wiki.servarr.com/sonarr"> + wiki.servarr.com/sonarr + </Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('Forums')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('Twitter')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://twitter.com/sonarrtv">@sonarrtv</Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('Discord')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://discord.sonarr.tv/">discord.sonarr.tv</Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle>{translate('IRC')}</DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="irc://irc.libera.chat/#sonarr"> + {translate('IRCLinkText')} + </Link> + </DescriptionListItemDescription> + <DescriptionListItemDescription> + <Link to="https://web.libera.chat/?channels=#sonarr"> + {translate('LiberaWebchat')} + </Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('Donations')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://sonarr.tv/donate">sonarr.tv/donate</Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('Source')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://github.com/Sonarr/Sonarr/"> + github.com/Sonarr/Sonarr + </Link> + </DescriptionListItemDescription> + + <DescriptionListItemTitle> + {translate('FeatureRequests')} + </DescriptionListItemTitle> + <DescriptionListItemDescription> + <Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link> + </DescriptionListItemDescription> + <DescriptionListItemDescription> + <Link to="https://github.com/Sonarr/Sonarr/issues"> + github.com/Sonarr/Sonarr/issues + </Link> + </DescriptionListItemDescription> + </DescriptionList> + </FieldSet> + ); +} + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.tsx similarity index 65% rename from frontend/src/System/Status/Status.js rename to frontend/src/System/Status/Status.tsx index 429a149ee..ae1636b3e 100644 --- a/frontend/src/System/Status/Status.js +++ b/frontend/src/System/Status/Status.tsx @@ -2,13 +2,12 @@ import React, { Component } from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import AboutConnector from './About/AboutConnector'; -import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector'; -import HealthConnector from './Health/HealthConnector'; +import About from './About/About'; +import DiskSpace from './DiskSpace/DiskSpace'; +import Health from './Health/Health'; import MoreInfo from './MoreInfo/MoreInfo'; class Status extends Component { - // // Render @@ -16,15 +15,14 @@ class Status extends Component { return ( <PageContent title={translate('Status')}> <PageContentBody> - <HealthConnector /> - <DiskSpaceConnector /> - <AboutConnector /> + <Health /> + <DiskSpace /> + <About /> <MoreInfo /> </PageContentBody> </PageContent> ); } - } export default Status; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js deleted file mode 100644 index acb8c8d36..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js +++ /dev/null @@ -1,203 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import styles from './ScheduledTaskRow.css'; - -function getFormattedDates(props) { - const { - lastExecution, - nextExecution, - interval, - showRelativeDates, - shortDateFormat - } = props; - - const isDisabled = interval === 0; - - if (showRelativeDates) { - return { - lastExecutionTime: moment(lastExecution).fromNow(), - nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow() - }; - } - - return { - lastExecutionTime: formatDate(lastExecution, shortDateFormat), - nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat) - }; -} - -class ScheduledTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = getFormattedDates(props); - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - lastExecution, - nextExecution - } = this.props; - - if ( - lastExecution !== prevProps.lastExecution || - nextExecution !== prevProps.nextExecution - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Listeners - - setUpdateTimer() { - const { interval } = this.props; - const timeout = interval < 60 ? 10000 : 60000; - - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, timeout); - } - - // - // Render - - render() { - const { - name, - interval, - lastExecution, - lastStartTime, - lastDuration, - nextExecution, - isQueued, - isExecuting, - longDateFormat, - timeFormat, - onExecutePress - } = this.props; - - const { - lastExecutionTime, - nextExecutionTime - } = this.state; - - const isDisabled = interval === 0; - const executeNow = !isDisabled && moment().isAfter(nextExecution); - const hasNextExecutionTime = !isDisabled && !executeNow; - const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); - const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); - - return ( - <TableRow> - <TableRowCell>{name}</TableRowCell> - <TableRowCell - className={styles.interval} - > - {isDisabled ? 'disabled' : duration} - </TableRowCell> - - <TableRowCell - className={styles.lastExecution} - title={formatDateTime(lastExecution, longDateFormat, timeFormat)} - > - {lastExecutionTime} - </TableRowCell> - - { - !hasLastStartTime && - <TableRowCell className={styles.lastDuration}>-</TableRowCell> - } - - { - hasLastStartTime && - <TableRowCell - className={styles.lastDuration} - title={lastDuration} - > - {formatTimeSpan(lastDuration)} - </TableRowCell> - } - - { - isDisabled && - <TableRowCell className={styles.nextExecution}>-</TableRowCell> - } - - { - executeNow && isQueued && - <TableRowCell className={styles.nextExecution}>queued</TableRowCell> - } - - { - executeNow && !isQueued && - <TableRowCell className={styles.nextExecution}>now</TableRowCell> - } - - { - hasNextExecutionTime && - <TableRowCell - className={styles.nextExecution} - title={formatDateTime(nextExecution, longDateFormat, timeFormat, { includeSeconds: true })} - > - {nextExecutionTime} - </TableRowCell> - } - - <TableRowCell - className={styles.actions} - > - <SpinnerIconButton - name={icons.REFRESH} - spinningName={icons.REFRESH} - isSpinning={isExecuting} - onPress={onExecutePress} - /> - </TableRowCell> - </TableRow> - ); - } -} - -ScheduledTaskRow.propTypes = { - name: PropTypes.string.isRequired, - interval: PropTypes.number.isRequired, - lastExecution: PropTypes.string.isRequired, - lastStartTime: PropTypes.string.isRequired, - lastDuration: PropTypes.string.isRequired, - nextExecution: PropTypes.string.isRequired, - isQueued: PropTypes.bool.isRequired, - isExecuting: PropTypes.bool.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onExecutePress: PropTypes.func.isRequired -}; - -export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx new file mode 100644 index 000000000..3a3cd02de --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx @@ -0,0 +1,170 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { isCommandExecuting } from 'Utilities/Command'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import styles from './ScheduledTaskRow.css'; + +interface ScheduledTaskRowProps { + id: number; + taskName: string; + name: string; + interval: number; + lastExecution: string; + lastStartTime: string; + lastDuration: string; + nextExecution: string; +} + +function ScheduledTaskRow(props: ScheduledTaskRowProps) { + const { + id, + taskName, + name, + interval, + lastExecution, + lastStartTime, + lastDuration, + nextExecution, + } = props; + + const dispatch = useDispatch(); + + const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } = + useSelector(createUISettingsSelector()); + const command = useSelector(createCommandSelector(taskName)); + + const [time, setTime] = useState(Date.now()); + + const isQueued = !!(command && command.status === 'queued'); + const isExecuting = isCommandExecuting(command); + const wasExecuting = usePrevious(isExecuting); + const isDisabled = interval === 0; + const executeNow = !isDisabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !isDisabled && !executeNow; + const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); + + const duration = useMemo(() => { + return moment + .duration(interval, 'minutes') + .humanize() + .replace(/an?(?=\s)/, '1'); + }, [interval]); + + const { lastExecutionTime, nextExecutionTime } = useMemo(() => { + const isDisabled = interval === 0; + + if (showRelativeDates && time) { + return { + lastExecutionTime: moment(lastExecution).fromNow(), + nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow(), + }; + } + + return { + lastExecutionTime: formatDate(lastExecution, shortDateFormat), + nextExecutionTime: isDisabled + ? '-' + : formatDate(nextExecution, shortDateFormat), + }; + }, [ + time, + interval, + lastExecution, + nextExecution, + showRelativeDates, + shortDateFormat, + ]); + + const handleExecutePress = useCallback(() => { + dispatch( + executeCommand({ + name: taskName, + }) + ); + }, [taskName, dispatch]); + + useEffect(() => { + if (!isExecuting && wasExecuting) { + setTimeout(() => { + dispatch(fetchTask({ id })); + }, 1000); + } + }, [id, isExecuting, wasExecuting, dispatch]); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + return () => { + clearInterval(interval); + }; + }, [setTime]); + + return ( + <TableRow> + <TableRowCell>{name}</TableRowCell> + <TableRowCell className={styles.interval}> + {isDisabled ? 'disabled' : duration} + </TableRowCell> + + <TableRowCell + className={styles.lastExecution} + title={formatDateTime(lastExecution, longDateFormat, timeFormat)} + > + {lastExecutionTime} + </TableRowCell> + + {hasLastStartTime ? ( + <TableRowCell className={styles.lastDuration} title={lastDuration}> + {formatTimeSpan(lastDuration)} + </TableRowCell> + ) : ( + <TableRowCell className={styles.lastDuration}>-</TableRowCell> + )} + + {isDisabled ? ( + <TableRowCell className={styles.nextExecution}>-</TableRowCell> + ) : null} + + {executeNow && isQueued ? ( + <TableRowCell className={styles.nextExecution}>queued</TableRowCell> + ) : null} + + {executeNow && !isQueued ? ( + <TableRowCell className={styles.nextExecution}>now</TableRowCell> + ) : null} + + {hasNextExecutionTime ? ( + <TableRowCell + className={styles.nextExecution} + title={formatDateTime(nextExecution, longDateFormat, timeFormat, { + includeSeconds: true, + })} + > + {nextExecutionTime} + </TableRowCell> + ) : null} + + <TableRowCell className={styles.actions}> + <SpinnerIconButton + name={icons.REFRESH} + spinningName={icons.REFRESH} + isSpinning={isExecuting} + onPress={handleExecutePress} + /> + </TableRowCell> + </TableRow> + ); +} + +export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js deleted file mode 100644 index dae790d68..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js +++ /dev/null @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchTask } from 'Store/Actions/systemActions'; -import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { findCommand, isCommandExecuting } from 'Utilities/Command'; -import ScheduledTaskRow from './ScheduledTaskRow'; - -function createMapStateToProps() { - return createSelector( - (state, { taskName }) => taskName, - createCommandsSelector(), - createUISettingsSelector(), - (taskName, commands, uiSettings) => { - const command = findCommand(commands, { name: taskName }); - - return { - isQueued: !!(command && command.state === 'queued'), - isExecuting: isCommandExecuting(command), - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - const taskName = props.taskName; - - return { - dispatchFetchTask() { - dispatch(fetchTask({ - id: props.id - })); - }, - - onExecutePress() { - dispatch(executeCommand({ - name: taskName - })); - } - }; -} - -class ScheduledTaskRowConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - const { - isExecuting, - dispatchFetchTask - } = this.props; - - if (!isExecuting && prevProps.isExecuting) { - // Give the host a moment to update after the command completes - setTimeout(() => { - dispatchFetchTask(); - }, 1000); - } - } - - // - // Render - - render() { - const { - dispatchFetchTask, - ...otherProps - } = this.props; - - return ( - <ScheduledTaskRow - {...otherProps} - /> - ); - } -} - -ScheduledTaskRowConnector.propTypes = { - id: PropTypes.number.isRequired, - isExecuting: PropTypes.bool.isRequired, - dispatchFetchTask: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector); diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js deleted file mode 100644 index bec151613..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js +++ /dev/null @@ -1,85 +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 ScheduledTaskRowConnector from './ScheduledTaskRowConnector'; - -const columns = [ - { - name: 'name', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'interval', - label: () => translate('Interval'), - isVisible: true - }, - { - name: 'lastExecution', - label: () => translate('LastExecution'), - isVisible: true - }, - { - name: 'lastDuration', - label: () => translate('LastDuration'), - isVisible: true - }, - { - name: 'nextExecution', - label: () => translate('NextExecution'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function ScheduledTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( - <FieldSet legend={translate('Scheduled')}> - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - isPopulated && - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - return ( - <ScheduledTaskRowConnector - key={item.id} - {...item} - /> - ); - }) - } - </TableBody> - </Table> - } - </FieldSet> - ); -} - -ScheduledTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx new file mode 100644 index 000000000..fcf5764bb --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx @@ -0,0 +1,73 @@ +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 Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import translate from 'Utilities/String/translate'; +import ScheduledTaskRow from './ScheduledTaskRow'; + +const columns: Column[] = [ + { + name: 'name', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'interval', + label: () => translate('Interval'), + isVisible: true, + }, + { + name: 'lastExecution', + label: () => translate('LastExecution'), + isVisible: true, + }, + { + name: 'lastDuration', + label: () => translate('LastDuration'), + isVisible: true, + }, + { + name: 'nextExecution', + label: () => translate('NextExecution'), + isVisible: true, + }, + { + name: 'actions', + label: '', + isVisible: true, + }, +]; + +function ScheduledTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.system.tasks + ); + + useEffect(() => { + dispatch(fetchTasks()); + }, [dispatch]); + + return ( + <FieldSet legend={translate('Scheduled')}> + {isFetching && !isPopulated && <LoadingIndicator />} + + {isPopulated && ( + <Table columns={columns}> + <TableBody> + {items.map((item) => { + return <ScheduledTaskRow key={item.id} {...item} />; + })} + </TableBody> + </Table> + )} + </FieldSet> + ); +} + +export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js deleted file mode 100644 index 8f418d3bb..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.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 { fetchTasks } from 'Store/Actions/systemActions'; -import ScheduledTasks from './ScheduledTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.tasks, - (tasks) => { - return tasks; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchTasks: fetchTasks -}; - -class ScheduledTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchTasks(); - } - - // - // Render - - render() { - return ( - <ScheduledTasks - {...this.props} - /> - ); - } -} - -ScheduledTasksConnector.propTypes = { - dispatchFetchTasks: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.tsx similarity index 79% rename from frontend/src/System/Tasks/Tasks.js rename to frontend/src/System/Tasks/Tasks.tsx index 03a3b6ce4..26473d7ba 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.tsx @@ -3,13 +3,13 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; import QueuedTasks from './Queued/QueuedTasks'; -import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; +import ScheduledTasks from './Scheduled/ScheduledTasks'; function Tasks() { return ( <PageContent title={translate('Tasks')}> <PageContentBody> - <ScheduledTasksConnector /> + <ScheduledTasks /> <QueuedTasks /> </PageContentBody> </PageContent> diff --git a/frontend/src/typings/DiskSpace.ts b/frontend/src/typings/DiskSpace.ts new file mode 100644 index 000000000..82083eecc --- /dev/null +++ b/frontend/src/typings/DiskSpace.ts @@ -0,0 +1,8 @@ +interface DiskSpace { + path: string; + label: string; + freeSpace: number; + totalSpace: number; +} + +export default DiskSpace; diff --git a/frontend/src/typings/Health.ts b/frontend/src/typings/Health.ts new file mode 100644 index 000000000..66f385bbb --- /dev/null +++ b/frontend/src/typings/Health.ts @@ -0,0 +1,8 @@ +interface Health { + source: string; + type: string; + message: string; + wikiUrl: string; +} + +export default Health; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index 47f2b3552..d5eab3ca3 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -4,6 +4,8 @@ interface SystemStatus { authentication: string; branch: string; buildTime: string; + databaseVersion: string; + databaseType: string; instanceName: string; isAdmin: boolean; isDebug: boolean; @@ -18,8 +20,10 @@ interface SystemStatus { mode: string; osName: string; osVersion: string; + packageAuthor: string; packageUpdateMechanism: string; packageUpdateMechanismMessage: string; + packageVersion: string; runtimeName: string; runtimeVersion: string; sqliteVersion: string; diff --git a/frontend/src/typings/Task.ts b/frontend/src/typings/Task.ts new file mode 100644 index 000000000..57895d73e --- /dev/null +++ b/frontend/src/typings/Task.ts @@ -0,0 +1,13 @@ +import ModelBase from 'App/ModelBase'; + +interface Task extends ModelBase { + name: string; + taskName: string; + interval: number; + lastExecution: string; + lastStartTime: string; + nextExecution: string; + lastDuration: string; +} + +export default Task; From cc85a28ff7cf85d844bfcc384f9833cb6bfe0210 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 30 Jul 2024 10:25:24 +0000 Subject: [PATCH 418/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Wolfy The Broccoly <theproviderofsolace@gmail.com> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 9 +++++++-- src/NzbDrone.Core/Localization/Core/fr.json | 6 ++++-- src/NzbDrone.Core/Localization/Core/pt_BR.json | 9 +++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index d593248e8..de29b9547 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1839,7 +1839,7 @@ "Titles": "Títulos", "ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar", "TotalFileSize": "Tamaño total de archivo", - "UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización", + "UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización: {version}", "UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado", "UrlBase": "URL base", "UseSsl": "Usar SSL", @@ -2093,5 +2093,10 @@ "CountVotes": "{votes} votos", "InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?", "InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.", - "NextAiringDate": "Siguiente emisión: {date}" + "NextAiringDate": "Siguiente emisión: {date}", + "SeasonsMonitoredAll": "Todas", + "SeasonsMonitoredNone": "Ninguna", + "SeasonsMonitoredStatus": "Temporadas monitorizadas", + "NoBlocklistItems": "Ningún elemento en la lista de bloqueo", + "SeasonsMonitoredPartial": "Parcial" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 9dcf80578..9c4ae95c9 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -452,7 +452,7 @@ "RootFolderSelectFreeSpace": "{freeSpace} Libre", "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", + "UpdateAll": "Actualiser", "EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif", "UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}", "RssSync": "Synchronisation RSS", @@ -2076,5 +2076,7 @@ "UnableToImportAutomatically": "Impossible d'importer automatiquement", "DayOfWeekAt": "{day} à {time}", "TomorrowAt": "Demain à {time}", - "TodayAt": "Aujourd'hui à {time}" + "TodayAt": "Aujourd'hui à {time}", + "ShowTagsHelpText": "Afficher les labels sous l'affiche", + "ShowTags": "Afficher les labels" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index a23e33456..f603040a0 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -51,7 +51,7 @@ "SizeOnDisk": "Tamanho no disco", "SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido", "Unmonitored": "Não monitorado", - "UpdateAvailableHealthCheckMessage": "Nova atualização está disponível", + "UpdateAvailableHealthCheckMessage": "Nova atualização disponível: {version}", "Added": "Adicionado", "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "RemoveCompletedDownloads": "Remover downloads concluídos", @@ -2093,5 +2093,10 @@ "Install": "Instalar", "InstallMajorVersionUpdate": "Instalar Atualização", "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.", - "NextAiringDate": "Próxima Exibição: {date}" + "NextAiringDate": "Próxima Exibição: {date}", + "SeasonsMonitoredAll": "Todas", + "SeasonsMonitoredPartial": "Parcial", + "SeasonsMonitoredNone": "Nenhuma", + "SeasonsMonitoredStatus": "Temporadas monitoradas", + "NoBlocklistItems": "Sem itens na lista de bloqueio" } From 9127a91dfc460f442498a00faed98737047098cd Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jul 2024 20:16:51 -0700 Subject: [PATCH 419/762] Fixed: Allow leading/trailing spaces on non-Windows Closes #6971 --- .../PathExtensionFixture.cs | 11 +++++++++- .../Extensions/PathExtensions.cs | 20 +++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index ddb54c538..2daf8b7bd 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -380,8 +380,17 @@ namespace NzbDrone.Common.Test [TestCase(@" C:\Test\TV\")] [TestCase(@" C:\Test\TV")] - public void IsPathValid_should_be_false(string path) + public void IsPathValid_should_be_false_on_windows(string path) { + WindowsOnly(); + path.IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); + } + + [TestCase(@"")] + [TestCase(@"relative/path")] + public void IsPathValid_should_be_false_on_unix(string path) + { + PosixOnly(); path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 7dced0c0e..c8737d661 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -152,16 +152,20 @@ namespace NzbDrone.Common.Extensions return false; } - var directoryInfo = new DirectoryInfo(path); - - while (directoryInfo != null) + // Only check for leading or trailing spaces for path when running on Windows. + if (OsInfo.IsWindows) { - if (directoryInfo.Name.Trim() != directoryInfo.Name) - { - return false; - } + var directoryInfo = new DirectoryInfo(path); - directoryInfo = directoryInfo.Parent; + while (directoryInfo != null) + { + if (directoryInfo.Name.Trim() != directoryInfo.Name) + { + return false; + } + + directoryInfo = directoryInfo.Parent; + } } if (validationType == PathValidationType.AnyOs) From c9b5a1258ade0dbf45132aae5c1df8730c4c0fe3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 30 Jul 2024 02:25:01 +0300 Subject: [PATCH 420/762] New: Title filter for Series Index --- frontend/src/Store/Actions/seriesActions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index c18104065..84f79fd56 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -251,6 +251,11 @@ export const filterBuilderProps = [ type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.SERIES_TYPES }, + { + name: 'title', + label: () => translate('Title'), + type: filterBuilderTypes.STRING + }, { name: 'network', label: () => translate('Network'), From 4eab168267db716a9e897a992e3a7f6889571f9f Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 30 Jul 2024 21:25:48 -0700 Subject: [PATCH 421/762] New: Add metadata links to telegram messages Closes #5342 --------- Co-authored-by: Ivar Stangeby <istangeby@gmail.com> --- src/NzbDrone.Core/Localization/Core/en.json | 2 + .../Notifications/Telegram/Telegram.cs | 59 +++++++++++++++---- .../Notifications/Telegram/TelegramLink.cs | 14 +++++ .../Notifications/Telegram/TelegramProxy.cs | 22 +++++-- .../Telegram/TelegramSettings.cs | 37 +++++++++++- 5 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 01456d5b0..a36ccadec 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1429,6 +1429,8 @@ "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", + "NotificationsTelegramSettingsMetadataLinks": "Metadata Links", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications", "NotificationsTelegramSettingsSendSilently": "Send Silently", "NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound", "NotificationsTelegramSettingsTopicId": "Topic ID", diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 3a8513e27..4d23c3ac9 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Telegram { @@ -19,71 +20,77 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnGrab(GrabMessage grabMessage) { var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE; + var links = GetLinks(grabMessage.Series); - _proxy.SendNotification(title, grabMessage.Message, Settings); + _proxy.SendNotification(title, grabMessage.Message, links, Settings); } public override void OnDownload(DownloadMessage message) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + var links = GetLinks(message.Series); - _proxy.SendNotification(title, message.Message, Settings); + _proxy.SendNotification(title, message.Message, links, Settings); } public override void OnImportComplete(ImportCompleteMessage message) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + var links = GetLinks(message.Series); - _proxy.SendNotification(title, message.Message, Settings); + _proxy.SendNotification(title, message.Message, links, Settings); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE; + var links = GetLinks(deleteMessage.Series); - _proxy.SendNotification(title, deleteMessage.Message, Settings); + _proxy.SendNotification(title, deleteMessage.Message, links, Settings); } public override void OnSeriesAdd(SeriesAddMessage message) { var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE; + var links = GetLinks(message.Series); - _proxy.SendNotification(title, message.Message, Settings); + _proxy.SendNotification(title, message.Message, links, Settings); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE; + var links = GetLinks(deleteMessage.Series); - _proxy.SendNotification(title, deleteMessage.Message, Settings); + _proxy.SendNotification(title, deleteMessage.Message, links, Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; - _proxy.SendNotification(title, healthCheck.Message, Settings); + _proxy.SendNotification(title, healthCheck.Message, null, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; - _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings); + _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", null, Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; - _proxy.SendNotification(title, updateMessage.Message, Settings); + _proxy.SendNotification(title, updateMessage.Message, null, Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE; - _proxy.SendNotification(title, message.Message, Settings); + _proxy.SendNotification(title, message.Message, null, Settings); } public override ValidationResult Test() @@ -94,5 +101,37 @@ namespace NzbDrone.Core.Notifications.Telegram return new ValidationResult(failures); } + + private List<TelegramLink> GetLinks(Series series) + { + var links = new List<TelegramLink>(); + + foreach (var link in Settings.MetadataLinks) + { + var linkType = (MetadataLinkType)link; + + if (linkType == MetadataLinkType.Imdb && series.ImdbId.IsNotNullOrWhiteSpace()) + { + links.Add(new TelegramLink("IMDb", $"https://www.imdb.com/title/{series.ImdbId}")); + } + + if (linkType == MetadataLinkType.Tvdb && series.TvdbId > 0) + { + links.Add(new TelegramLink("TVDb", $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}")); + } + + if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0) + { + links.Add(new TelegramLink("TVMaze", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show")); + } + + if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0) + { + links.Add(new TelegramLink("Trakt", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_")); + } + } + + return links; + } } } diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs new file mode 100644 index 000000000..ac131b483 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Core.Notifications.Telegram +{ + public class TelegramLink + { + public string Label { get; set; } + public string Link { get; set; } + + public TelegramLink(string label, string link) + { + Label = label; + Link = link; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs index f1cc39f1a..48f70761e 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Text; using System.Web; using FluentValidation.Results; using NLog; @@ -13,7 +14,7 @@ namespace NzbDrone.Core.Notifications.Telegram { public interface ITelegramProxy { - void SendNotification(string title, string message, TelegramSettings settings); + void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings); ValidationFailure Test(TelegramSettings settings); } @@ -32,10 +33,16 @@ namespace NzbDrone.Core.Notifications.Telegram _logger = logger; } - public void SendNotification(string title, string message, TelegramSettings settings) + public void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings) { - // Format text to add the title before and bold using markdown - var text = $"<b>{HttpUtility.HtmlEncode(title)}</b>\n{HttpUtility.HtmlEncode(message)}"; + var text = new StringBuilder($"<b>{HttpUtility.HtmlEncode(title)}</b>\n"); + + text.AppendLine(HttpUtility.HtmlEncode(message)); + + foreach (var link in links) + { + text.AppendLine($"<a href=\"{link.Link}\">{HttpUtility.HtmlEncode(link.Label)}</a>"); + } var requestBuilder = new HttpRequestBuilder(URL).Resource("bot{token}/sendmessage").Post(); @@ -58,7 +65,12 @@ namespace NzbDrone.Core.Notifications.Telegram const string title = "Test Notification"; const string body = "This is a test message from Sonarr"; - SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings); + var links = new List<TelegramLink> + { + new TelegramLink("Sonarr.tv", "https://sonarr.tv") + }; + + SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, links, settings); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index f3e4d2499..6ec3d4d15 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -1,7 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; - namespace NzbDrone.Core.Notifications.Telegram { public class TelegramSettingsValidator : AbstractValidator<TelegramSettings> @@ -12,6 +14,16 @@ namespace NzbDrone.Core.Notifications.Telegram RuleFor(c => c.ChatId).NotEmpty(); RuleFor(c => c.TopicId).Must(topicId => !topicId.HasValue || topicId > 1) .WithMessage("Topic ID must be greater than 1 or empty"); + RuleFor(c => c.MetadataLinks).Custom((links, context) => + { + foreach (var link in links) + { + if (!Enum.IsDefined(typeof(MetadataLinkType), link)) + { + context.AddFailure("MetadataLinks", $"MetadataLink is not valid: {link}"); + } + } + }); } } @@ -19,6 +31,11 @@ namespace NzbDrone.Core.Notifications.Telegram { private static readonly TelegramSettingsValidator Validator = new (); + public TelegramSettings() + { + MetadataLinks = Enumerable.Empty<int>(); + } + [FieldDefinition(0, Label = "NotificationsTelegramSettingsBotToken", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://core.telegram.org/bots")] public string BotToken { get; set; } @@ -34,9 +51,27 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] public bool IncludeAppNameInTitle { get; set; } + [FieldDefinition(5, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")] + public IEnumerable<int> MetadataLinks { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } + + public enum MetadataLinkType + { + [FieldOption(Label = "IMDb")] + Imdb, + + [FieldOption(Label = "TVDb")] + Tvdb, + + [FieldOption(Label = "TVMaze")] + Tvmaze, + + [FieldOption(Label = "Trakt")] + Trakt, + } } From 11a9dcb3890eaf99602900f37e64007f2fbf9b8e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 30 Jul 2024 21:26:24 -0700 Subject: [PATCH 422/762] New: Return downloading magnets from Transmission Closes #7029 --- .../TransmissionTests/TransmissionFixture.cs | 7 +++++-- .../DownloadClientTests/VuzeTests/VuzeFixture.cs | 7 +++++-- .../Download/Clients/Transmission/TransmissionBase.cs | 10 ++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs index 9d3cb6ef3..3bbb417b9 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs @@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests } [Test] - public void magnet_download_should_not_return_the_item() + public void magnet_download_should_be_returned_as_queued() { PrepareClientToReturnMagnetItem(); - Subject.GetItems().Count().Should().Be(0); + + var item = Subject.GetItems().Single(); + + item.Status.Should().Be(DownloadItemStatus.Queued); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs index 38602f71d..b3cf1d0f0 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests public void magnet_download_should_not_return_the_item() { PrepareClientToReturnMagnetItem(); - Subject.GetItems().Count().Should().Be(0); + + var item = Subject.GetItems().Single(); + + item.Status.Should().Be(DownloadItemStatus.Queued); } [Test] diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 59113cbab..180c6094a 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -43,12 +43,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission foreach (var torrent in torrents) { - // If totalsize == 0 the torrent is a magnet downloading metadata - if (torrent.TotalSize == 0) - { - continue; - } - var outputPath = new OsPath(torrent.DownloadDir); if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) @@ -99,6 +93,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Status = DownloadItemStatus.Warning; item.Message = torrent.ErrorString; } + else if (torrent.TotalSize == 0) + { + item.Status = DownloadItemStatus.Queued; + } else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped || torrent.Status == TransmissionTorrentStatus.Seeding || torrent.Status == TransmissionTorrentStatus.SeedingWait)) From 78a0def46a4c8628d9bcf6af2701aa35b3f959b9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 31 Jul 2024 07:27:19 +0300 Subject: [PATCH 423/762] Fixed: Moving files for torrents when Remove Completed is disabled --- .../ImportApprovedEpisodesFixture.cs | 48 ++++++++++++++++++- .../Download/DownloadClientItem.cs | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index 179cf5b3f..86ee8e2dd 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -74,8 +74,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Returns(new List<EpisodeHistory>()); _downloadClientItem = Builder<DownloadClientItem>.CreateNew() - .With(d => d.OutputPath = new OsPath(outputPath)) - .Build(); + .With(d => d.OutputPath = new OsPath(outputPath)) + .With(d => d.DownloadClientInfo = new DownloadClientItemClientInfo()) + .Build(); } private void GivenNewDownload() @@ -201,6 +202,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenNewDownload(); _downloadClientItem.Title = "30.Rock.S01E01"; _downloadClientItem.CanMoveFiles = false; + _downloadClientItem.DownloadClientInfo = null; Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); @@ -208,6 +210,48 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); } + [Test] + public void should_copy_when_remove_completed_downloads_is_disabled_and_can_move_files() + { + GivenNewDownload(); + _downloadClientItem.Title = "30.Rock.S01E01"; + _downloadClientItem.CanMoveFiles = true; + _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = false; + + Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock<IUpgradeMediaFiles>() + .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); + } + + [Test] + public void should_copy_when_remove_completed_downloads_is_enabled_and_cannot_move_files() + { + GivenNewDownload(); + _downloadClientItem.Title = "30.Rock.S01E01"; + _downloadClientItem.CanMoveFiles = false; + _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = true; + + Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock<IUpgradeMediaFiles>() + .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); + } + + [Test] + public void should_move_when_remove_completed_downloads_is_enabled_and_can_move_files() + { + GivenNewDownload(); + _downloadClientItem.Title = "30.Rock.S01E01"; + _downloadClientItem.CanMoveFiles = true; + _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = true; + + Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock<IUpgradeMediaFiles>() + .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once()); + } + [Test] public void should_use_override_importmode() { diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 6dd1b6173..76ed0cb2c 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -37,6 +37,7 @@ namespace NzbDrone.Core.Download public string Type { get; set; } public int Id { get; set; } public string Name { get; set; } + public bool RemoveCompletedDownloads { get; set; } public bool HasPostImportCategory { get; set; } public static DownloadClientItemClientInfo FromDownloadClient<TSettings>( @@ -49,6 +50,7 @@ namespace NzbDrone.Core.Download Type = downloadClient.Name, Id = downloadClient.Definition.Id, Name = downloadClient.Definition.Name, + RemoveCompletedDownloads = downloadClient.Definition is DownloadClientDefinition { RemoveCompletedDownloads: true }, HasPostImportCategory = hasPostImportCategory }; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 739620039..df2198fb1 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -140,7 +140,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { default: case ImportMode.Auto: - copyOnly = downloadClientItem != null && !downloadClientItem.CanMoveFiles; + copyOnly = downloadClientItem is { CanMoveFiles: false } or { DownloadClientInfo.RemoveCompletedDownloads: false }; break; case ImportMode.Move: copyOnly = false; From 4c0de556724943f2f3cb55ed82b81eabc9c23351 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 30 Jul 2024 16:47:13 -0700 Subject: [PATCH 424/762] Fixed: Setting page size in Queue, History and Blocklist Closes #7035 --- frontend/src/Activity/Blocklist/Blocklist.tsx | 3 +++ frontend/src/Activity/History/History.tsx | 3 +++ frontend/src/Activity/Queue/Queue.tsx | 4 ++++ frontend/src/Activity/Queue/QueueOptions.tsx | 6 +++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx index 4205ae12e..4163bc9ca 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.tsx +++ b/frontend/src/Activity/Blocklist/Blocklist.tsx @@ -59,6 +59,7 @@ function Blocklist() { sortKey, sortDirection, page, + pageSize, totalPages, totalRecords, isRemoving, @@ -223,6 +224,7 @@ function Blocklist() { <PageToolbarSection alignContent={align.RIGHT}> <TableOptionsModalWrapper columns={columns} + pageSize={pageSize} onTableOptionChange={handleTableOptionChange} > <PageToolbarButton @@ -264,6 +266,7 @@ function Blocklist() { allSelected={allSelected} allUnselected={allUnselected} columns={columns} + pageSize={pageSize} sortKey={sortKey} sortDirection={sortDirection} onTableOptionChange={handleTableOptionChange} diff --git a/frontend/src/Activity/History/History.tsx b/frontend/src/Activity/History/History.tsx index 1020d90ea..9f00a1ab3 100644 --- a/frontend/src/Activity/History/History.tsx +++ b/frontend/src/Activity/History/History.tsx @@ -53,6 +53,7 @@ function History() { sortKey, sortDirection, page, + pageSize, totalPages, totalRecords, } = useSelector((state: AppState) => state.history); @@ -154,6 +155,7 @@ function History() { <PageToolbarSection alignContent={align.RIGHT}> <TableOptionsModalWrapper columns={columns} + pageSize={pageSize} onTableOptionChange={handleTableOptionChange} > <PageToolbarButton @@ -193,6 +195,7 @@ function History() { <div> <Table columns={columns} + pageSize={pageSize} sortKey={sortKey} sortDirection={sortDirection} onTableOptionChange={handleTableOptionChange} diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx index 6c5e0fb1b..bd063e69a 100644 --- a/frontend/src/Activity/Queue/Queue.tsx +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -73,6 +73,7 @@ function Queue() { sortKey, sortDirection, page, + pageSize, totalPages, totalRecords, isGrabbing, @@ -269,8 +270,10 @@ function Queue() { allSelected={allSelected} allUnselected={allUnselected} columns={columns} + pageSize={pageSize} sortKey={sortKey} sortDirection={sortDirection} + optionsComponent={QueueOptions} onTableOptionChange={handleTableOptionChange} onSelectAllChange={handleSelectAllChange} onSortPress={handleSortPress} @@ -344,6 +347,7 @@ function Queue() { <PageToolbarSection alignContent={align.RIGHT}> <TableOptionsModalWrapper columns={columns} + pageSize={pageSize} maxPageSize={200} optionsComponent={QueueOptions} onTableOptionChange={handleTableOptionChange} diff --git a/frontend/src/Activity/Queue/QueueOptions.tsx b/frontend/src/Activity/Queue/QueueOptions.tsx index 70bb0fdcf..615f6922a 100644 --- a/frontend/src/Activity/Queue/QueueOptions.tsx +++ b/frontend/src/Activity/Queue/QueueOptions.tsx @@ -5,7 +5,7 @@ import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import { inputTypes } from 'Helpers/Props'; -import { setQueueOption } from 'Store/Actions/queueActions'; +import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions'; import { CheckInputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; @@ -22,6 +22,10 @@ function QueueOptions() { [name]: value, }) ); + + if (name === 'includeUnknownSeriesItems') { + dispatch(gotoQueuePage({ page: 1 })); + } }, [dispatch] ); From 1299a97579bec52ee3d16ab8d05c9e22edd80330 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 30 Jul 2024 17:44:39 -0700 Subject: [PATCH 425/762] Update React Lint rules for TSX --- frontend/.eslintrc.js | 49 +++++++++++++++++-- frontend/src/Activity/Queue/QueueOptions.tsx | 24 +++++---- frontend/src/App/AppRoutes.tsx | 24 +++------ .../Components/Error/ErrorBoundaryError.tsx | 2 +- frontend/src/Episode/EpisodeNumber.tsx | 10 ++-- .../src/Helpers/Hooks/useModalOpenState.ts | 10 ++-- .../InteractiveImportModalContent.tsx | 6 +-- .../Language/SelectLanguageModal.tsx | 2 +- .../Series/SelectSeriesModalContent.tsx | 23 ++++----- .../SelectDownloadClientModal.tsx | 2 +- frontend/src/Parse/ParseToolbarButton.tsx | 6 +-- .../Index/Overview/SeriesIndexOverviews.tsx | 8 +-- .../Index/Posters/SeriesIndexPosters.tsx | 6 +-- .../Series/Index/Table/SeriesIndexTable.tsx | 8 +-- .../Index/Table/SeriesIndexTableOptions.tsx | 6 +-- .../CustomFormatSettingsPage.tsx | 6 +-- .../ManageDownloadClientsModalContent.tsx | 4 +- .../Manage/ManageImportListsModalContent.tsx | 2 +- .../Manage/ManageIndexersModalContent.tsx | 4 +- frontend/src/System/Status/About/About.tsx | 2 +- frontend/src/System/Updates/Updates.tsx | 12 ++--- 21 files changed, 113 insertions(+), 103 deletions(-) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index cc26a2633..ddc7300fd 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -359,11 +359,16 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - 'no-shadow': 'off', - // These should be enabled after cleaning things up - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'after-used', + argsIgnorePattern: '^_', + ignoreRestSiblings: true + } + ], '@typescript-eslint/explicit-function-return-type': 'off', - 'react/prop-types': 'off', + 'no-shadow': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -376,7 +381,41 @@ module.exports = { ['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$'] ] } - ] + ], + + // React Hooks + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + // React + 'react/function-component-definition': 'error', + 'react/hook-use-state': 'error', + 'react/jsx-boolean-value': ['error', 'always'], + 'react/jsx-curly-brace-presence': [ + 'error', + { props: 'never', children: 'never' } + ], + 'react/jsx-fragments': 'error', + 'react/jsx-handler-names': [ + 'error', + { + eventHandlerPrefix: 'on', + eventHandlerPropPrefix: 'on' + } + ], + 'react/jsx-no-bind': ['error', { ignoreRefs: true }], + 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], + 'react/jsx-pascal-case': ['error', { allowAllCaps: true }], + 'react/jsx-sort-props': [ + 'error', + { + callbacksLast: true, + noSortAlphabetically: true, + reservedFirst: true + } + ], + 'react/prop-types': 'off', + 'react/self-closing-comp': 'error' }) }, { diff --git a/frontend/src/Activity/Queue/QueueOptions.tsx b/frontend/src/Activity/Queue/QueueOptions.tsx index 615f6922a..17a6ac1fe 100644 --- a/frontend/src/Activity/Queue/QueueOptions.tsx +++ b/frontend/src/Activity/Queue/QueueOptions.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import FormGroup from 'Components/Form/FormGroup'; @@ -31,19 +31,17 @@ function QueueOptions() { ); return ( - <Fragment> - <FormGroup> - <FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel> + <FormGroup> + <FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel> - <FormInputGroup - type={inputTypes.CHECK} - name="includeUnknownSeriesItems" - value={includeUnknownSeriesItems} - helpText={translate('ShowUnknownSeriesItemsHelpText')} - onChange={handleOptionChange} - /> - </FormGroup> - </Fragment> + <FormInputGroup + type={inputTypes.CHECK} + name="includeUnknownSeriesItems" + value={includeUnknownSeriesItems} + helpText={translate('ShowUnknownSeriesItemsHelpText')} + onChange={handleOptionChange} + /> + </FormGroup> ); } diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index f66a4df40..e3bf426c9 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector'; +function RedirectWithUrlBase() { + return <Redirect to={getPathWithUrlBase('/')} />; +} + function AppRoutes() { return ( <Switch> @@ -51,9 +55,7 @@ function AppRoutes() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore addUrlBase={false} - render={() => { - return <Redirect to={getPathWithUrlBase('/')} />; - }} + render={RedirectWithUrlBase} /> )} @@ -61,21 +63,9 @@ function AppRoutes() { <Route path="/add/import" component={ImportSeries} /> - <Route - path="/serieseditor" - exact={true} - render={() => { - return <Redirect to={getPathWithUrlBase('/')} />; - }} - /> + <Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} /> - <Route - path="/seasonpass" - exact={true} - render={() => { - return <Redirect to={getPathWithUrlBase('/')} />; - }} - /> + <Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} /> <Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} /> diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 14bd8a87f..870b28058 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) { <div>{info.componentStack}</div> )} - {<div className={styles.version}>Version: {window.Sonarr.version}</div>} + <div className={styles.version}>Version: {window.Sonarr.version}</div> </details> </div> ); diff --git a/frontend/src/Episode/EpisodeNumber.tsx b/frontend/src/Episode/EpisodeNumber.tsx index 596174499..73afdc70a 100644 --- a/frontend/src/Episode/EpisodeNumber.tsx +++ b/frontend/src/Episode/EpisodeNumber.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import Icon from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; @@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) { <Popover anchor={ <span> - {showSeasonNumber && seasonNumber != null && ( - <Fragment>{seasonNumber}x</Fragment> - )} + {showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>} {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} @@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) { /> ) : ( <span> - {showSeasonNumber && seasonNumber != null && ( - <Fragment>{seasonNumber}x</Fragment> - )} + {showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>} {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts index f5b5a96f0..24cffb2f1 100644 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -3,15 +3,15 @@ import { useCallback, useState } from 'react'; export default function useModalOpenState( initialState: boolean ): [boolean, () => void, () => void] { - const [isOpen, setOpen] = useState(initialState); + const [isOpen, setIsOpen] = useState(initialState); const setModalOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); + setIsOpen(true); + }, [setIsOpen]); const setModalClosed = useCallback(() => { - setOpen(false); - }, [setOpen]); + setIsOpen(false); + }, [setIsOpen]); return [isOpen, setModalOpen, setModalClosed]; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index dbcd10613..990e0dfab 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -857,7 +857,7 @@ function InteractiveImportModalContent( <MenuContent> <SelectedMenuItem - name={'all'} + name="all" isSelected={!filterExistingFiles} onPress={onFilterExistingFilesChange} > @@ -865,7 +865,7 @@ function InteractiveImportModalContent( </SelectedMenuItem> <SelectedMenuItem - name={'new'} + name="new" isSelected={filterExistingFiles} onPress={onFilterExistingFilesChange} > @@ -945,7 +945,7 @@ function InteractiveImportModalContent( <SelectInput className={styles.bulkSelect} name="select" - value={'select'} + value="select" values={bulkSelectOptions} isDisabled={!selectedIds.length} onChange={onSelectModalSelect} diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModal.tsx b/frontend/src/InteractiveImport/Language/SelectLanguageModal.tsx index dbde852f2..c16cd8555 100644 --- a/frontend/src/InteractiveImport/Language/SelectLanguageModal.tsx +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModal.tsx @@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) { props; return ( - <Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}> + <Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}> <SelectLanguageModalContent languageIds={languageIds} modalTitle={modalTitle} diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx index 86e46a5bb..7ae5824bb 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx @@ -64,19 +64,20 @@ interface RowItemData { onSeriesSelect(seriesId: number): void; } -const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps<RowItemData>) { const { items, columns, onSeriesSelect } = data; + const series = index >= items.length ? null : items[index]; - if (index >= items.length) { + const handlePress = useCallback(() => { + if (series?.id) { + onSeriesSelect(series.id); + } + }, [series?.id, onSeriesSelect]); + + if (series == null) { return null; } - const series = items[index]; - return ( <VirtualTableRowButton style={{ @@ -84,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ justifyContent: 'space-between', ...style, }} - onPress={() => onSeriesSelect(series.id)} + onPress={handlePress} > <SelectSeriesRow key={series.id} @@ -98,7 +99,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ /> </VirtualTableRowButton> ); -}; +} function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { const { modalTitle, onSeriesSelect, onModalClose } = props; @@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { /> <Scroller + ref={scrollerRef} className={styles.scroller} autoFocus={false} - ref={scrollerRef} > <SelectSeriesModalTableHeader columns={columns} /> <List<RowItemData> diff --git a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx index 81bf86e59..7d623decd 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx @@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) { props; return ( - <Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}> + <Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}> <SelectDownloadClientModalContent protocol={protocol} modalTitle={modalTitle} diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx index 43b8b959f..6dae456d5 100644 --- a/frontend/src/Parse/ParseToolbarButton.tsx +++ b/frontend/src/Parse/ParseToolbarButton.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import { icons } from 'Helpers/Props'; import ParseModal from 'Parse/ParseModal'; @@ -16,7 +16,7 @@ function ParseToolbarButton() { }, [setIsParseModalOpen]); return ( - <Fragment> + <> <PageToolbarButton label={translate('TestParsing')} iconName={icons.PARSE} @@ -24,7 +24,7 @@ function ParseToolbarButton() { /> <ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} /> - </Fragment> + </> ); } diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx index a1d0b7076..f4e05014b 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx @@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps { isSmallScreen: boolean; } -const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps<RowItemData>) { const { items, ...otherData } = data; if (index >= items.length) { @@ -60,7 +56,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ <SeriesIndexOverview seriesId={series.id} {...otherData} /> </div> ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index 055685216..b4f859f84 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector( } ); -const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({ +function Cell({ columnIndex, rowIndex, style, data, -}) => { +}: GridChildComponentProps<CellItemData>) { const { layout, items, sortKey, isSelectMode } = data; const { columnCount, padding, posterWidth, posterHeight } = layout; const index = rowIndex * columnCount + columnIndex; @@ -92,7 +92,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({ /> </div> ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx index c1401f984..e6b4ca010 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx @@ -45,11 +45,7 @@ const columnsSelector = createSelector( (columns) => columns ); -const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps<RowItemData>) { const { items, sortKey, columns, isSelectMode } = data; if (index >= items.length) { @@ -75,7 +71,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({ /> </div> ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx b/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx index df648cadb..0a9cc1d17 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) { ); return ( - <Fragment> + <> <FormGroup> <FormLabel>{translate('ShowBanners')}</FormLabel> @@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) { onChange={onTableOptionChangeWrapper} /> </FormGroup> - </Fragment> + </> ); } diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx index fee176554..cc02a2a9a 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import PageContent from 'Components/Page/PageContent'; @@ -17,11 +17,11 @@ function CustomFormatSettingsPage() { // @ts-ignore showSave={false} additionalButtons={ - <Fragment> + <> <PageToolbarSeparator /> <ParseToolbarButton /> - </Fragment> + </> } /> diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 2722f02fa..a788d824e 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} - onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} + onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > <TableBody> @@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent( <ManageDownloadClientsEditModal isOpen={isEditModalOpen} + downloadClientIds={selectedIds} onModalClose={onEditModalClose} onSavePress={onSavePress} - downloadClientIds={selectedIds} /> <TagsModal diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 60619c662..5cbad933a 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -261,9 +261,9 @@ function ManageImportListsModalContent( <ManageImportListsEditModal isOpen={isEditModalOpen} + importListIds={selectedIds} onModalClose={onEditModalClose} onSavePress={onSavePress} - importListIds={selectedIds} /> <TagsModal diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index a6fa968c9..f03b39a21 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -226,9 +226,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { selectAll={true} allSelected={allSelected} allUnselected={allUnselected} - onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} + onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > <TableBody> @@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { <ManageIndexersEditModal isOpen={isEditModalOpen} + indexerIds={selectedIds} onModalClose={onEditModalClose} onSavePress={onSavePress} - indexerIds={selectedIds} /> <TagsModal diff --git a/frontend/src/System/Status/About/About.tsx b/frontend/src/System/Status/About/About.tsx index 1480318ee..a7ca64536 100644 --- a/frontend/src/System/Status/About/About.tsx +++ b/frontend/src/System/Status/About/About.tsx @@ -66,7 +66,7 @@ function About() { ) : null} {isDocker ? ( - <DescriptionListItem title={translate('Docker')} data={'Yes'} /> + <DescriptionListItem title={translate('Docker')} data="Yes" /> ) : null} <DescriptionListItem diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index c0a5fb882..af4235cec 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -1,10 +1,4 @@ -import React, { - Fragment, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; @@ -158,7 +152,7 @@ function Updates() { {translate('InstallLatest')} </SpinnerButton> ) : ( - <Fragment> + <> <Icon name={icons.WARNING} kind={kinds.WARNING} size={30} /> <div className={styles.message}> @@ -171,7 +165,7 @@ function Updates() { } /> </div> - </Fragment> + </> )} {isFetching ? ( From 217611d7165e2f24068697e4996f0dcfc54f786c Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 31 Jul 2024 07:28:01 +0300 Subject: [PATCH 426/762] Fixed: Persist Indexer Flags when manual importing from queue --- .../EpisodeImport/Manual/ManualImportService.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index c413172aa..c82da9c78 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -215,6 +215,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { DownloadClientItem downloadClientItem = null; Series series = null; + TrackedDownload trackedDownload = null; var directoryInfo = new DirectoryInfo(baseFolder); @@ -236,7 +237,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (downloadId.IsNotNullOrWhiteSpace()) { - var trackedDownload = _trackedDownloadService.Find(downloadId); + trackedDownload = _trackedDownloadService.Find(downloadId); downloadClientItem = trackedDownload.DownloadItem; if (series == null) @@ -272,6 +273,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var seriesFiles = _diskScanService.FilterPaths(rootFolder, _diskScanService.GetVideoFiles(baseFolder).ToList()); var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder), filterExistingFiles); + foreach (var decision in decisions) + { + decision.LocalEpisode.IndexerFlags = trackedDownload?.RemoteEpisode?.Release?.IndexerFlags ?? 0; + } + return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList(); } @@ -331,7 +337,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (importDecisions.Any()) { - return MapItem(importDecisions.First(), rootFolder, downloadId, null); + var importDecision = importDecisions.First(); + importDecision.LocalEpisode.IndexerFlags = trackedDownload?.RemoteEpisode?.Release?.IndexerFlags ?? 0; + + return MapItem(importDecision, rootFolder, downloadId, null); } } catch (Exception ex) From 4ff83f9efc8abb0890722c3f462fb884937c5e34 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:15:36 +0300 Subject: [PATCH 427/762] Fixed: Persist Indexer Flags for automatic imports Revert "Fixed: Persist Indexer Flags when manual importing from queue" This reverts commit 217611d7165e2f24068697e4996f0dcfc54f786c. --- .../EpisodeImport/ImportDecisionMaker.cs | 14 ++++++++++++++ .../EpisodeImport/Manual/ManualImportService.cs | 13 ++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index c3f07a0a5..d192c72f2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -29,6 +30,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IAggregationService _aggregationService; private readonly IDiskProvider _diskProvider; private readonly IDetectSample _detectSample; + private readonly ITrackedDownloadService _trackedDownloadService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly Logger _logger; @@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport IAggregationService aggregationService, IDiskProvider diskProvider, IDetectSample detectSample, + ITrackedDownloadService trackedDownloadService, ICustomFormatCalculationService formatCalculator, Logger logger) { @@ -45,6 +48,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _aggregationService = aggregationService; _diskProvider = diskProvider; _detectSample = detectSample; + _trackedDownloadService = trackedDownloadService; _formatCalculator = formatCalculator; _logger = logger; } @@ -145,6 +149,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } else { + if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true) + { + var trackedDownload = _trackedDownloadService.Find(downloadClientItem.DownloadId); + + if (trackedDownload?.RemoteEpisode?.Release?.IndexerFlags != null) + { + localEpisode.IndexerFlags = trackedDownload.RemoteEpisode.Release.IndexerFlags; + } + } + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index c82da9c78..c413172aa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -215,7 +215,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { DownloadClientItem downloadClientItem = null; Series series = null; - TrackedDownload trackedDownload = null; var directoryInfo = new DirectoryInfo(baseFolder); @@ -237,7 +236,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (downloadId.IsNotNullOrWhiteSpace()) { - trackedDownload = _trackedDownloadService.Find(downloadId); + var trackedDownload = _trackedDownloadService.Find(downloadId); downloadClientItem = trackedDownload.DownloadItem; if (series == null) @@ -273,11 +272,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var seriesFiles = _diskScanService.FilterPaths(rootFolder, _diskScanService.GetVideoFiles(baseFolder).ToList()); var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder), filterExistingFiles); - foreach (var decision in decisions) - { - decision.LocalEpisode.IndexerFlags = trackedDownload?.RemoteEpisode?.Release?.IndexerFlags ?? 0; - } - return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList(); } @@ -337,10 +331,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (importDecisions.Any()) { - var importDecision = importDecisions.First(); - importDecision.LocalEpisode.IndexerFlags = trackedDownload?.RemoteEpisode?.Release?.IndexerFlags ?? 0; - - return MapItem(importDecision, rootFolder, downloadId, null); + return MapItem(importDecisions.First(), rootFolder, downloadId, null); } } catch (Exception ex) From 4c0b8961741a7dd0cf2aba81cdbcb74c1208a1ff Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 31 Jul 2024 17:34:31 -0700 Subject: [PATCH 428/762] Improve messaging for for Send Notifications setting in Emby / Jellyfin Closes #7042 --- src/NzbDrone.Core/Localization/Core/en.json | 4 ++-- .../MediaBrowser/MediaBrowserProxy.cs | 18 +++++++++++++++++- .../MediaBrowser/MediaBrowserSettings.cs | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a36ccadec..8d7d90087 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1325,8 +1325,8 @@ "NotificationsEmailSettingsUseEncryption": "Use Encryption", "NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption", "NotificationsEmbySettingsSendNotifications": "Send Notifications", - "NotificationsEmbySettingsSendNotificationsHelpText": "Have MediaBrowser send notifications to configured providers", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete?", + "NotificationsEmbySettingsSendNotificationsHelpText": "Have Emby send notifications to configured providers. Not supported on Jellyfin.", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete", "NotificationsGotifySettingIncludeSeriesPoster": "Include Series Poster", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Include series poster in message", "NotificationsGotifySettingsAppToken": "App Token", diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 3d074034b..db17d965e 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -24,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby var path = "/Notifications/Admin"; var request = BuildRequest(path, settings); request.Headers.ContentType = "application/json"; + request.LogHttpError = false; request.SetContent(new { @@ -32,7 +34,21 @@ namespace NzbDrone.Core.Notifications.Emby ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png" }.ToJson()); - ProcessRequest(request, settings); + try + { + ProcessRequest(request, settings); + } + catch (HttpException e) + { + if (e.Response.StatusCode == HttpStatusCode.NotFound) + { + _logger.Warn("Unable to send notification to Emby. If you're using Jellyfin disable 'Send Notifications'"); + } + else + { + throw; + } + } } public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series) diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index 369888ceb..bd15c140e 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby public MediaBrowserSettings() { Port = 8096; + UpdateLibrary = true; } [FieldDefinition(0, Label = "Host")] From 9b528eb82914a05cfc3b67d4d6146ce51e86f68d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 31 Jul 2024 22:16:24 -0700 Subject: [PATCH 429/762] New: Default file log level changed to debug --- src/NzbDrone.Core/Configuration/ConfigFileProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index be132cc6c..2f5a4d05e 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -220,7 +220,7 @@ namespace NzbDrone.Core.Configuration public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant(); - public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant(); + public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant(); public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false); public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false); From 291d792810d071f28c389d100b9642854d7cd70e Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:17:10 +0300 Subject: [PATCH 430/762] Fixed: Moving files on import for usenet clients Closes #7043 --- .../ImportApprovedEpisodesFixture.cs | 44 ------------------- .../Download/Clients/Aria2/Aria2.cs | 9 ++-- .../Clients/Blackhole/TorrentBlackhole.cs | 13 +++--- .../Download/Clients/Deluge/Deluge.cs | 1 + .../DownloadStation/TorrentDownloadStation.cs | 8 ++-- .../Download/Clients/Flood/Flood.cs | 4 +- .../FreeboxDownload/TorrentFreeboxDownload.cs | 2 +- .../Download/Clients/Hadouken/Hadouken.cs | 5 ++- .../Clients/QBittorrent/QBittorrent.cs | 7 ++- .../Clients/Transmission/TransmissionBase.cs | 2 +- .../Download/Clients/rTorrent/RTorrent.cs | 2 +- .../Download/Clients/uTorrent/UTorrent.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- 13 files changed, 35 insertions(+), 65 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index 86ee8e2dd..e5f9ea2a6 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -75,7 +75,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _downloadClientItem = Builder<DownloadClientItem>.CreateNew() .With(d => d.OutputPath = new OsPath(outputPath)) - .With(d => d.DownloadClientInfo = new DownloadClientItemClientInfo()) .Build(); } @@ -202,7 +201,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenNewDownload(); _downloadClientItem.Title = "30.Rock.S01E01"; _downloadClientItem.CanMoveFiles = false; - _downloadClientItem.DownloadClientInfo = null; Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); @@ -210,48 +208,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); } - [Test] - public void should_copy_when_remove_completed_downloads_is_disabled_and_can_move_files() - { - GivenNewDownload(); - _downloadClientItem.Title = "30.Rock.S01E01"; - _downloadClientItem.CanMoveFiles = true; - _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = false; - - Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); - - Mocker.GetMock<IUpgradeMediaFiles>() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); - } - - [Test] - public void should_copy_when_remove_completed_downloads_is_enabled_and_cannot_move_files() - { - GivenNewDownload(); - _downloadClientItem.Title = "30.Rock.S01E01"; - _downloadClientItem.CanMoveFiles = false; - _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = true; - - Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); - - Mocker.GetMock<IUpgradeMediaFiles>() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); - } - - [Test] - public void should_move_when_remove_completed_downloads_is_enabled_and_can_move_files() - { - GivenNewDownload(); - _downloadClientItem.Title = "30.Rock.S01E01"; - _downloadClientItem.CanMoveFiles = true; - _downloadClientItem.DownloadClientInfo.RemoveCompletedDownloads = true; - - Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem); - - Mocker.GetMock<IUpgradeMediaFiles>() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once()); - } - [Test] public void should_use_override_importmode() { diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 970d09d35..6b91b2a63 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -129,10 +129,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2 var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(GetOutputPath(torrent))); - yield return new DownloadClientItem + var queueItem = new DownloadClientItem { - CanMoveFiles = false, - CanBeRemoved = torrent.Status == "complete", Category = null, DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.InfoHash?.ToUpper(), @@ -146,7 +144,12 @@ namespace NzbDrone.Core.Download.Clients.Aria2 Status = status, Title = title, TotalSize = totalLength, + CanMoveFiles = false }; + + queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == "complete"; + + yield return queueItem; } } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index 8364a1fb2..eca8dc2f2 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod)) { - yield return new DownloadClientItem + var queueItem = new DownloadClientItem { DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, @@ -101,11 +101,14 @@ namespace NzbDrone.Core.Download.Clients.Blackhole OutputPath = item.OutputPath, - Status = item.Status, - - CanMoveFiles = !Settings.ReadOnly, - CanBeRemoved = !Settings.ReadOnly + Status = item.Status }; + + queueItem.CanMoveFiles = queueItem.CanBeRemoved = + queueItem.DownloadClientInfo.RemoveCompletedDownloads && + !Settings.ReadOnly; + + yield return queueItem; } } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index f2f8ff7d6..44d78e311 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -190,6 +190,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge // Here we detect if Deluge is managing the torrent and whether the seed criteria has been met. // This allows Sonarr to delete the torrent as appropriate. item.CanMoveFiles = item.CanBeRemoved = + item.DownloadClientInfo.RemoveCompletedDownloads && torrent.IsAutoManaged && torrent.StopAtRatio && torrent.Ratio >= torrent.StopRatio && diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 42efcaedf..59288d3b5 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } } - var item = new DownloadClientItem() + var item = new DownloadClientItem { Category = Settings.TvCategory, DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), @@ -99,11 +99,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation RemainingTime = GetRemainingTime(torrent), SeedRatio = GetSeedRatio(torrent), Status = GetStatus(torrent), - Message = GetMessage(torrent), - CanMoveFiles = IsFinished(torrent), - CanBeRemoved = IsFinished(torrent) + Message = GetMessage(torrent) }; + item.CanMoveFiles = item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && IsFinished(torrent); + if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed) { item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber); diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index 60b153441..0fa02446b 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -153,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Flood item.Status = DownloadItemStatus.Downloading; } - if (item.Status == DownloadItemStatus.Completed) + if (item.DownloadClientInfo.RemoveCompletedDownloads && item.Status == DownloadItemStatus.Completed) { // Grab cached seedConfig var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(item.DownloadId); @@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Flood // Check if seed ratio reached item.CanMoveFiles = item.CanBeRemoved = true; } - else if (properties.DateFinished != null && properties.DateFinished > 0) + else if (properties.DateFinished is > 0) { // Check if seed time reached if ((DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds((long)properties.DateFinished)) >= seedConfig.SeedTime) diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs index 88248e4b5..365cab274 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload break; } - item.CanBeRemoved = item.CanMoveFiles = torrent.Status == FreeboxDownloadTaskStatus.Done; + item.CanBeRemoved = item.CanMoveFiles = item.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == FreeboxDownloadTaskStatus.Done; queueItems.Add(item); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 59f28e34d..59ee6c923 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -92,7 +92,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken item.Status = DownloadItemStatus.Downloading; } - item.CanMoveFiles = item.CanBeRemoved = torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused; + item.CanMoveFiles = item.CanBeRemoved = + item.DownloadClientInfo.RemoveCompletedDownloads && + torrent.IsFinished && + torrent.State == HadoukenTorrentState.Paused; items.Add(item); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 56eb302fe..8bec2d2fd 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -225,7 +225,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent foreach (var torrent in torrents) { - var item = new DownloadClientItem() + var item = new DownloadClientItem { DownloadId = torrent.Hash.ToUpper(), Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label, @@ -239,7 +239,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Avoid removing torrents that haven't reached the global max ratio. // Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api). - item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config); + item.CanMoveFiles = item.CanBeRemoved = + item.DownloadClientInfo.RemoveCompletedDownloads && + torrent.State is "pausedUP" or "stoppedUP" && + HasReachedSeedLimit(torrent, config); switch (torrent.State) { diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 180c6094a..87810d016 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Status = DownloadItemStatus.Downloading; } - item.CanBeRemoved = HasReachedSeedLimit(torrent, item.SeedRatio, configFunc); + item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && HasReachedSeedLimit(torrent, item.SeedRatio, configFunc); item.CanMoveFiles = item.CanBeRemoved && torrent.Status == TransmissionTorrentStatus.Stopped; items.Add(item); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index fd91a3833..88fb3ef86 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -185,7 +185,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent // Grab cached seedConfig var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash); - if (torrent.IsFinished && seedConfig != null) + if (item.DownloadClientInfo.RemoveCompletedDownloads && torrent.IsFinished && seedConfig != null) { var canRemove = false; diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index 5b93a1d5d..f99229b0e 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -167,6 +167,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent // 'Started' without 'Queued' is when the torrent is 'forced seeding' item.CanMoveFiles = item.CanBeRemoved = + item.DownloadClientInfo.RemoveCompletedDownloads && !torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) && !torrent.Status.HasFlag(UTorrentTorrentStatus.Started); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index df2198fb1..b4f62addc 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -140,7 +140,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { default: case ImportMode.Auto: - copyOnly = downloadClientItem is { CanMoveFiles: false } or { DownloadClientInfo.RemoveCompletedDownloads: false }; + copyOnly = downloadClientItem is { CanMoveFiles: false }; break; case ImportMode.Move: copyOnly = false; From b1527f9abbe4903c89058d5e351b914bae6a7162 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 1 Aug 2024 05:16:07 +0000 Subject: [PATCH 431/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ar/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ar.json | 4 +++- src/NzbDrone.Core/Localization/Core/es.json | 4 +++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index 5e2d116df..9131d047e 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -1,4 +1,6 @@ { "AddAutoTag": "أضف كلمات دلالية تلقائيا", - "AddCondition": "إضافة شرط" + "AddCondition": "إضافة شرط", + "AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.", + "ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه." } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index de29b9547..e93da6188 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2098,5 +2098,7 @@ "SeasonsMonitoredNone": "Ninguna", "SeasonsMonitoredStatus": "Temporadas monitorizadas", "NoBlocklistItems": "Ningún elemento en la lista de bloqueo", - "SeasonsMonitoredPartial": "Parcial" + "SeasonsMonitoredPartial": "Parcial", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones", + "NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index f603040a0..4720a80a6 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2098,5 +2098,7 @@ "SeasonsMonitoredPartial": "Parcial", "SeasonsMonitoredNone": "Nenhuma", "SeasonsMonitoredStatus": "Temporadas monitoradas", - "NoBlocklistItems": "Sem itens na lista de bloqueio" + "NoBlocklistItems": "Sem itens na lista de bloqueio", + "NotificationsTelegramSettingsMetadataLinks": "Links de Metadados", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações" } From 4c86d673ea8d7ebd77b5ea401d7b538f482a3818 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 9 Aug 2024 11:25:17 +0000 Subject: [PATCH 432/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ano10 <arnaudthommeray+github@ik.me> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 4 +- src/NzbDrone.Core/Localization/Core/fr.json | 40 ++++++++++++++----- .../Localization/Core/pt_BR.json | 4 +- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index e93da6188..687303ac5 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1401,7 +1401,7 @@ "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", + "NotificationsEmbySettingsSendNotificationsHelpText": "Hace que Emby envíe notificaciones a los proveedores configurados. No soportado en Jellyfin.", "NotificationsGotifySettingsAppToken": "Token de app", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje", "NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo", @@ -1919,7 +1919,7 @@ "NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord", "NotificationsEmailSettingsCcAddress": "Dirección(es) CC", "NotificationsEmbySettingsSendNotifications": "Enviar notificaciones", - "NotificationsEmbySettingsUpdateLibraryHelpText": "¿Actualiza biblioteca en importar, renombrar o borrar?", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Actualiza biblioteca al 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", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 9c4ae95c9..8394ff2d8 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -299,7 +299,7 @@ "Sunday": "Dimanche", "TorrentDelay": "Retard du torrent", "DownloadClients": "Clients de télécharg.", - "CustomFormats": "Formats perso.", + "CustomFormats": "Formats personnalisés", "NoIndexersFound": "Aucun indexeur n'a été trouvé", "Profiles": "Profils", "Dash": "Tiret", @@ -431,7 +431,7 @@ "Replace": "Remplacer", "ResetAPIKeyMessageText": "Êtes-vous sûr de vouloir réinitialiser votre clé API ?", "StopSelecting": "Effacer la sélection", - "WhatsNew": "Quoi de neuf ?", + "WhatsNew": "Quoi de neuf ?", "EditDownloadClientImplementation": "Modifier le client de téléchargement - {implementationName}", "External": "Externe", "Monday": "Lundi", @@ -452,7 +452,7 @@ "RootFolderSelectFreeSpace": "{freeSpace} Libre", "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": "Actualiser", + "UpdateAll": "Tout mettre à jour", "EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif", "UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}", "RssSync": "Synchronisation RSS", @@ -1130,7 +1130,7 @@ "NotificationsTagsSeriesHelpText": "N'envoyer des notifications que pour les séries avec au moins une balise correspondante", "OnApplicationUpdate": "Lors de la mise à jour de l'application", "OnEpisodeFileDelete": "Lors de la suppression du fichier de l'épisode", - "OnHealthIssue": "Sur la question de la santé", + "OnHealthIssue": "Lors de problème de santé", "OnManualInteractionRequired": "Sur l'interaction manuelle requise", "OnRename": "Au renommage", "PreferredSize": "Taille préférée", @@ -1176,7 +1176,7 @@ "Total": "Total", "Upcoming": "À venir", "UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour", - "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible", + "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}", "UpdateFiltered": "Mise à jour filtrée", "IconForSpecialsHelpText": "Afficher l'icône pour les épisodes spéciaux (saison 0)", "Ignored": "Ignoré", @@ -1276,7 +1276,7 @@ "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é", + "Custom": "Personnaliser", "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": "Formats et paramètres personnalisés", "CustomFormatsSettings": "Paramètre des formats personnalisés", @@ -1724,8 +1724,8 @@ "NotificationsGotifySettingsPriorityHelpText": "Priorité de la notification", "NotificationsGotifySettingsAppTokenHelpText": "Le jeton d'application généré par Gotify", "NotificationsGotifySettingsAppToken": "Jeton d'app", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors d'import, de renommage ou de suppression ?", - "NotificationsEmbySettingsSendNotificationsHelpText": "Faire en sorte que MediaBrowser envoie des notifications aux fournisseurs configurés", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors de l'importation, du changement de nom ou de la suppression", + "NotificationsEmbySettingsSendNotificationsHelpText": "Demandez à Emby d'envoyer des notifications aux fournisseurs configurés. Non pris en charge sur Jellyfin.", "NotificationsEmbySettingsSendNotifications": "Envoyer des notifications", "NotificationsEmailSettingsServerHelpText": "Nom d'hôte ou adresse IP du serveur de courriel", "NotificationsEmailSettingsServer": "Serveur", @@ -2078,5 +2078,27 @@ "TomorrowAt": "Demain à {time}", "TodayAt": "Aujourd'hui à {time}", "ShowTagsHelpText": "Afficher les labels sous l'affiche", - "ShowTags": "Afficher les labels" + "ShowTags": "Afficher les labels", + "CountVotes": "{votes} votes", + "NoBlocklistItems": "Aucun élément de la liste de blocage", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Ajouter un lien vers les métadonnées de la série lors de l'envoi de notifications", + "RatingVotes": "Votes de notation", + "OnFileImport": "Lors de l'importation du fichier", + "OnImportComplete": "Une fois l'importation terminée", + "NotificationsPlexSettingsServer": "Serveur", + "NotificationsPlexSettingsServerHelpText": "Sélectionnez le serveur à partir du compte plex.tv après l'authentification", + "OnFileUpgrade": "Lors de la mise à jour du fichier", + "NextAiringDate": "Prochaine diffusion : {date}", + "CustomColonReplacement": "Remplacement personnalisé des deux‐points", + "CustomColonReplacementFormatHelpText": "Caractères à utiliser en remplacement des deux-points", + "CustomColonReplacementFormatHint": "Caractère valide du système de fichiers tel que deux-points (lettre)", + "Install": "Installer", + "InstallMajorVersionUpdate": "Installer la mise à jour", + "InstallMajorVersionUpdateMessage": "Cette mise à jour installera une nouvelle version majeure et pourrait ne pas être compatible avec votre système. Êtes-vous sûr de vouloir installer cette mise à jour ?", + "InstallMajorVersionUpdateMessageLink": "Veuillez consulter [{domain}]({url}) pour plus d'informations.", + "SeasonsMonitoredAll": "Toutes", + "SeasonsMonitoredPartial": "Partielle", + "SeasonsMonitoredNone": "Aucune", + "SeasonsMonitoredStatus": "Saisons surveillées", + "NotificationsTelegramSettingsMetadataLinks": "Liens de métadonnées" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 4720a80a6..55c078246 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1733,7 +1733,7 @@ "NotificationsEmailSettingsServer": "Servidor", "NotificationsEmailSettingsServerHelpText": "Nome do host ou IP do servidor de e-mail", "NotificationsEmbySettingsSendNotifications": "Enviar Notificações", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Atualizar Biblioteca ao Importar, Renomear ou Excluir?", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Atualizar Biblioteca ao Importar, Renomear ou Excluir", "NotificationsGotifySettingIncludeSeriesPoster": "Incluir Pôster da Série", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluir Pôster da Série na Mensagem", "NotificationsGotifySettingsAppToken": "Token do Aplicativo", @@ -1841,7 +1841,7 @@ "NotificationsValidationUnableToSendTestMessage": "Não foi possível enviar a mensagem de teste: {exceptionMessage}", "NotificationsValidationUnableToSendTestMessageApiResponse": "Não foi possível enviar mensagem de teste. Resposta da API: {error}", "NotificationsAppriseSettingsStatelessUrlsHelpText": "Uma ou mais URLs separadas por vírgulas identificando para onde a notificação deve ser enviada. Deixe em branco se o armazenamento persistente for usado.", - "NotificationsEmbySettingsSendNotificationsHelpText": "Faça com que o MediaBrowser envie notificações para provedores configurados", + "NotificationsEmbySettingsSendNotificationsHelpText": "Faça com que Emby envie notificações para provedores configurados. Não compatível com Jellyfin.", "NotificationsJoinSettingsDeviceIdsHelpText": "Obsoleto, use nomes de dispositivos. Lista separada por vírgulas de IDs de dispositivos para os quais você gostaria de enviar notificações. Se não for definido, todos os dispositivos receberão notificações.", "NotificationsTwitterSettingsConnectToTwitter": "Conecte-se ao Twitter / X", "NotificationsTelegramSettingsBotToken": "Token do Bot", From ae7f73208a5db30c98cb7b0e7841ab0a87793d12 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 31 Jul 2024 18:19:44 -0700 Subject: [PATCH 433/762] Upgrade nlog to 5.3.2 --- .../Extensions/SentryLoggerExtensions.cs | 31 +++++++++---------- .../Instrumentation/NzbDroneFileTarget.cs | 8 +++-- .../Instrumentation/NzbDroneLogger.cs | 13 ++++++++ src/NzbDrone.Common/Sonarr.Common.csproj | 6 ++-- .../Download/CompletedDownloadService.cs | 5 ++- .../Instrumentation/DatabaseTarget.cs | 11 ++++--- .../Instrumentation/ReconfigureLogging.cs | 2 +- .../MediaInfo/MediaInfoFormatter.cs | 9 +++--- src/NzbDrone.Core/Parser/ParsingService.cs | 17 +++++----- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- src/NzbDrone.Host/Sonarr.Host.csproj | 2 +- src/NzbDrone.Host/Startup.cs | 4 +-- .../Sonarr.Test.Common.csproj | 2 +- src/NzbDrone.Update/Sonarr.Update.csproj | 2 +- src/NzbDrone.Windows/Sonarr.Windows.csproj | 2 +- src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 2 +- src/Sonarr.Http/Sonarr.Http.csproj | 2 +- 17 files changed, 67 insertions(+), 53 deletions(-) diff --git a/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs index 2e17aad44..68201d62c 100644 --- a/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs +++ b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs @@ -1,6 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using NLog; -using NLog.Fluent; namespace NzbDrone.Common.Instrumentation.Extensions { @@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions { public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry"); - public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint) + public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint) { return logBuilder.Property("Sentry", fingerprint); } - public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint) + public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint) { return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint); } - public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint) + public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint) { return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint); } - public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint) + public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint) { return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint); } - public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint) + public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint) { return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint); } - private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint) + private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint) { - SentryLogger.Log(level) - .CopyLogEvent(logBuilder.LogEventInfo) + SentryLogger.ForLogEvent(level) + .CopyLogEvent(logBuilder.LogEvent) .SentryFingerprint(fingerprint) - .Write(); + .Log(); - return logBuilder.Property("Sentry", null); + return logBuilder.Property<string>("Sentry", null); } - private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent) + private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent) { - return logBuilder.LoggerName(logEvent.LoggerName) - .TimeStamp(logEvent.TimeStamp) + return logBuilder.TimeStamp(logEvent.TimeStamp) .Message(logEvent.Message, logEvent.Parameters) - .Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value)) + .Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value))) .Exception(logEvent.Exception); } } diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs index 62e41b0e0..84658cf74 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs @@ -1,13 +1,15 @@ -using NLog; +using System.Text; +using NLog; using NLog.Targets; namespace NzbDrone.Common.Instrumentation { public class NzbDroneFileTarget : FileTarget { - protected override string GetFormattedMessage(LogEventInfo logEvent) + protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) { - return CleanseLogMessage.Cleanse(Layout.Render(logEvent)); + var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent)); + target.Append(result); } } } diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 750ad659c..3f5145c17 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation var appFolderInfo = new AppFolderInfo(startupContext); + RegisterGlobalFilters(); + if (Debugger.IsAttached) { RegisterDebugger(); @@ -196,6 +198,17 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration.LoggingRules.Insert(0, rule); } + private static void RegisterGlobalFilters() + { + LogManager.Setup().LoadConfiguration(c => + { + c.ForLogger("System.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); + c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); + }); + } + public static Logger GetLogger(Type obj) { return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", "")); diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index afd914994..6a55e3b5e 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -8,9 +8,9 @@ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="4.7.14" /> - <PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" /> - <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> + <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="System.Text.Json" Version="6.0.9" /> diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index d22589c37..a71c6b9cf 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; -using NLog.Fluent; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; @@ -247,14 +246,14 @@ namespace NzbDrone.Core.Download } else { - _logger.Debug() + _logger.ForDebugEvent() .Message("No Episodes were just imported, but all episodes were previously imported, possible issue with download history.") .Property("SeriesId", trackedDownload.RemoteEpisode.Series.Id) .Property("DownloadId", trackedDownload.DownloadItem.DownloadId) .Property("Title", trackedDownload.DownloadItem.Title) .Property("Path", trackedDownload.ImportItem.OutputPath.ToString()) .WriteSentryWarn("DownloadHistoryIncomplete") - .Write(); + .Log(); } var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id)); diff --git a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs index 64a23782e..181e60278 100644 --- a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs +++ b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs @@ -33,22 +33,25 @@ namespace NzbDrone.Core.Instrumentation LogManager.Configuration.AddTarget("DbLogger", target); LogManager.Configuration.LoggingRules.Add(Rule); - LogManager.ConfigurationReloaded += OnLogManagerOnConfigurationReloaded; + LogManager.ConfigurationChanged += OnLogManagerOnConfigurationReloaded; LogManager.ReconfigExistingLoggers(); } public void UnRegister() { - LogManager.ConfigurationReloaded -= OnLogManagerOnConfigurationReloaded; + LogManager.ConfigurationChanged -= OnLogManagerOnConfigurationReloaded; LogManager.Configuration.RemoveTarget("DbLogger"); LogManager.Configuration.LoggingRules.Remove(Rule); LogManager.ReconfigExistingLoggers(); Dispose(); } - private void OnLogManagerOnConfigurationReloaded(object sender, LoggingConfigurationReloadedEventArgs args) + private void OnLogManagerOnConfigurationReloaded(object sender, LoggingConfigurationChangedEventArgs args) { - Register(); + if (args.ActivatedConfiguration != null) + { + Register(); + } } public LoggingRule Rule { get; set; } diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index d4b44eaa6..08cb4ca60 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Instrumentation syslogTarget.MessageSend.Protocol = ProtocolType.Udp; syslogTarget.MessageSend.Udp.Port = syslogPort; syslogTarget.MessageSend.Udp.Server = syslogServer; - syslogTarget.MessageSend.Udp.ReconnectInterval = 500; + syslogTarget.MessageSend.Retry.ConstantBackoff.BaseDelay = 500; syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424; syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName; diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 154d25b7b..be89a95e4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using NLog; -using NLog.Fluent; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; @@ -153,10 +152,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return "WMA"; } - Logger.Debug() + Logger.ForDebugEvent() .Message("Unknown audio format: '{0}' in '{1}'. Streams: {2}", audioFormat, sceneName, mediaInfo.RawStreamData) .WriteSentryWarn("UnknownAudioFormatFFProbe", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID) - .Write(); + .Log(); return mediaInfo.AudioFormat; } @@ -268,10 +267,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return ""; } - Logger.Debug() + Logger.ForDebugEvent() .Message("Unknown video format: '{0}' in '{1}'. Streams: {2}", videoFormat, sceneName, mediaInfo.RawStreamData) .WriteSentryWarn("UnknownVideoFormatFFProbe", mediaInfo.ContainerFormat, videoFormat, videoCodecID) - .Write(); + .Log(); return result; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index a07cfaebc..99045915e 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using NLog; -using NLog.Fluent; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.DataAugmentation.Scene; @@ -386,24 +385,24 @@ namespace NzbDrone.Core.Parser if (tvdbId > 0 && tvdbId == searchCriteria.Series.TvdbId) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by TVDB ID {0}, an alias may be needed for: {1}", tvdbId, parsedEpisodeInfo.SeriesTitle) .Property("TvdbId", tvdbId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("TvdbIdMatch", tvdbId.ToString(), parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); } if (tvRageId > 0 && tvRageId == searchCriteria.Series.TvRageId && tvdbId <= 0) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by TVRage ID {0}, an alias may be needed for: {1}", tvRageId, parsedEpisodeInfo.SeriesTitle) .Property("TvRageId", tvRageId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("TvRageIdMatch", tvRageId.ToString(), parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); } @@ -435,12 +434,12 @@ namespace NzbDrone.Core.Parser if (series != null) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by TVDB ID {0}, an alias may be needed for: {1}", tvdbId, parsedEpisodeInfo.SeriesTitle) .Property("TvdbId", tvdbId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("TvdbIdMatch", tvdbId.ToString(), parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); matchType = SeriesMatchType.Id; } @@ -452,12 +451,12 @@ namespace NzbDrone.Core.Parser if (series != null) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by TVRage ID {0}, an alias may be needed for: {1}", tvRageId, parsedEpisodeInfo.SeriesTitle) .Property("TvRageId", tvRageId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("TvRageIdMatch", tvRageId.ToString(), parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); matchType = SeriesMatchType.Id; } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index b5de0b05f..dd25a7184 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -21,7 +21,7 @@ <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.3.2" /> <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.9" /> diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index 7eb3a4058..c2b82e95c 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.21" /> - <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index b010e8e2c..f26a8fcf9 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -49,8 +49,8 @@ namespace NzbDrone.Host services.AddLogging(b => { b.ClearProviders(); - b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); - b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning); + b.SetMinimumLevel(LogLevel.Trace); + b.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); b.AddFilter("Sonarr.Http.Authentication", LogLevel.Information); b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error); b.AddNLog(); diff --git a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj index bc5f721f6..4155803b2 100644 --- a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj @@ -6,7 +6,7 @@ <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="NLog" Version="5.3.2" /> <PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="RestSharp" Version="106.15.0" /> </ItemGroup> diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index 624151093..4d07b3d1a 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -6,7 +6,7 @@ <ItemGroup> <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.3.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Windows/Sonarr.Windows.csproj b/src/NzbDrone.Windows/Sonarr.Windows.csproj index 96ab6ac02..e534a2b3f 100644 --- a/src/NzbDrone.Windows/Sonarr.Windows.csproj +++ b/src/NzbDrone.Windows/Sonarr.Windows.csproj @@ -4,7 +4,7 @@ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.3.2" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" /> </ItemGroup> <ItemGroup> diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 659783773..ac6900d33 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -5,7 +5,7 @@ <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Ical.Net" Version="4.2.0" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.3.2" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" /> </ItemGroup> <ItemGroup> diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index 6c0adc7d8..290357a96 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -5,7 +5,7 @@ <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="ImpromptuInterface" Version="7.0.1" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.3.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" /> From 0d914f4c53876540ed2df83ad3d71615c013856f Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 31 Jul 2024 21:42:07 -0700 Subject: [PATCH 434/762] New: Add Compact Log Event Format option for console logging Closes #7045 --- .../Instrumentation/NzbDroneLogger.cs | 20 ++++++++++++++++++- src/NzbDrone.Common/Options/LogOptions.cs | 1 + src/NzbDrone.Common/Sonarr.Common.csproj | 1 + .../Configuration/ConfigFileProvider.cs | 7 +++++++ .../Instrumentation/ReconfigureLogging.cs | 18 +++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 3f5145c17..b72abb617 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using NLog; using NLog.Config; +using NLog.Layouts.ClefJsonLayout; using NLog.Targets; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -13,6 +14,8 @@ namespace NzbDrone.Common.Instrumentation public static class NzbDroneLogger { private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}"; + public const string ConsoleLogLayout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}"; + public static CompactJsonLayout ClefLogLayout = new CompactJsonLayout(); private static bool _isConfigured; @@ -124,7 +127,16 @@ namespace NzbDrone.Common.Instrumentation var coloredConsoleTarget = new ColoredConsoleTarget(); coloredConsoleTarget.Name = "consoleLogger"; - coloredConsoleTarget.Layout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}"; + + var logFormat = Enum.TryParse<ConsoleLogFormat>(Environment.GetEnvironmentVariable("SONARR__LOG__CONSOLEFORMAT"), out var formatEnumValue) + ? formatEnumValue + : ConsoleLogFormat.Standard; + + coloredConsoleTarget.Layout = logFormat switch + { + ConsoleLogFormat.Clef => ClefLogLayout, + _ => ConsoleLogLayout + }; var loggingRule = new LoggingRule("*", level, coloredConsoleTarget); @@ -219,4 +231,10 @@ namespace NzbDrone.Common.Instrumentation return GetLogger(obj.GetType()); } } + + public enum ConsoleLogFormat + { + Standard, + Clef + } } diff --git a/src/NzbDrone.Common/Options/LogOptions.cs b/src/NzbDrone.Common/Options/LogOptions.cs index 1529bb1d0..4110f6654 100644 --- a/src/NzbDrone.Common/Options/LogOptions.cs +++ b/src/NzbDrone.Common/Options/LogOptions.cs @@ -7,6 +7,7 @@ public class LogOptions public int? Rotate { get; set; } public bool? Sql { get; set; } public string ConsoleLevel { get; set; } + public string ConsoleFormat { get; set; } public bool? AnalyticsEnabled { get; set; } public string SyslogServer { get; set; } public int? SyslogPort { get; set; } diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 6a55e3b5e..6778f7fb5 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -9,6 +9,7 @@ <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> <PackageReference Include="Sentry" Version="4.0.2" /> diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 2f5a4d05e..2a52a54db 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Options; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration.Events; @@ -38,6 +39,7 @@ namespace NzbDrone.Core.Configuration bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } + ConsoleLogFormat ConsoleLogFormat { get; } bool LogSql { get; } int LogRotate { get; } bool FilterSentryEvents { get; } @@ -223,6 +225,11 @@ namespace NzbDrone.Core.Configuration public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant(); public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false); + public ConsoleLogFormat ConsoleLogFormat => + Enum.TryParse<ConsoleLogFormat>(_logOptions.ConsoleFormat, out var enumValue) + ? enumValue + : GetValueEnum("ConsoleLogFormat", ConsoleLogFormat.Standard, false); + public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false); public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false); diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index 08cb4ca60..5785662d9 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NLog.Config; +using NLog.Targets; using NLog.Targets.Syslog; using NLog.Targets.Syslog.Settings; using NzbDrone.Common.EnvironmentInfo; @@ -51,6 +52,7 @@ namespace NzbDrone.Core.Instrumentation var rules = LogManager.Configuration.LoggingRules; // Console + ReconfigureConsole(); SetMinimumLogLevel(rules, "consoleLogger", minimumConsoleLogLevel); // Log Files @@ -109,6 +111,22 @@ namespace NzbDrone.Core.Instrumentation } } + private void ReconfigureConsole() + { + var consoleTarget = LogManager.Configuration.AllTargets.OfType<ColoredConsoleTarget>().FirstOrDefault(); + + if (consoleTarget != null) + { + var format = _configFileProvider.ConsoleLogFormat; + + consoleTarget.Layout = format switch + { + ConsoleLogFormat.Clef => NzbDroneLogger.ClefLogLayout, + _ => NzbDroneLogger.ConsoleLogLayout + }; + } + } + private void SetSyslogParameters(string syslogServer, int syslogPort, LogLevel minimumLogLevel) { var syslogTarget = new SyslogTarget(); From 813965e6a20edef2772d68eaa7646af33028425a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 31 Jul 2024 22:03:23 -0700 Subject: [PATCH 435/762] New: Configurable log file size limit --- ...onFixture.cs => NumberExtensionFixture.cs} | 4 ++-- ...Int64Extensions.cs => NumberExtensions.cs} | 24 +++++++++++++++++-- .../Instrumentation/NzbDroneLogger.cs | 2 +- src/NzbDrone.Common/Options/LogOptions.cs | 1 + .../MultiLanguageFixture.cs | 1 + .../OriginalLanguageFixture.cs | 1 + .../SingleLanguageFixture.cs | 1 + .../Migration/203_release_typeFixture.cs | 1 + .../AcceptableSizeSpecificationFixture.cs | 1 + .../DownloadedEpisodesImportServiceFixture.cs | 1 + .../ImportApprovedEpisodesFixture.cs | 1 + .../FreeSpaceSpecificationFixture.cs | 3 ++- .../Configuration/ConfigFileProvider.cs | 2 ++ .../Specifications/SizeSpecification.cs | 1 + .../DownloadDecisionComparer.cs | 1 + .../Specifications/NotSampleSpecification.cs | 3 ++- .../Consumers/Xbmc/XbmcNfoDetector.cs | 1 + src/NzbDrone.Core/Fluent.cs | 20 ---------------- .../Instrumentation/ReconfigureLogging.cs | 5 ++-- .../Specifications/FreeSpaceSpecification.cs | 3 ++- 20 files changed, 47 insertions(+), 30 deletions(-) rename src/NzbDrone.Common.Test/ExtensionTests/{Int64ExtensionFixture.cs => NumberExtensionFixture.cs} (91%) rename src/NzbDrone.Common/Extensions/{Int64Extensions.cs => NumberExtensions.cs} (53%) diff --git a/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs similarity index 91% rename from src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs rename to src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs index 76e28f3f7..c51ab7ad4 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs @@ -1,11 +1,11 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Test.ExtensionTests { [TestFixture] - public class Int64ExtensionFixture + public class NumberExtensionFixture { [TestCase(0, "0 B")] [TestCase(1000, "1,000.0 B")] diff --git a/src/NzbDrone.Common/Extensions/Int64Extensions.cs b/src/NzbDrone.Common/Extensions/NumberExtensions.cs similarity index 53% rename from src/NzbDrone.Common/Extensions/Int64Extensions.cs rename to src/NzbDrone.Common/Extensions/NumberExtensions.cs index bfca7f66c..15037b20b 100644 --- a/src/NzbDrone.Common/Extensions/Int64Extensions.cs +++ b/src/NzbDrone.Common/Extensions/NumberExtensions.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Globalization; namespace NzbDrone.Common.Extensions { - public static class Int64Extensions + public static class NumberExtensions { private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; @@ -26,5 +26,25 @@ namespace NzbDrone.Common.Extensions return string.Format(CultureInfo.InvariantCulture, "{0:n1} {1}", adjustedSize, SizeSuffixes[mag]); } + + public static long Megabytes(this int megabytes) + { + return Convert.ToInt64(megabytes * 1024L * 1024L); + } + + public static long Gigabytes(this int gigabytes) + { + return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); + } + + public static long Megabytes(this double megabytes) + { + return Convert.ToInt64(megabytes * 1024L * 1024L); + } + + public static long Gigabytes(this double gigabytes) + { + return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); + } } } diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index b72abb617..964431881 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -162,7 +162,7 @@ namespace NzbDrone.Common.Instrumentation fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttempts = 10; - fileTarget.ArchiveAboveSize = 1024000; + fileTarget.ArchiveAboveSize = 1.Megabytes(); fileTarget.MaxArchiveFiles = maxArchiveFiles; fileTarget.EnableFileDelete = true; fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling; diff --git a/src/NzbDrone.Common/Options/LogOptions.cs b/src/NzbDrone.Common/Options/LogOptions.cs index 4110f6654..6460eeaa6 100644 --- a/src/NzbDrone.Common/Options/LogOptions.cs +++ b/src/NzbDrone.Common/Options/LogOptions.cs @@ -5,6 +5,7 @@ public class LogOptions public string Level { get; set; } public bool? FilterSentryEvents { get; set; } public int? Rotate { get; set; } + public int? SizeLimit { get; set; } public bool? Sql { get; set; } public string ConsoleLevel { get; set; } public string ConsoleFormat { get; set; } diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs index 5527610ed..afc775398 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs index 33f2b65fc..14777c634 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/OriginalLanguageFixture.cs @@ -3,6 +3,7 @@ using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs index 6718057a2..faee8afe7 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs index 9010ba27d..1259e12f9 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Migration; using NzbDrone.Core.MediaFiles.MediaInfo; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index b11de1c93..34ee3dad5 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -4,6 +4,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 892c3b65e..0dce58278 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -7,6 +7,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index e5f9ea2a6..0561e765a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -6,6 +6,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.History; diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs index 7a607f97c..edbae4d63 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs @@ -1,10 +1,11 @@ -using System.IO; +using System.IO; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 2a52a54db..bed0399f9 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Configuration ConsoleLogFormat ConsoleLogFormat { get; } bool LogSql { get; } int LogRotate { get; } + int LogSizeLimit { get; } bool FilterSentryEvents { get; } string Branch { get; } string ApiKey { get; } @@ -241,6 +242,7 @@ namespace NzbDrone.Core.Configuration public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false); public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false); public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false); + public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10); public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false); public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", ""); public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", ""); diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index 3a1e404ec..039d446e7 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -1,4 +1,5 @@ using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index ce29d1919..b938f8729 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index 091efb948..7340df169 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs index 4132602ec..07cd62089 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 1aab8bbb9..cc0dcfc4a 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -20,26 +20,6 @@ namespace NzbDrone.Core return actual; } - public static long Megabytes(this int megabytes) - { - return Convert.ToInt64(megabytes * 1024L * 1024L); - } - - public static long Gigabytes(this int gigabytes) - { - return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); - } - - public static long Megabytes(this double megabytes) - { - return Convert.ToInt64(megabytes * 1024L * 1024L); - } - - public static long Gigabytes(this double gigabytes) - { - return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); - } - public static long Round(this long number, long level) { return Convert.ToInt64(Math.Floor((decimal)number / level) * level); diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index 5785662d9..1310255fd 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Instrumentation SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off); SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off); SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off); - SetLogRotation(); + ReconfigureFile(); // Log Sql SqlBuilderExtensions.LogSql = _configFileProvider.LogSql; @@ -93,11 +93,12 @@ namespace NzbDrone.Core.Instrumentation } } - private void SetLogRotation() + private void ReconfigureFile() { foreach (var target in LogManager.Configuration.AllTargets.OfType<NzbDroneFileTarget>()) { target.MaxArchiveFiles = _configFileProvider.LogRotate; + target.ArchiveAboveSize = _configFileProvider.LogSizeLimit.Megabytes(); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index ac39cfc07..29f74ca6b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.IO; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; From e6f82270a96810564390e7abee7175e05203e393 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:45:00 +0300 Subject: [PATCH 436/762] Parse TVDB ID for releases from HDBits ignore-downstream --- src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index 2a0d0f352..8e902e1c6 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -61,6 +61,7 @@ namespace NzbDrone.Core.Indexers.HDBits Seeders = result.Seeders, Peers = result.Leechers + result.Seeders, PublishDate = result.Added.ToUniversalTime(), + TvdbId = result.TvdbInfo?.Id ?? 0, IndexerFlags = GetIndexerFlags(result) }); } From 8b253c36ead2d41fb9abb7fd89fc3a5df1c4e709 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:45:15 +0300 Subject: [PATCH 437/762] Validation for bulk series editor --- .../Series/SeriesEditorController.cs | 28 +++++++++++++------ .../Series/SeriesEditorValidator.cs | 22 +++++++++++++++ 2 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 src/Sonarr.Api.V3/Series/SeriesEditorValidator.cs diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorController.cs b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs index cea1220e1..9b2a01dea 100644 --- a/src/Sonarr.Api.V3/Series/SeriesEditorController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Commands; @@ -14,11 +15,13 @@ namespace Sonarr.Api.V3.Series { private readonly ISeriesService _seriesService; private readonly IManageCommandQueue _commandQueueManager; + private readonly SeriesEditorValidator _seriesEditorValidator; - public SeriesEditorController(ISeriesService seriesService, IManageCommandQueue commandQueueManager) + public SeriesEditorController(ISeriesService seriesService, IManageCommandQueue commandQueueManager, SeriesEditorValidator seriesEditorValidator) { _seriesService = seriesService; _commandQueueManager = commandQueueManager; + _seriesEditorValidator = seriesEditorValidator; } [HttpPut] @@ -58,10 +61,10 @@ namespace Sonarr.Api.V3.Series { series.RootFolderPath = resource.RootFolderPath; seriesToMove.Add(new BulkMoveSeries - { - SeriesId = series.Id, - SourcePath = series.Path - }); + { + SeriesId = series.Id, + SourcePath = series.Path + }); } if (resource.Tags != null) @@ -82,15 +85,22 @@ namespace Sonarr.Api.V3.Series break; } } + + var validationResult = _seriesEditorValidator.Validate(series); + + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } } if (resource.MoveFiles && seriesToMove.Any()) { _commandQueueManager.Push(new BulkMoveSeriesCommand - { - DestinationRootFolder = resource.RootFolderPath, - Series = seriesToMove - }); + { + DestinationRootFolder = resource.RootFolderPath, + Series = seriesToMove + }); } return Accepted(_seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles).ToResource()); diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorValidator.cs b/src/Sonarr.Api.V3/Series/SeriesEditorValidator.cs new file mode 100644 index 000000000..71f403552 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesEditorValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesEditorValidator : AbstractValidator<NzbDrone.Core.Tv.Series> + { + public SeriesEditorValidator(RootFolderExistsValidator rootFolderExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator) + { + RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop) + .IsValidPath() + .SetValidator(rootFolderExistsValidator) + .When(s => s.RootFolderPath.IsNotNullOrWhiteSpace()); + + RuleFor(c => c.QualityProfileId).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(qualityProfileExistsValidator); + } + } +} From 0877a6718d3df8e217a72cc5b113b8398e495eb1 Mon Sep 17 00:00:00 2001 From: RaZaSB <razasb@proton.me> Date: Sun, 11 Aug 2024 16:46:02 +0100 Subject: [PATCH 438/762] New: Remove all single quote characters from searches --- .../IndexerSearchTests/SearchDefinitionFixture.cs | 4 ++++ .../IndexerSearch/Definitions/SearchCriteriaBase.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs index 5885de408..4f582c029 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs @@ -15,6 +15,10 @@ namespace NzbDrone.Core.Test.IndexerSearchTests [TestCase("Franklin & Bash", "Franklin+and+Bash")] [TestCase("Chicago P.D.", "Chicago+PD")] [TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", "Kourtney+And+Khloe+Take+The+Hamptons")] + [TestCase("Betty White`s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")] + [TestCase("Betty White\u00b4s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")] + [TestCase("Betty White‘s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")] + [TestCase("Betty White’s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")] public void should_replace_some_special_characters(string input, string expected) { Subject.SceneTitles = new List<string> { input }; diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 7aeddba02..5c5c1a3fb 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions { public abstract class SearchCriteriaBase { - private static readonly Regex SpecialCharacter = new Regex(@"[`'.]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SpecialCharacter = new Regex(@"['.\u0060\u00B4\u2018\u2019]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); From 363f8fc347315f629aae096e7300539bee4928cb Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:46:46 +0300 Subject: [PATCH 439/762] New: Match search releases using IMDb ID if available --- .../DownloadDecisionMakerFixture.cs | 18 +++--- .../DownloadClientFixtureBase.cs | 3 +- .../TrackedDownloadServiceFixture.cs | 14 ++--- .../MatchesFolderSpecificationFixture.cs | 4 +- .../ParsingServiceTests/GetEpisodesFixture.cs | 26 ++++---- .../ParsingServiceTests/MapFixture.cs | 46 ++++++++++---- .../DecisionEngine/DownloadDecisionMaker.cs | 4 +- .../TrackedDownloadService.cs | 6 +- src/NzbDrone.Core/History/HistoryService.cs | 1 + .../BroadcastheNet/BroadcastheNetParser.cs | 5 ++ .../Indexers/FileList/FileListParser.cs | 12 +++- .../Indexers/Newznab/NewznabRssParser.cs | 13 ++++ .../Indexers/Torznab/TorznabRssParser.cs | 13 ++++ .../MatchesFolderSpecification.cs | 4 +- src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 1 + src/NzbDrone.Core/Parser/ParsingService.cs | 60 +++++++++++++++---- src/NzbDrone.Core/Tv/SeriesRepository.cs | 6 ++ src/NzbDrone.Core/Tv/SeriesService.cs | 6 ++ src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 3 + src/Sonarr.Api.V3/Parse/ParseController.cs | 2 +- 20 files changed, 181 insertions(+), 66 deletions(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index dcef16654..f195a2c99 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; Mocker.GetMock<IParsingService>() - .Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())) + .Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>())) .Returns(_remoteEpisode); } @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.GetRssDecision(_reports).ToList(); - Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); + Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); _pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); _pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); @@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var results = Subject.GetRssDecision(_reports).ToList(); - Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); + Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); _pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); _pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.GetSearchDecision(_reports, new SingleEpisodeSearchCriteria()).ToList(); - Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); + Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never()); _pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); _pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never()); @@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenSpecifications(_pass1); - Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())) + Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>())) .Throws<TestException>(); _reports = new List<ReleaseInfo> @@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.GetRssDecision(_reports); - Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count)); + Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count)); ExceptionVerification.ExpectedErrors(3); } @@ -263,8 +263,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }).ToList(); Mocker.GetMock<IParsingService>() - .Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())) - .Returns<ParsedEpisodeInfo, int, int, SearchCriteriaBase>((p, tvdbid, tvrageid, c) => + .Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>())) + .Returns<ParsedEpisodeInfo, int, int, string, SearchCriteriaBase>((p, _, _, _, _) => new RemoteEpisode { DownloadAllowed = true, @@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenSpecifications(_pass1); - Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())) + Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>())) .Throws<TestException>(); _reports = new List<ReleaseInfo> diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 804099e4a..f920257ea 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -10,7 +10,6 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; -using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests .Returns(30); Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), (SearchCriteriaBase)null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(() => CreateRemoteEpisode()); Mocker.GetMock<IHttpClient>() diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index c82ff848d..3e541c404 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads .Returns(remoteEpisode); Mocker.GetMock<IParsingService>() - .Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(remoteEpisode.ParsedEpisodeInfo); var client = new DownloadClientDefinition() @@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads }; Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(remoteEpisode); Mocker.GetMock<IHistoryService>() @@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads Subject.GetTrackedDownloads().Should().HaveCount(1); Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(default(RemoteEpisode)); Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes)); @@ -228,7 +228,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads }; Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(default(RemoteEpisode)); Mocker.GetMock<IHistoryService>() @@ -258,7 +258,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads Subject.GetTrackedDownloads().Should().HaveCount(1); Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(default(RemoteEpisode)); Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes)); @@ -287,7 +287,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads }; Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(default(RemoteEpisode)); Mocker.GetMock<IHistoryService>() @@ -317,7 +317,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads Subject.GetTrackedDownloads().Should().HaveCount(1); Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null)) + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) .Returns(default(RemoteEpisode)); Subject.Handle(new SeriesDeletedEvent(new List<Series> { remoteEpisode.Series }, true, true)); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs index 29b87cc42..b4d4de34b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs @@ -233,11 +233,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers); Mocker.GetMock<IParsingService>() - .Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null)) + .Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null, null)) .Returns(actualInfo); Mocker.GetMock<IParsingService>() - .Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null)) + .Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null, null)) .Returns(actualInfo); Subject.IsSatisfiedBy(localEpisode, null).Accepted.Should().BeTrue(); diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs index 77c726fa8..9a5610838 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests GivenDailySeries(); GivenDailyParseResult(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once()); @@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests GivenDailySeries(); GivenDailyParseResult(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never()); @@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests GivenDailySeries(); _parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5).ToString(Episode.AIR_DATE_FORMAT); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once()); @@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests GivenDailyParseResult(); _parsedEpisodeInfo.DailyPart = 1; - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 1), Times.Once()); @@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>())) .Returns(new List<Episode>()); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never()); @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenSceneNumberingSeries(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once()); @@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenSceneNumberingSeries(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never()); @@ -177,7 +177,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests GivenSceneNumberingSeries(); _episodes.First().SceneEpisodeNumber = 10; - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once()); @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests [Test] public void should_find_episode() { - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once()); @@ -195,7 +195,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests [Test] public void should_match_episode_with_search_criteria() { - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never()); @@ -206,7 +206,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { _episodes.First().EpisodeNumber = 10; - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once()); @@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .With(e => e.EpisodeNumber = 1) .Build()); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once()); @@ -555,7 +555,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle)) .Returns((Episode)null); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<IEpisodeService>() .Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs index 0aac2ecbc..5b26d0461 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenMatchBySeriesTitle(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once()); @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenMatchByTvdbId(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once()); @@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenMatchByTvRageId(); - Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId); + Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId, null); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once()); @@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>())) .Returns(new SceneMapping { TvdbId = 10 }); - var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never()); @@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenMatchBySeriesTitle(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never()); @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once()); @@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Setup(s => s.FindByTitle(_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, _parsedEpisodeInfo.SeriesTitleInfo.Year)) .Returns(_series); - Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once()); @@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once()); @@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, 0, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 0, 10, null, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once()); @@ -202,12 +202,34 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never()); } + [Test] + public void should_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails() + { + GivenParseResultSeriesDoesntMatchSearchCriteria(); + + Subject.Map(_parsedEpisodeInfo, 0, 0, "tt12345", _singleEpisodeSearchCriteria); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Once()); + } + + [Test] + public void should_not_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails_and_tvdb_id_is_specified() + { + GivenParseResultSeriesDoesntMatchSearchCriteria(); + + Subject.Map(_parsedEpisodeInfo, 10, 10, "tt12345", _singleEpisodeSearchCriteria); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Never()); + } + [Test] public void should_use_tvdbid_matching_when_alias_is_found() { @@ -215,7 +237,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Setup(s => s.FindTvdbId(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>())) .Returns(_series.TvdbId); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never()); @@ -226,7 +248,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests { GivenParseResultSeriesDoesntMatchSearchCriteria(); - Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria); Mocker.GetMock<ISeriesService>() .Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never()); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index c38cbb822..e5ee8ac03 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -80,7 +80,7 @@ namespace NzbDrone.Core.DecisionEngine if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) { - var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, searchCriteria); + var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria); if (specialEpisodeInfo != null) { @@ -90,7 +90,7 @@ namespace NzbDrone.Core.DecisionEngine if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace()) { - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria); + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria); remoteEpisode.Release = report; if (remoteEpisode.Series == null) diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 2f789f23b..608221e70 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (parsedEpisodeInfo != null) { - trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null); _aggregationService.Augment(trackedDownload.RemoteEpisode); } @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads // Try parsing the original source title and if that fails, try parsing it as a special // TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ?? - _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0); + _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0, null); if (parsedEpisodeInfo != null) { @@ -234,7 +234,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads { var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); - trackedDownload.RemoteEpisode = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, 0); + trackedDownload.RemoteEpisode = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, 0, null); _aggregationService.Augment(trackedDownload.RemoteEpisode); } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index d7e76d94f..6a8ff6520 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -165,6 +165,7 @@ namespace NzbDrone.Core.History history.Data.Add("Guid", message.Episode.Release.Guid); history.Data.Add("TvdbId", message.Episode.Release.TvdbId.ToString()); history.Data.Add("TvRageId", message.Episode.Release.TvRageId.ToString()); + history.Data.Add("ImdbId", message.Episode.Release.ImdbId); history.Data.Add("Protocol", ((int)message.Episode.Release.DownloadProtocol).ToString()); history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString()); history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString()); diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs index 15341e067..ec652879e 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs @@ -95,6 +95,11 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet torrentInfo.TvRageId = torrent.TvrageID.Value; } + if (torrent.ImdbID.IsNotNullOrWhiteSpace() && int.TryParse(torrent.ImdbID, out var imdbId) && imdbId > 0) + { + torrentInfo.ImdbId = $"tt{imdbId:D7}"; + } + results.Add(torrentInfo); } diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index 59e6237d0..8b26a1d60 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList { var id = result.Id; - torrentInfos.Add(new TorrentInfo + var torrentInfo = new TorrentInfo { Guid = $"FileList-{id}", Title = result.Name, @@ -48,9 +48,15 @@ namespace NzbDrone.Core.Indexers.FileList Seeders = result.Seeders, Peers = result.Leechers + result.Seeders, PublishDate = result.UploadDate.ToUniversalTime(), - ImdbId = result.ImdbId, IndexerFlags = GetIndexerFlags(result) - }); + }; + + if (result.ImdbId is { Length: > 2 } && int.TryParse(result.ImdbId.TrimStart('t'), out var imdbId) && imdbId > 0) + { + torrentInfo.ImdbId = $"tt{imdbId:D7}"; + } + + torrentInfos.Add(torrentInfo); } return torrentInfos.ToArray(); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index 2444d901c..408d22b36 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -90,6 +90,7 @@ namespace NzbDrone.Core.Indexers.Newznab releaseInfo.TvdbId = GetTvdbId(item); releaseInfo.TvRageId = GetTvRageId(item); + releaseInfo.ImdbId = GetImdbId(item); return releaseInfo; } @@ -182,6 +183,18 @@ namespace NzbDrone.Core.Indexers.Newznab return 0; } + protected virtual string GetImdbId(XElement item) + { + var imdbIdString = TryGetNewznabAttribute(item, "imdb"); + + if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out var imdbId) && imdbId > 0) + { + return $"tt{imdbId:D7}"; + } + + return null; + } + protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "") { var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 3bcd87d76..2186c4f77 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -83,6 +83,7 @@ namespace NzbDrone.Core.Indexers.Torznab { torrentInfo.TvdbId = GetTvdbId(item); torrentInfo.TvRageId = GetTvRageId(item); + releaseInfo.ImdbId = GetImdbId(item); torrentInfo.IndexerFlags = GetFlags(item); } @@ -177,6 +178,18 @@ namespace NzbDrone.Core.Indexers.Torznab return 0; } + protected virtual string GetImdbId(XElement item) + { + var imdbIdString = TryGetTorznabAttribute(item, "imdb"); + + if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out var imdbId) && imdbId > 0) + { + return $"tt{imdbId:D7}"; + } + + return null; + } + protected override string GetInfoHash(XElement item) { return TryGetTorznabAttribute(item, "infohash"); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index f4b346d5c..230190cd5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -33,12 +33,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (fileInfo != null && fileInfo.IsPossibleSceneSeasonSpecial) { - fileInfo = _parsingService.ParseSpecialEpisodeTitle(fileInfo, fileInfo.ReleaseTitle, localEpisode.Series.TvdbId, 0); + fileInfo = _parsingService.ParseSpecialEpisodeTitle(fileInfo, fileInfo.ReleaseTitle, localEpisode.Series.TvdbId, 0, null); } if (folderInfo != null && folderInfo.IsPossibleSceneSeasonSpecial) { - folderInfo = _parsingService.ParseSpecialEpisodeTitle(folderInfo, folderInfo.ReleaseTitle, localEpisode.Series.TvdbId, 0); + folderInfo = _parsingService.ParseSpecialEpisodeTitle(folderInfo, folderInfo.ReleaseTitle, localEpisode.Series.TvdbId, 0, null); } if (folderInfo == null) diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index ade3e3467..1266ac10c 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -103,6 +103,7 @@ namespace NzbDrone.Core.Parser.Model stringBuilder.AppendLine("DownloadProtocol: " + DownloadProtocol ?? "Empty"); stringBuilder.AppendLine("TvdbId: " + TvdbId ?? "Empty"); stringBuilder.AppendLine("TvRageId: " + TvRageId ?? "Empty"); + stringBuilder.AppendLine("ImdbId: " + ImdbId ?? "Empty"); stringBuilder.AppendLine("PublishDate: " + PublishDate ?? "Empty"); return stringBuilder.ToString(); default: diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 99045915e..487824092 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -13,11 +14,11 @@ namespace NzbDrone.Core.Parser public interface IParsingService { Series GetSeries(string title); - RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); + RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Series series); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds); List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); - ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); + ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, Series series); } @@ -115,14 +116,14 @@ namespace NzbDrone.Core.Parser return foundSeries; } - public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null) { - return Map(parsedEpisodeInfo, tvdbId, tvRageId, null, searchCriteria); + return Map(parsedEpisodeInfo, tvdbId, tvRageId, imdbId, null, searchCriteria); } public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Series series) { - return Map(parsedEpisodeInfo, 0, 0, series, null); + return Map(parsedEpisodeInfo, 0, 0, null, series, null); } public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds) @@ -135,7 +136,7 @@ namespace NzbDrone.Core.Parser }; } - private RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, Series series, SearchCriteriaBase searchCriteria) + private RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, Series series, SearchCriteriaBase searchCriteria) { var sceneMapping = _sceneMappingService.FindSceneMapping(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle, parsedEpisodeInfo.SeasonNumber); @@ -171,7 +172,7 @@ namespace NzbDrone.Core.Parser if (series == null) { - var seriesMatch = FindSeries(parsedEpisodeInfo, tvdbId, tvRageId, sceneMapping, searchCriteria); + var seriesMatch = FindSeries(parsedEpisodeInfo, tvdbId, tvRageId, imdbId, sceneMapping, searchCriteria); if (seriesMatch != null) { @@ -210,7 +211,7 @@ namespace NzbDrone.Core.Parser { if (sceneSource) { - var remoteEpisode = Map(parsedEpisodeInfo, 0, 0, series, searchCriteria); + var remoteEpisode = Map(parsedEpisodeInfo, 0, 0, null, series, searchCriteria); return remoteEpisode.Episodes; } @@ -272,7 +273,7 @@ namespace NzbDrone.Core.Parser return GetStandardEpisodes(series, parsedEpisodeInfo, mappedSeasonNumber, sceneSource, searchCriteria); } - public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) + public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null) { if (searchCriteria != null) { @@ -285,6 +286,11 @@ namespace NzbDrone.Core.Parser { return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, searchCriteria.Series); } + + if (imdbId.IsNotNullOrWhiteSpace() && imdbId.Equals(searchCriteria.Series.ImdbId, StringComparison.Ordinal)) + { + return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, searchCriteria.Series); + } } var series = GetSeries(releaseTitle); @@ -304,6 +310,11 @@ namespace NzbDrone.Core.Parser series = _seriesService.FindByTvRageId(tvRageId); } + if (series == null && imdbId.IsNotNullOrWhiteSpace()) + { + series = _seriesService.FindByImdbId(imdbId); + } + if (series == null) { _logger.Debug("No matching series {0}", releaseTitle); @@ -354,7 +365,7 @@ namespace NzbDrone.Core.Parser return null; } - private FindSeriesResult FindSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SceneMapping sceneMapping, SearchCriteriaBase searchCriteria) + private FindSeriesResult FindSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, SceneMapping sceneMapping, SearchCriteriaBase searchCriteria) { Series series = null; @@ -406,6 +417,18 @@ namespace NzbDrone.Core.Parser return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); } + + if (imdbId.IsNotNullOrWhiteSpace() && imdbId.Equals(searchCriteria.Series.ImdbId, StringComparison.Ordinal) && tvdbId <= 0) + { + _logger.Debug() + .Message("Found matching series by IMDb ID {0}, an alias may be needed for: {1}", imdbId, parsedEpisodeInfo.SeriesTitle) + .Property("ImdbId", imdbId) + .Property("ParsedEpisodeInfo", parsedEpisodeInfo) + .WriteSentryWarn("ImdbIdMatch", imdbId, parsedEpisodeInfo.SeriesTitle) + .Write(); + + return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); + } } var matchType = SeriesMatchType.Unknown; @@ -462,6 +485,23 @@ namespace NzbDrone.Core.Parser } } + if (series == null && imdbId.IsNotNullOrWhiteSpace() && tvdbId <= 0) + { + series = _seriesService.FindByImdbId(imdbId); + + if (series != null) + { + _logger.Debug() + .Message("Found matching series by IMDb ID {0}, an alias may be needed for: {1}", imdbId, parsedEpisodeInfo.SeriesTitle) + .Property("ImdbId", imdbId) + .Property("ParsedEpisodeInfo", parsedEpisodeInfo) + .WriteSentryWarn("ImdbIdMatch", imdbId, parsedEpisodeInfo.SeriesTitle) + .Write(); + + matchType = SeriesMatchType.Id; + } + } + if (series == null) { _logger.Debug("No matching series {0}", parsedEpisodeInfo.SeriesTitle); diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index 88233f64e..f83545cc8 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Tv List<Series> FindByTitleInexact(string cleanTitle); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); + Series FindByImdbId(string imdbId); Series FindByPath(string path); List<int> AllSeriesTvdbIds(); Dictionary<int, string> AllSeriesPaths(); @@ -73,6 +74,11 @@ namespace NzbDrone.Core.Tv return Query(s => s.TvRageId == tvRageId).SingleOrDefault(); } + public Series FindByImdbId(string imdbId) + { + return Query(s => s.ImdbId == imdbId).SingleOrDefault(); + } + public Series FindByPath(string path) { return Query(s => s.Path == path) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index b582acffa..d34c63be4 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Tv List<Series> AddSeries(List<Series> newSeries); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); + Series FindByImdbId(string imdbId); Series FindByTitle(string title); Series FindByTitle(string title, int year); Series FindByTitleInexact(string title); @@ -94,6 +95,11 @@ namespace NzbDrone.Core.Tv return _seriesRepository.FindByTvRageId(tvRageId); } + public Series FindByImdbId(string imdbId) + { + return _seriesRepository.FindByImdbId(imdbId); + } + public Series FindByTitle(string title) { return _seriesRepository.FindByTitle(title.CleanSeriesTitle()); diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index ae8a6ed78..2b4ddb899 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -48,6 +48,7 @@ namespace Sonarr.Api.V3.Indexers public bool Rejected { get; set; } public int TvdbId { get; set; } public int TvRageId { get; set; } + public string ImdbId { get; set; } public IEnumerable<string> Rejections { get; set; } public DateTime PublishDate { get; set; } public string CommentUrl { get; set; } @@ -136,6 +137,7 @@ namespace Sonarr.Api.V3.Indexers Rejected = model.Rejected, TvdbId = releaseInfo.TvdbId, TvRageId = releaseInfo.TvRageId, + ImdbId = releaseInfo.ImdbId, Rejections = model.Rejections.Select(r => r.Reason).ToList(), PublishDate = releaseInfo.PublishDate, CommentUrl = releaseInfo.CommentUrl, @@ -194,6 +196,7 @@ namespace Sonarr.Api.V3.Indexers model.DownloadProtocol = resource.Protocol; model.TvdbId = resource.TvdbId; model.TvRageId = resource.TvRageId; + model.ImdbId = resource.ImdbId; model.PublishDate = resource.PublishDate.ToUniversalTime(); return model; diff --git a/src/Sonarr.Api.V3/Parse/ParseController.cs b/src/Sonarr.Api.V3/Parse/ParseController.cs index 2f0719741..5338e4434 100644 --- a/src/Sonarr.Api.V3/Parse/ParseController.cs +++ b/src/Sonarr.Api.V3/Parse/ParseController.cs @@ -45,7 +45,7 @@ namespace Sonarr.Api.V3.Parse }; } - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null); if (remoteEpisode != null) { From cc03ce04f1f8e50f2fe1c81f37d3684c12c3a983 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:56:15 +0300 Subject: [PATCH 440/762] Fixed: Formatting empty size on disk values --- frontend/src/RootFolder/RootFolderRow.tsx | 2 +- .../Details/{SeasonInfo.js => SeasonInfo.tsx} | 27 +++++++++---------- frontend/src/Series/Details/SeriesDetails.js | 9 +++---- .../Overview/SeriesIndexOverviewInfo.tsx | 4 ++- .../Index/Posters/SeriesIndexPosterInfo.tsx | 2 +- frontend/src/Utilities/Number/formatBytes.ts | 6 +---- 6 files changed, 22 insertions(+), 28 deletions(-) rename frontend/src/Series/Details/{SeasonInfo.js => SeasonInfo.tsx} (75%) diff --git a/frontend/src/RootFolder/RootFolderRow.tsx b/frontend/src/RootFolder/RootFolderRow.tsx index bf8ac6f7a..3b97319da 100644 --- a/frontend/src/RootFolder/RootFolderRow.tsx +++ b/frontend/src/RootFolder/RootFolderRow.tsx @@ -21,7 +21,7 @@ interface RootFolderRowProps { } function RootFolderRow(props: RootFolderRowProps) { - const { id, path, accessible, freeSpace, unmappedFolders = [] } = props; + const { id, path, accessible, freeSpace = 0, unmappedFolders = [] } = props; const isUnavailable = !accessible; diff --git a/frontend/src/Series/Details/SeasonInfo.js b/frontend/src/Series/Details/SeasonInfo.tsx similarity index 75% rename from frontend/src/Series/Details/SeasonInfo.js rename to frontend/src/Series/Details/SeasonInfo.tsx index bcadbcaf1..83e93a20d 100644 --- a/frontend/src/Series/Details/SeasonInfo.js +++ b/frontend/src/Series/Details/SeasonInfo.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; @@ -6,14 +5,19 @@ import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import styles from './SeasonInfo.css'; -function SeasonInfo(props) { - const { - totalEpisodeCount, - monitoredEpisodeCount, - episodeFileCount, - sizeOnDisk - } = props; +interface SeasonInfoProps { + totalEpisodeCount: number; + monitoredEpisodeCount: number; + episodeFileCount: number; + sizeOnDisk: number; +} +function SeasonInfo({ + totalEpisodeCount, + monitoredEpisodeCount, + episodeFileCount, + sizeOnDisk, +}: SeasonInfoProps) { return ( <DescriptionList> <DescriptionListItem @@ -47,11 +51,4 @@ function SeasonInfo(props) { ); } -SeasonInfo.propTypes = { - totalEpisodeCount: PropTypes.number.isRequired, - monitoredEpisodeCount: PropTypes.number.isRequired, - episodeFileCount: PropTypes.number.isRequired, - sizeOnDisk: PropTypes.number.isRequired -}; - export default SeasonInfo; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 10e7938ee..2871212ea 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -212,8 +212,8 @@ class SeriesDetails extends Component { } = this.props; const { - episodeFileCount, - sizeOnDisk + episodeFileCount = 0, + sizeOnDisk = 0 } = statistics; const { @@ -454,10 +454,9 @@ class SeriesDetails extends Component { name={icons.DRIVE} size={17} /> + <span className={styles.sizeOnDisk}> - { - formatBytes(sizeOnDisk || 0) - } + {formatBytes(sizeOnDisk)} </span> </div> </Label> diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 58a98c86d..6024e7967 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -194,10 +194,12 @@ function getInfoRowProps( } if (name === 'sizeOnDisk') { + const { sizeOnDisk = 0 } = props; + return { title: translate('SizeOnDisk'), iconName: icons.DRIVE, - label: formatBytes(props.sizeOnDisk), + label: formatBytes(sizeOnDisk), }; } diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx index 559ee9532..a85a95ae4 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosterInfo.tsx @@ -37,7 +37,7 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { added, seasonCount, path, - sizeOnDisk, + sizeOnDisk = 0, tags, sortKey, showRelativeDates, diff --git a/frontend/src/Utilities/Number/formatBytes.ts b/frontend/src/Utilities/Number/formatBytes.ts index bccf7435a..a0ae8a985 100644 --- a/frontend/src/Utilities/Number/formatBytes.ts +++ b/frontend/src/Utilities/Number/formatBytes.ts @@ -1,10 +1,6 @@ import { filesize } from 'filesize'; -function formatBytes(input?: string | number) { - if (!input) { - return ''; - } - +function formatBytes(input: string | number) { const size = Number(input); if (isNaN(size)) { From 35a2bc940328bf61b39dd0012867bdaa564ee489 Mon Sep 17 00:00:00 2001 From: kephasdev <160031725+kephasdev@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:47:59 -0400 Subject: [PATCH 441/762] Fix: Use indexer's Multi Languages setting for pushed releases Closes #7059 --- .../Aggregators/AggregateLanguagesFixture.cs | 59 +++++++++++++++++++ .../Aggregators/AggregateLanguages.cs | 18 +++++- src/NzbDrone.Core/Indexers/IndexerBase.cs | 5 +- src/NzbDrone.Core/Parser/Parser.cs | 7 +++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs index 4c9e3b4f3..50d09a154 100644 --- a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -4,6 +4,8 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Download.Aggregation.Aggregators; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.TorrentRss; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -62,6 +64,63 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_remoteEpisode.ParsedEpisodeInfo.Languages); } + [Test] + public void should_return_multi_languages_when_indexer_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + } + + [Test] + public void should_return_multi_languages_when_release_as_unknown_as_default_language_and_indexer_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.Unknown }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + } + + [Test] + public void should_return_original_when_indexer_has_no_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Settings = new TorrentRssIndexerSettings { } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage }); + } + [Test] public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title() { diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs index b180d9ed7..77c9c9170 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -10,10 +12,13 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators { public class AggregateLanguages : IAggregateRemoteEpisode { + private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; - public AggregateLanguages(Logger logger) + public AggregateLanguages(IIndexerFactory indexerFactory, + Logger logger) { + _indexerFactory = indexerFactory; _logger = logger; } @@ -71,6 +76,17 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators languages = languages.Except(languagesToRemove).ToList(); } + if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo is { IndexerId: > 0 } && releaseInfo.Title.IsNotNullOrWhiteSpace()) + { + var indexer = _indexerFactory.Get(releaseInfo.IndexerId); + + if (indexer?.Settings is IIndexerSettings settings && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(releaseInfo.Title)) + { + // Use indexer setting for Multi-languages + languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + } + } + // Use series language as fallback if we couldn't parse a language if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) { diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 4696bea3c..00a8965c9 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation.Results; using NLog; @@ -20,8 +19,6 @@ namespace NzbDrone.Core.Indexers public abstract class IndexerBase<TSettings> : IIndexer where TSettings : IIndexerSettings, new() { - private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - protected readonly IIndexerStatusService _indexerStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; @@ -94,7 +91,7 @@ namespace NzbDrone.Core.Indexers result.ForEach(c => { // Use multi languages from setting if ReleaseInfo languages is empty - if (c.Languages.Empty() && MultiRegex.IsMatch(c.Title) && settings.MultiLanguages.Any()) + if (c.Languages.Empty() && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(c.Title)) { c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index daad36a6f..54e32470e 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -575,6 +575,8 @@ namespace NzbDrone.Core.Parser private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; + private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); @@ -959,6 +961,11 @@ namespace NzbDrone.Core.Parser return title; } + public static bool HasMultipleLanguages(string title) + { + return MultiRegex.IsMatch(title); + } + private static SeriesTitleInfo GetSeriesTitleInfo(string title, MatchCollection matchCollection) { var seriesTitleInfo = new SeriesTitleInfo(); From 4b186e894e4e229a435c077e00c65b67ca178333 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 11 Aug 2024 08:48:22 -0700 Subject: [PATCH 442/762] Fixed: Marking queued item as failed not blocking the correct Torrent Info Hash --- .../Blocklisting/BlocklistService.cs | 6 ++- .../Download/FailedDownloadService.cs | 49 ++++++++++--------- src/Sonarr.Api.V3/Queue/QueueController.cs | 2 +- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 0ec53522c..f6aa6ceef 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -185,8 +185,10 @@ namespace NzbDrone.Core.Blocklisting Indexer = message.Data.GetValueOrDefault("indexer"), Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), Message = message.Message, - TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), - Languages = message.Languages + Languages = message.Languages, + TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent + ? message.TrackedDownload.DownloadItem.DownloadId + : message.Data.GetValueOrDefault("torrentInfoHash", null) }; if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index d392f0ea4..163035b6a 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download public interface IFailedDownloadService { void MarkAsFailed(int historyId, bool skipRedownload = false); - void MarkAsFailed(string downloadId, bool skipRedownload = false); + void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false); void Check(TrackedDownload trackedDownload); void ProcessFailed(TrackedDownload trackedDownload); } @@ -20,7 +20,6 @@ namespace NzbDrone.Core.Download public class FailedDownloadService : IFailedDownloadService { private readonly IHistoryService _historyService; - private readonly ITrackedDownloadService _trackedDownloadService; private readonly IEventAggregator _eventAggregator; public FailedDownloadService(IHistoryService historyService, @@ -28,7 +27,6 @@ namespace NzbDrone.Core.Download IEventAggregator eventAggregator) { _historyService = historyService; - _trackedDownloadService = trackedDownloadService; _eventAggregator = eventAggregator; } @@ -37,9 +35,10 @@ namespace NzbDrone.Core.Download var history = _historyService.Get(historyId); var downloadId = history.DownloadId; + if (downloadId.IsNullOrWhiteSpace()) { - PublishDownloadFailedEvent(new List<EpisodeHistory> { history }, "Manually marked as failed", skipRedownload: skipRedownload); + PublishDownloadFailedEvent(history, new List<int> { history.EpisodeId }, "Manually marked as failed", skipRedownload: skipRedownload); return; } @@ -53,21 +52,19 @@ namespace NzbDrone.Core.Download } // Add any other history items for the download ID then filter out any duplicate history items. - grabbedHistory.AddRange(_historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed)); + grabbedHistory.AddRange(GetGrabbedHistory(downloadId)); grabbedHistory = grabbedHistory.DistinctBy(h => h.Id).ToList(); - PublishDownloadFailedEvent(grabbedHistory, "Manually marked as failed"); + PublishDownloadFailedEvent(history, GetEpisodeIds(grabbedHistory), "Manually marked as failed"); } - public void MarkAsFailed(string downloadId, bool skipRedownload = false) + public void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false) { - var history = _historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed); + var history = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId); if (history.Any()) { - var trackedDownload = _trackedDownloadService.Find(downloadId); - - PublishDownloadFailedEvent(history, "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload); + PublishDownloadFailedEvent(history.First(), GetEpisodeIds(history), "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload); } } @@ -82,9 +79,7 @@ namespace NzbDrone.Core.Download if (trackedDownload.DownloadItem.IsEncrypted || trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed) { - var grabbedItems = _historyService - .Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed) - .ToList(); + var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId); if (grabbedItems.Empty()) { @@ -103,9 +98,7 @@ namespace NzbDrone.Core.Download return; } - var grabbedItems = _historyService - .Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed) - .ToList(); + var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId); if (grabbedItems.Empty()) { @@ -124,18 +117,17 @@ namespace NzbDrone.Core.Download } trackedDownload.State = TrackedDownloadState.Failed; - PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload); + PublishDownloadFailedEvent(grabbedItems.First(), GetEpisodeIds(grabbedItems), failure, trackedDownload); } - private void PublishDownloadFailedEvent(List<EpisodeHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false) + private void PublishDownloadFailedEvent(EpisodeHistory historyItem, List<int> episodeIds, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false) { - var historyItem = historyItems.Last(); Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource); var downloadFailedEvent = new DownloadFailedEvent { SeriesId = historyItem.SeriesId, - EpisodeIds = historyItems.Select(h => h.EpisodeId).Distinct().ToList(), + EpisodeIds = episodeIds, Quality = historyItem.Quality, SourceTitle = historyItem.SourceTitle, DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT), @@ -145,10 +137,23 @@ namespace NzbDrone.Core.Download TrackedDownload = trackedDownload, Languages = historyItem.Languages, SkipRedownload = skipRedownload, - ReleaseSource = releaseSource + ReleaseSource = releaseSource, }; _eventAggregator.PublishEvent(downloadFailedEvent); } + + private List<int> GetEpisodeIds(List<EpisodeHistory> historyItems) + { + return historyItems.Select(h => h.EpisodeId).Distinct().ToList(); + } + + private List<EpisodeHistory> GetGrabbedHistory(string downloadId) + { + // Sort by date so items are always in the same order + return _historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed) + .OrderByDescending(h => h.Date) + .ToList(); + } } } diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 34622ad18..2e74dec94 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -323,7 +323,7 @@ namespace Sonarr.Api.V3.Queue if (blocklist) { - _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload); + _failedDownloadService.MarkAsFailed(trackedDownload, skipRedownload); } if (!removeFromClient && !blocklist && !changeCategory) From f7a58aab339e2012b6bb91d0b3a38d733ec213c6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 10 Aug 2024 20:38:58 -0700 Subject: [PATCH 443/762] Align queue action buttons on right --- frontend/src/Activity/Queue/QueueRow.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Activity/Queue/QueueRow.css b/frontend/src/Activity/Queue/QueueRow.css index 4a9ff08b9..459cdad8e 100644 --- a/frontend/src/Activity/Queue/QueueRow.css +++ b/frontend/src/Activity/Queue/QueueRow.css @@ -26,4 +26,5 @@ composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 70px; + text-align: right; } From 37c4647f242c37f22c7ac455d304055441acf362 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 10 Aug 2024 20:52:53 -0700 Subject: [PATCH 444/762] Fix typos and improve log messages --- distribution/debian/install.sh | 4 ++-- src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs | 2 +- src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/distribution/debian/install.sh b/distribution/debian/install.sh index 87a0b0914..803d7cf51 100644 --- a/distribution/debian/install.sh +++ b/distribution/debian/install.sh @@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ') app_guid=${app_guid:-media} echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory" -echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories" +echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories" read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty # Create User / Group as needed @@ -114,7 +114,7 @@ case "$ARCH" in esac echo "" echo "Removing previous tarballs" -# -f to Force so we fail if it doesnt exist +# -f to Force so we fail if it doesn't exist rm -f "${app^}".*.tar.gz echo "" echo "Downloading..." diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 44d78e311..35701d3de 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -201,7 +201,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge if (ignoredCount > 0) { - _logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents"); + _logger.Warn("{0} torrent(s) were ignored because they did not have a title. Check Deluge and remove any invalid torrents"); } return items; diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 948994c51..288209030 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Test.Common.AutoMoq if (behavior != MockBehavior.Default && mock.Behavior == MockBehavior.Default) { - throw new InvalidOperationException("Unable to change be behaviour of a an existing mock."); + throw new InvalidOperationException("Unable to change be behaviour of an existing mock."); } return mock; From ffdb08cfe655a5357666d18d3dc6b9f0b668434b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:40:21 +0300 Subject: [PATCH 445/762] Fixed: Dedupe titles to avoid similar search requests --- .../DataAugmentation/Scene/SceneMappingService.cs | 4 +++- src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index 94e30fec0..10aac05ec 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -63,7 +63,9 @@ namespace NzbDrone.Core.DataAugmentation.Scene sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) || ((n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb")) .Where(n => IsEnglish(n.SearchTerm)) - .Select(n => n.SearchTerm).Distinct().ToList(); + .Select(n => n.SearchTerm) + .Distinct(StringComparer.InvariantCultureIgnoreCase) + .ToList(); return names; } diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index ee892b7b9..10b9fc0fb 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -193,7 +193,7 @@ namespace NzbDrone.Core.IndexerSearch foreach (var item in dict) { item.Value.Episodes = item.Value.Episodes.Distinct().ToList(); - item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList(); + item.Value.SceneTitles = item.Value.SceneTitles.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); } return dict.Values.ToList(); @@ -221,7 +221,7 @@ namespace NzbDrone.Core.IndexerSearch foreach (var item in dict) { - item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList(); + item.Value.SceneTitles = item.Value.SceneTitles.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); } return dict.Values.ToList(); @@ -463,7 +463,7 @@ namespace NzbDrone.Core.IndexerSearch spec.UserInvokedSearch = userInvokedSearch; spec.InteractiveSearch = interactiveSearch; - if (!spec.SceneTitles.Contains(series.Title)) + if (!spec.SceneTitles.Contains(series.Title, StringComparer.InvariantCultureIgnoreCase)) { spec.SceneTitles.Add(series.Title); } From eb2fd1350904cdcc8e7d56147111da40a5df8a11 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:51:11 +0300 Subject: [PATCH 446/762] Fixed: Overwriting query params for remove item handler (#7075) --- frontend/src/Store/Actions/Creators/createRemoveItemHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js index dfe29ace8..3de794bdf 100644 --- a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js +++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js @@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) { return function(getState, payload, dispatch) { const { id, - ...queryParams + queryParams } = payload; dispatch(set({ section, isDeleting: true })); From 7b87de2e93c2aa499cff224f84253ba944bb58d4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:53:17 +0300 Subject: [PATCH 447/762] Clear pending changes for edit import list exclusions on modal close --- .../ImportListExclusions/EditImportListExclusionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx index 9b7afb3ba..b889a8105 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -32,7 +32,7 @@ function EditImportListExclusionModal( <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}> <EditImportListExclusionModalContent {...otherProps} - onModalClose={onModalClose} + onModalClose={onModalClosePress} /> </Modal> ); From 2f04b037a18749f89a976e57d787a106eac86829 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:06:51 +0300 Subject: [PATCH 448/762] Fixed nlog deprecated calls --- src/NzbDrone.Core/Parser/ParsingService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 487824092..7cbff94f1 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -420,12 +420,12 @@ namespace NzbDrone.Core.Parser if (imdbId.IsNotNullOrWhiteSpace() && imdbId.Equals(searchCriteria.Series.ImdbId, StringComparison.Ordinal) && tvdbId <= 0) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by IMDb ID {0}, an alias may be needed for: {1}", imdbId, parsedEpisodeInfo.SeriesTitle) .Property("ImdbId", imdbId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("ImdbIdMatch", imdbId, parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); return new FindSeriesResult(searchCriteria.Series, SeriesMatchType.Id); } @@ -491,12 +491,12 @@ namespace NzbDrone.Core.Parser if (series != null) { - _logger.Debug() + _logger.ForDebugEvent() .Message("Found matching series by IMDb ID {0}, an alias may be needed for: {1}", imdbId, parsedEpisodeInfo.SeriesTitle) .Property("ImdbId", imdbId) .Property("ParsedEpisodeInfo", parsedEpisodeInfo) .WriteSentryWarn("ImdbIdMatch", imdbId, parsedEpisodeInfo.SeriesTitle) - .Write(); + .Log(); matchType = SeriesMatchType.Id; } From d713b83a362bab907e63bc1d0cb7cb4c1cca3bc9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:46:09 +0300 Subject: [PATCH 449/762] Fixed: Sending Manual Interaction Required notifications for unknown series For Discord/Webhooks/CustomScript --- .../CustomScript/CustomScript.cs | 29 ++-- .../Notifications/Discord/Discord.cs | 60 +++++--- .../Notifications/Slack/Slack.cs | 138 +++++++++--------- .../Notifications/Webhook/WebhookBase.cs | 14 +- 4 files changed, 134 insertions(+), 107 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index f1b344101..c5e2e2146 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -406,18 +406,18 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_EventType", "ManualInteractionRequired"); environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); - environmentVariables.Add("Sonarr_Series_Id", series.Id.ToString()); - environmentVariables.Add("Sonarr_Series_Title", series.Title); - environmentVariables.Add("Sonarr_Series_TitleSlug", series.TitleSlug); - environmentVariables.Add("Sonarr_Series_Path", series.Path); - environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString()); - environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString()); - environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString()); - environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty); - environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString()); - environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString()); - environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode); - environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres)); + environmentVariables.Add("Sonarr_Series_Id", series?.Id.ToString()); + environmentVariables.Add("Sonarr_Series_Title", series?.Title); + environmentVariables.Add("Sonarr_Series_TitleSlug", series?.TitleSlug); + environmentVariables.Add("Sonarr_Series_Path", series?.Path); + environmentVariables.Add("Sonarr_Series_TvdbId", series?.TvdbId.ToString()); + environmentVariables.Add("Sonarr_Series_TvMazeId", series?.TvMazeId.ToString()); + environmentVariables.Add("Sonarr_Series_TmdbId", series?.TmdbId.ToString()); + environmentVariables.Add("Sonarr_Series_ImdbId", series?.ImdbId ?? string.Empty); + environmentVariables.Add("Sonarr_Series_Type", series?.SeriesType.ToString()); + environmentVariables.Add("Sonarr_Series_Year", series?.Year.ToString()); + environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series?.OriginalLanguage)?.ThreeLetterCode); + environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series?.Genres ?? new List<string>())); environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientInfo?.Name ?? string.Empty); environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientInfo?.Type ?? string.Empty); @@ -482,6 +482,11 @@ namespace NzbDrone.Core.Notifications.CustomScript private List<string> GetTagLabels(Series series) { + if (series == null) + { + return null; + } + return _tagRepository.GetTags(series.Tags) .Select(s => s.Label) .Where(l => l.IsNotNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 8ef8dbf62..08d21ca0c 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using FluentValidation.Results; using NzbDrone.Common.Extensions; @@ -329,12 +330,12 @@ namespace NzbDrone.Core.Notifications.Discord public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { var attachments = new List<Embed> - { - new Embed - { - Title = series.Title, - } - }; + { + new () + { + Title = series.Title, + } + }; var payload = CreatePayload("Renamed", attachments); @@ -361,8 +362,8 @@ namespace NzbDrone.Core.Notifications.Discord Color = (int)DiscordColors.Danger, Fields = new List<DiscordField> { - new DiscordField { Name = "Reason", Value = reason.ToString() }, - new DiscordField { Name = "File name", Value = string.Format("```{0}```", deletedFile) } + new () { Name = "Reason", Value = reason.ToString() }, + new () { Name = "File name", Value = string.Format("```{0}```", deletedFile) } }, Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), }; @@ -386,7 +387,7 @@ namespace NzbDrone.Core.Notifications.Discord Title = series.Title, Description = "Series Added", Color = (int)DiscordColors.Success, - Fields = new List<DiscordField> { new DiscordField { Name = "Links", Value = GetLinksString(series) } } + Fields = new List<DiscordField> { new () { Name = "Links", Value = GetLinksString(series) } } }; if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) @@ -425,7 +426,7 @@ namespace NzbDrone.Core.Notifications.Discord Title = series.Title, Description = deleteMessage.DeletedFilesMessage, Color = (int)DiscordColors.Danger, - Fields = new List<DiscordField> { new DiscordField { Name = "Links", Value = GetLinksString(series) } } + Fields = new List<DiscordField> { new () { Name = "Links", Value = GetLinksString(series) } } }; if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) @@ -503,12 +504,12 @@ namespace NzbDrone.Core.Notifications.Discord Color = (int)DiscordColors.Standard, Fields = new List<DiscordField>() { - new DiscordField() + new () { Name = "Previous Version", Value = updateMessage.PreviousVersion.ToString() }, - new DiscordField() + new () { Name = "New Version", Value = updateMessage.NewVersion.ToString() @@ -533,7 +534,7 @@ namespace NzbDrone.Core.Notifications.Discord Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/256.png" }, - Url = $"http://thetvdb.com/?tab=series&id={series.TvdbId}", + Url = series?.TvdbId > 0 ? $"http://thetvdb.com/?tab=series&id={series.TvdbId}" : null, Description = "Manual interaction needed", Title = GetTitle(series, episodes), Color = (int)DiscordColors.Standard, @@ -545,7 +546,7 @@ namespace NzbDrone.Core.Notifications.Discord { embed.Thumbnail = new DiscordImage { - Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url + Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url }; } @@ -553,7 +554,7 @@ namespace NzbDrone.Core.Notifications.Discord { embed.Image = new DiscordImage { - Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url + Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url }; } @@ -564,26 +565,26 @@ namespace NzbDrone.Core.Notifications.Discord switch ((DiscordManualInteractionFieldType)field) { case DiscordManualInteractionFieldType.Overview: - var overview = episodes.First().Overview ?? ""; + var overview = episodes.FirstOrDefault()?.Overview ?? ""; discordField.Name = "Overview"; discordField.Value = overview.Length <= 300 ? overview : $"{overview.AsSpan(0, 300)}..."; break; case DiscordManualInteractionFieldType.Rating: discordField.Name = "Rating"; - discordField.Value = episodes.First().Ratings.Value.ToString(); + discordField.Value = episodes.FirstOrDefault()?.Ratings?.Value.ToString(CultureInfo.InvariantCulture); break; case DiscordManualInteractionFieldType.Genres: discordField.Name = "Genres"; - discordField.Value = series.Genres.Take(5).Join(", "); + discordField.Value = series?.Genres.Take(5).Join(", "); break; case DiscordManualInteractionFieldType.Quality: discordField.Name = "Quality"; discordField.Inline = true; - discordField.Value = message.Quality.Quality.Name; + discordField.Value = message.Quality?.Quality?.Name; break; case DiscordManualInteractionFieldType.Group: discordField.Name = "Group"; - discordField.Value = message.Episode.ParsedEpisodeInfo.ReleaseGroup; + discordField.Value = message.Episode?.ParsedEpisodeInfo?.ReleaseGroup; break; case DiscordManualInteractionFieldType.Size: discordField.Name = "Size"; @@ -592,7 +593,7 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordManualInteractionFieldType.DownloadTitle: discordField.Name = "Download"; - discordField.Value = string.Format("```{0}```", message.TrackedDownload.DownloadItem.Title); + discordField.Value = $"```{message.TrackedDownload.DownloadItem.Title}```"; break; case DiscordManualInteractionFieldType.Links: discordField.Name = "Links"; @@ -677,10 +678,16 @@ namespace NzbDrone.Core.Notifications.Discord private string GetLinksString(Series series) { - var links = new List<string>(); + if (series == null) + { + return null; + } - links.Add($"[The TVDB](https://thetvdb.com/?tab=series&id={series.TvdbId})"); - links.Add($"[Trakt](https://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show)"); + var links = new List<string> + { + $"[The TVDB](https://thetvdb.com/?tab=series&id={series.TvdbId})", + $"[Trakt](https://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show)" + }; if (series.ImdbId.IsNotNullOrWhiteSpace()) { @@ -692,6 +699,11 @@ namespace NzbDrone.Core.Notifications.Discord private string GetTitle(Series series, List<Episode> episodes) { + if (series == null) + { + return null; + } + if (series.SeriesType == SeriesTypes.Daily) { var episode = episodes.First(); diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index eb6bfb636..ce1fa76b3 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -28,15 +28,15 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnGrab(GrabMessage message) { var attachments = new List<Attachment> - { - new Attachment - { - Fallback = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = "warning" - } - }; + { + new () + { + Fallback = message.Message, + Title = message.Series.Title, + Text = message.Message, + Color = "warning" + } + }; var payload = CreatePayload($"Grabbed: {message.Message}", attachments); _proxy.SendPayload(payload, Settings); @@ -45,15 +45,15 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnDownload(DownloadMessage message) { var attachments = new List<Attachment> - { - new Attachment - { - Fallback = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = "good" - } - }; + { + new () + { + Fallback = message.Message, + Title = message.Series.Title, + Text = message.Message, + Color = "good" + } + }; var payload = CreatePayload($"Imported: {message.Message}", attachments); _proxy.SendPayload(payload, Settings); @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Notifications.Slack { var attachments = new List<Attachment> { - new Attachment + new () { Fallback = message.Message, Title = message.Series.Title, @@ -79,12 +79,12 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles) { var attachments = new List<Attachment> - { - new Attachment - { - Title = series.Title, - } - }; + { + new () + { + Title = series.Title, + } + }; var payload = CreatePayload("Renamed", attachments); @@ -94,12 +94,12 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var attachments = new List<Attachment> - { - new Attachment - { - Title = GetTitle(deleteMessage.Series, deleteMessage.EpisodeFile.Episodes), - } - }; + { + new () + { + Title = GetTitle(deleteMessage.Series, deleteMessage.EpisodeFile.Episodes), + } + }; var payload = CreatePayload("Episode Deleted", attachments); @@ -109,12 +109,12 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnSeriesAdd(SeriesAddMessage message) { var attachments = new List<Attachment> - { - new Attachment - { - Title = message.Series.Title, - } - }; + { + new () + { + Title = message.Series.Title, + } + }; var payload = CreatePayload("Series Added", attachments); @@ -124,13 +124,13 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { var attachments = new List<Attachment> - { - new Attachment - { - Title = deleteMessage.Series.Title, - Text = deleteMessage.DeletedFilesMessage - } - }; + { + new () + { + Title = deleteMessage.Series.Title, + Text = deleteMessage.DeletedFilesMessage + } + }; var payload = CreatePayload("Series Deleted", attachments); @@ -140,14 +140,14 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { var attachments = new List<Attachment> - { - new Attachment - { - Title = healthCheck.Source.Name, - Text = healthCheck.Message, - Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? "warning" : "danger" - } - }; + { + new () + { + Title = healthCheck.Source.Name, + Text = healthCheck.Message, + Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? "warning" : "danger" + } + }; var payload = CreatePayload("Health Issue", attachments); @@ -157,14 +157,14 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var attachments = new List<Attachment> - { - new Attachment - { - Title = previousCheck.Source.Name, - Text = $"The following issue is now resolved: {previousCheck.Message}", - Color = "good" - } - }; + { + new () + { + Title = previousCheck.Source.Name, + Text = $"The following issue is now resolved: {previousCheck.Message}", + Color = "good" + } + }; var payload = CreatePayload("Health Issue Resolved", attachments); @@ -174,14 +174,14 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var attachments = new List<Attachment> - { - new Attachment - { - Title = Environment.MachineName, - Text = updateMessage.Message, - Color = "good" - } - }; + { + new () + { + Title = Environment.MachineName, + Text = updateMessage.Message, + Color = "good" + } + }; var payload = CreatePayload("Application Updated", attachments); @@ -192,7 +192,7 @@ namespace NzbDrone.Core.Notifications.Slack { var attachments = new List<Attachment> { - new Attachment + new () { Title = Environment.MachineName, Text = message.Message, diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index 558a6114f..71fe1bff0 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -229,9 +229,9 @@ namespace NzbDrone.Core.Notifications.Webhook TvdbId = 1234, Tags = new List<string> { "test-tag" } }, - Episodes = new List<WebhookEpisode>() + Episodes = new List<WebhookEpisode> { - new WebhookEpisode() + new () { Id = 123, EpisodeNumber = 1, @@ -244,6 +244,11 @@ namespace NzbDrone.Core.Notifications.Webhook private WebhookSeries GetSeries(Series series) { + if (series == null) + { + return null; + } + _mediaCoverService.ConvertToLocalUrls(series.Id, series.Images); return new WebhookSeries(series, GetTagLabels(series)); @@ -251,6 +256,11 @@ namespace NzbDrone.Core.Notifications.Webhook private List<string> GetTagLabels(Series series) { + if (series == null) + { + return null; + } + return _tagRepository.GetTags(series.Tags) .Select(s => s.Label) .Where(l => l.IsNotNullOrWhiteSpace()) From 2d237ae6b76cf7c365a84859b780a667ca83d553 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:31:13 +0300 Subject: [PATCH 450/762] Cleanup old prop-types for TS --- .../History/Details/HistoryDetailsModal.tsx | 6 +--- frontend/src/Activity/History/HistoryRow.tsx | 6 +--- frontend/src/Activity/Queue/QueueStatus.tsx | 6 ---- frontend/src/App/App.tsx | 8 +---- frontend/src/App/AppRoutes.tsx | 5 ---- frontend/src/App/State/SystemAppState.ts | 4 +-- frontend/src/Commands/Command.ts | 2 +- .../Components/Form/SeriesTypeSelectInput.tsx | 9 ++---- frontend/src/System/Status/Status.tsx | 29 ++++++++----------- 9 files changed, 20 insertions(+), 55 deletions(-) diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx index a833bca5b..a33e0b1ba 100644 --- a/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx @@ -51,7 +51,7 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) { sourceTitle, data, downloadId, - isMarkingAsFailed, + isMarkingAsFailed = false, shortDateFormat, timeFormat, onMarkAsFailedPress, @@ -93,8 +93,4 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) { ); } -HistoryDetailsModal.defaultProps = { - isMarkingAsFailed: false, -}; - export default HistoryDetailsModal; diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx index bcd84f606..c24f6a286 100644 --- a/frontend/src/Activity/History/HistoryRow.tsx +++ b/frontend/src/Activity/History/HistoryRow.tsx @@ -61,7 +61,7 @@ function HistoryRow(props: HistoryRowProps) { date, data, downloadId, - isMarkingAsFailed, + isMarkingAsFailed = false, markAsFailedError, columns, } = props; @@ -268,8 +268,4 @@ function HistoryRow(props: HistoryRowProps) { ); } -HistoryRow.defaultProps = { - customFormats: [], -}; - export default HistoryRow; diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx index d7314baff..64d802df8 100644 --- a/frontend/src/Activity/Queue/QueueStatus.tsx +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -155,10 +155,4 @@ function QueueStatus(props: QueueStatusProps) { ); } -QueueStatus.defaultProps = { - trackedDownloadStatus: 'ok', - trackedDownloadState: 'downloading', - canFlip: false, -}; - export default QueueStatus; diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.tsx index 6c2d799f3..0014c1d3f 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.tsx @@ -1,5 +1,4 @@ import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; -import PropTypes from 'prop-types'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; @@ -20,7 +19,7 @@ function App({ store, history }: AppProps) { <ConnectedRouter history={history}> <ApplyTheme /> <PageConnector> - <AppRoutes app={App} /> + <AppRoutes /> </PageConnector> </ConnectedRouter> </Provider> @@ -28,9 +27,4 @@ function App({ store, history }: AppProps) { ); } -App.propTypes = { - store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; - export default App; diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index e3bf426c9..1b4fea9c2 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Redirect, Route } from 'react-router-dom'; import Blocklist from 'Activity/Blocklist/Blocklist'; @@ -165,8 +164,4 @@ function AppRoutes() { ); } -AppRoutes.propTypes = { - app: PropTypes.func.isRequired, -}; - export default AppRoutes; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index d20dacc51..1161f0e1e 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -8,15 +8,15 @@ import AppSectionState, { AppSectionItemState } from './AppSectionState'; export type DiskSpaceAppState = AppSectionState<DiskSpace>; export type HealthAppState = AppSectionState<Health>; export type SystemStatusAppState = AppSectionItemState<SystemStatus>; -export type UpdateAppState = AppSectionState<Update>; export type TaskAppState = AppSectionState<Task>; +export type UpdateAppState = AppSectionState<Update>; interface SystemAppState { diskSpace: DiskSpaceAppState; health: HealthAppState; - updates: UpdateAppState; status: SystemStatusAppState; tasks: TaskAppState; + updates: UpdateAppState; } export default SystemAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index d0797a79a..ed0a449ab 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -26,7 +26,7 @@ export interface CommandBody { seriesId?: number; seriesIds?: number[]; seasonNumber?: number; - [key: string]: string | number | boolean | undefined | number[] | undefined; + [key: string]: string | number | boolean | number[] | undefined; } interface Command extends ModelBase { diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx index cea7f4fb5..17082c75c 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx @@ -46,9 +46,9 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { const values = [...seriesTypeOptions]; const { - includeNoChange, + includeNoChange = false, includeNoChangeDisabled = true, - includeMixed, + includeMixed = false, } = props; if (includeNoChange) { @@ -77,9 +77,4 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { ); } -SeriesTypeSelectInput.defaultProps = { - includeNoChange: false, - includeMixed: false, -}; - export default SeriesTypeSelectInput; diff --git a/frontend/src/System/Status/Status.tsx b/frontend/src/System/Status/Status.tsx index ae1636b3e..74fdf25d8 100644 --- a/frontend/src/System/Status/Status.tsx +++ b/frontend/src/System/Status/Status.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; @@ -7,22 +7,17 @@ import DiskSpace from './DiskSpace/DiskSpace'; import Health from './Health/Health'; import MoreInfo from './MoreInfo/MoreInfo'; -class Status extends Component { - // - // Render - - render() { - return ( - <PageContent title={translate('Status')}> - <PageContentBody> - <Health /> - <DiskSpace /> - <About /> - <MoreInfo /> - </PageContentBody> - </PageContent> - ); - } +function Status() { + return ( + <PageContent title={translate('Status')}> + <PageContentBody> + <Health /> + <DiskSpace /> + <About /> + <MoreInfo /> + </PageContentBody> + </PageContent> + ); } export default Status; From 3b29096e402c9a738b12ca8085b97924c52577f9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:06:27 +0300 Subject: [PATCH 451/762] Fix wiki link for update healthcheck --- src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index b505785a1..c7239b88a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -97,7 +97,8 @@ namespace NzbDrone.Core.HealthCheck.Checks _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary<string, object> { { "version", $"v{latestAvailable.Version}" } - })); + }), + "#new-update-is-available"); } } From 639b53887d516cc613d3702a1de3157892afc3e6 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:19:12 +0300 Subject: [PATCH 452/762] New: Bulk import list exclusions removal --- frontend/src/Components/Table/Table.js | 5 +- .../ImportListExclusion.css.d.ts | 7 - .../ImportListExclusionRow.tsx | 25 +- .../ImportListExclusions.css | 6 + .../ImportListExclusions.css.d.ts | 3 +- .../ImportListExclusions.tsx | 293 +++++++++++------- .../ImportLists/ImportListSettings.js | 5 +- .../ImportLists/ImportListsConnector.js | 6 +- .../ImportLists/Options/ImportListOptions.tsx | 3 +- .../Actions/Settings/importListExclusions.js | 40 ++- .../src/Store/Actions/Settings/importLists.js | 26 +- .../Exclusions/ImportListExclusionService.cs | 6 + src/NzbDrone.Core/Localization/Core/en.json | 4 +- .../ImportListExclusionBulkResource.cs | 9 + .../ImportListExclusionController.cs | 12 +- 15 files changed, 286 insertions(+), 164 deletions(-) delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css create mode 100644 src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js index 8afbf9ea0..4c970e469 100644 --- a/frontend/src/Components/Table/Table.js +++ b/frontend/src/Components/Table/Table.js @@ -66,7 +66,9 @@ function Table(props) { columns.map((column) => { const { name, - isVisible + isVisible, + isSortable, + ...otherColumnProps } = column; if (!isVisible) { @@ -84,6 +86,7 @@ function Table(props) { name={name} isSortable={false} {...otherProps} + {...otherColumnProps} > <TableOptionsModalWrapper columns={columns} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts deleted file mode 100644 index d8ea83dc1..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 index 37de7940a..155d53a78 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -1,21 +1,28 @@ import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { icons, kinds } from 'Helpers/Props'; +import { deleteImportListExclusion } from 'Store/Actions/Settings/importListExclusions'; import ImportListExclusion from 'typings/ImportListExclusion'; +import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import EditImportListExclusionModal from './EditImportListExclusionModal'; import styles from './ImportListExclusionRow.css'; interface ImportListExclusionRowProps extends ImportListExclusion { - onConfirmDeleteImportListExclusion: (id: number) => void; + isSelected: boolean; + onSelectedChange: (options: SelectStateInputProps) => void; } function ImportListExclusionRow(props: ImportListExclusionRowProps) { - const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props; + const { id, tvdbId, title, isSelected, onSelectedChange } = props; + + const dispatch = useDispatch(); const [ isEditImportListExclusionModalOpen, @@ -29,12 +36,18 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) { setDeleteImportListExclusionModalClosed, ] = useModalOpenState(false); - const onConfirmDeleteImportListExclusionPress = useCallback(() => { - onConfirmDeleteImportListExclusion(id); - }, [id, onConfirmDeleteImportListExclusion]); + const handleDeletePress = useCallback(() => { + dispatch(deleteImportListExclusion({ id })); + }, [id, dispatch]); return ( <TableRow> + <TableSelectCell + id={id} + isSelected={isSelected} + onSelectedChange={onSelectedChange} + /> + <TableRowCell>{title}</TableRowCell> <TableRowCell>{tvdbId}</TableRowCell> @@ -58,7 +71,7 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) { title={translate('DeleteImportListExclusion')} message={translate('DeleteImportListExclusionMessageText')} confirmLabel={translate('Delete')} - onConfirm={onConfirmDeleteImportListExclusionPress} + onConfirm={handleDeletePress} onCancel={setDeleteImportListExclusionModalClosed} /> </TableRow> diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css new file mode 100644 index 000000000..e213a1c11 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css @@ -0,0 +1,6 @@ +.actions { + composes: headerCell from '~Components/Table/TableHeaderCell.css'; + + width: 35px; + white-space: nowrap; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts index 626717e71..d8ea83dc1 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts @@ -1,8 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'addButton': string; - 'addImportListExclusion': string; + 'actions': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index 8c7033686..a93ecda3c 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -1,28 +1,46 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } 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 SpinnerButton from 'Components/Link/SpinnerButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; import PageSectionContent from 'Components/Page/PageSectionContent'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; 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 usePaging from 'Components/Table/usePaging'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; -import { icons } from 'Helpers/Props'; -import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import useSelectState from 'Helpers/Hooks/useSelectState'; +import { icons, kinds } from 'Helpers/Props'; +import { + bulkDeleteImportListExclusions, + clearImportListExclusions, + fetchImportListExclusions, + gotoImportListExclusionPage, + setImportListExclusionSort, + setImportListExclusionTableOption, +} from 'Store/Actions/Settings/importListExclusions'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; +import { TableOptionsChangePayload } from 'typings/Table'; import { registerPagePopulator, unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; import EditImportListExclusionModal from './EditImportListExclusionModal'; import ImportListExclusionRow from './ImportListExclusionRow'; +import styles from './ImportListExclusions.css'; -const COLUMNS = [ +const COLUMNS: Column[] = [ { name: 'title', label: () => translate('Title'), @@ -36,13 +54,15 @@ const COLUMNS = [ isSortable: true, }, { + className: styles.actions, name: 'actions', + label: '', isVisible: true, isSortable: false, }, ]; -function createImportListExlucionsSelector() { +function createImportListExclusionsSelector() { return createSelector( (state: AppState) => state.settings.importListExclusions, (importListExclusions) => { @@ -54,95 +74,7 @@ function createImportListExlucionsSelector() { } 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 requestCurrentPage = useCurrentPage(); const { isFetching, @@ -152,9 +84,127 @@ function ImportListExclusions() { sortKey, error, sortDirection, + page, + totalPages, totalRecords, - ...otherProps - } = selected; + isDeleting, + deleteError, + } = useSelector(createImportListExclusionsSelector()); + + const dispatch = useDispatch(); + + const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = + useState(false); + const previousIsDeleting = usePrevious(isDeleting); + + const [selectState, setSelectState] = useSelectState(); + const { allSelected, allUnselected, selectedState } = selectState; + + const selectedIds = useMemo(() => { + return getSelectedIds(selectedState); + }, [selectedState]); + + const handleSelectAllChange = useCallback( + ({ value }: CheckInputChanged) => { + setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + }, + [items, setSelectState] + ); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + setSelectState({ + type: 'toggleSelected', + items, + id, + isSelected: value, + shiftKey, + }); + }, + [items, setSelectState] + ); + + const handleDeleteSelectedPress = useCallback(() => { + setIsConfirmDeleteModalOpen(true); + }, [setIsConfirmDeleteModalOpen]); + + const handleDeleteSelectedConfirmed = useCallback(() => { + dispatch(bulkDeleteImportListExclusions({ ids: selectedIds })); + setIsConfirmDeleteModalOpen(false); + }, [selectedIds, setIsConfirmDeleteModalOpen, dispatch]); + + const handleConfirmDeleteModalClose = useCallback(() => { + setIsConfirmDeleteModalOpen(false); + }, [setIsConfirmDeleteModalOpen]); + + const { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + } = usePaging({ + page, + totalPages, + gotoPage: gotoImportListExclusionPage, + }); + + const handleSortPress = useCallback( + (sortKey: { sortKey: string }) => { + dispatch(setImportListExclusionSort({ sortKey })); + }, + [dispatch] + ); + + const handleTableOptionChange = useCallback( + (payload: TableOptionsChangePayload) => { + dispatch(setImportListExclusionTableOption(payload)); + + if (payload.pageSize) { + dispatch(gotoImportListExclusionPage({ page: 1 })); + } + }, + [dispatch] + ); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchImportListExclusions()); + } else { + dispatch(gotoImportListExclusionPage({ page: 1 })); + } + + return () => { + dispatch(clearImportListExclusions()); + }; + }, [requestCurrentPage, dispatch]); + + useEffect(() => { + const repopulate = () => { + dispatch(fetchImportListExclusions()); + }; + + registerPagePopulator(repopulate); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [dispatch]); + + useEffect(() => { + if (previousIsDeleting && !isDeleting && !deleteError) { + setSelectState({ type: 'unselectAll', items }); + + dispatch(fetchImportListExclusions()); + } + }, [ + previousIsDeleting, + isDeleting, + deleteError, + items, + dispatch, + setSelectState, + ]); const [ isAddImportListExclusionModalOpen, @@ -173,13 +223,17 @@ function ImportListExclusions() { error={error} > <Table + selectAll={true} + allSelected={allSelected} + allUnselected={allUnselected} columns={COLUMNS} canModifyColumns={false} pageSize={pageSize} sortKey={sortKey} sortDirection={sortDirection} - onSortPress={setImportListExclusionSort} - onTableOptionChange={setImportListTableOption} + onTableOptionChange={handleTableOptionChange} + onSelectAllChange={handleSelectAllChange} + onSortPress={handleSortPress} > <TableBody> {items.map((item) => { @@ -187,16 +241,23 @@ function ImportListExclusions() { <ImportListExclusionRow key={item.id} {...item} - onConfirmDeleteImportListExclusion={ - onConfirmDeleteImportListExclusion - } + isSelected={selectedState[item.id] || false} + onSelectedChange={handleSelectedChange} /> ); })} <TableRow> - <TableRowCell /> - <TableRowCell /> + <TableRowCell colSpan={3}> + <SpinnerButton + kind={kinds.DANGER} + isSpinning={isDeleting} + isDisabled={!selectedIds.length} + onPress={handleDeleteSelectedPress} + > + {translate('Delete')} + </SpinnerButton> + </TableRowCell> <TableRowCell> <IconButton @@ -209,21 +270,31 @@ function ImportListExclusions() { </Table> <TablePager + page={page} + totalPages={totalPages} totalRecords={totalRecords} - pageSize={pageSize} isFetching={isFetching} - onFirstPagePress={gotoImportListExclusionFirstPage} - onPreviousPagePress={gotoImportListExclusionPreviousPage} - onNextPagePress={gotoImportListExclusionNextPage} - onLastPagePress={gotoImportListExclusionLastPage} - onPageSelect={gotoImportListExclusionPage} - {...otherProps} + onFirstPagePress={handleFirstPagePress} + onPreviousPagePress={handlePreviousPagePress} + onNextPagePress={handleNextPagePress} + onLastPagePress={handleLastPagePress} + onPageSelect={handlePageSelect} /> <EditImportListExclusionModal isOpen={isAddImportListExclusionModalOpen} onModalClose={setAddImportListExclusionModalClosed} /> + + <ConfirmModal + isOpen={isConfirmDeleteModalOpen} + kind={kinds.DANGER} + title={translate('DeleteSelected')} + message={translate('DeleteSelectedImportListExclusionsMessageText')} + confirmLabel={translate('DeleteSelected')} + onConfirm={handleDeleteSelectedConfirmed} + onCancel={handleConfirmDeleteModalClose} + /> </PageSectionContent> </FieldSet> ); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index 1ec50526e..6a7365158 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 ImportListsExclusions from './ImportListExclusions/ImportListExclusions'; +import ImportListExclusions from './ImportListExclusions/ImportListExclusions'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; import ImportListOptions from './Options/ImportListOptions'; @@ -113,7 +113,8 @@ class ImportListSettings extends Component { onChildStateChange={this.onChildStateChange} /> - <ImportListsExclusions /> + <ImportListExclusions /> + <ManageImportListsModal isOpen={isManageImportListsOpen} onModalClose={this.onManageImportListsModalClose} diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index f3094d6c6..017467e53 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -21,7 +21,7 @@ const mapDispatchToProps = { fetchRootFolders }; -class ListsConnector extends Component { +class ImportListsConnector extends Component { // // Lifecycle @@ -51,10 +51,10 @@ class ListsConnector extends Component { } } -ListsConnector.propTypes = { +ImportListsConnector.propTypes = { fetchImportLists: PropTypes.func.isRequired, deleteImportList: PropTypes.func.isRequired, fetchRootFolders: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(ListsConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(ImportListsConnector); diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx index e518e592e..365f90f58 100644 --- a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx +++ b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx @@ -48,7 +48,6 @@ interface ImportListOptionsPageProps { function ImportListOptions(props: ImportListOptionsPageProps) { const { setChildSave, onChildStateChange } = props; - const selected = useSelector(createImportListOptionsSelector()); const { isSaving, @@ -58,7 +57,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) { error, settings, hasSettings, - } = selected; + } = useSelector(createImportListOptionsSelector()); const { listSyncLevel, listSyncTag } = settings; diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index 3af8bf9ec..a89d65208 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -1,7 +1,9 @@ import { createAction } from 'redux-actions'; +import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers'; +import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import { createThunk, handleThunks } from 'Store/thunks'; @@ -16,29 +18,26 @@ 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 BULK_DELETE_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/bulkDeleteImportListExclusions'; +export const CLEAR_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/clearImportListExclusions'; + +export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption'; export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue'; // // 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 bulkDeleteImportListExclusions = createThunk(BULK_DELETE_IMPORT_LIST_EXCLUSIONS); +export const clearImportListExclusions = createAction(CLEAR_IMPORT_LIST_EXCLUSIONS); export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION); export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => { @@ -64,6 +63,8 @@ export default { items: [], isSaving: false, saveError: null, + isDeleting: false, + deleteError: null, pendingChanges: {} }, @@ -77,16 +78,13 @@ export default { 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') + [DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion'), + [BULK_DELETE_IMPORT_LIST_EXCLUSIONS]: createBulkRemoveItemHandler(section, '/importlistexclusion/bulk') }), // @@ -94,7 +92,19 @@ export default { reducers: { [SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section), - [SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section) + [SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section), + + [CLEAR_IMPORT_LIST_EXCLUSIONS]: createClearReducer(section, { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isDeleting: false, + deleteError: null, + pendingChanges: {}, + totalPages: 0, + totalRecords: 0 + }) } }; diff --git a/frontend/src/Store/Actions/Settings/importLists.js b/frontend/src/Store/Actions/Settings/importLists.js index 3475fbd2b..13b5590bc 100644 --- a/frontend/src/Store/Actions/Settings/importLists.js +++ b/frontend/src/Store/Actions/Settings/importLists.js @@ -20,19 +20,19 @@ const section = 'settings.importLists'; // // Actions Types -export const FETCH_IMPORT_LISTS = 'settings/importlists/fetchImportLists'; -export const FETCH_IMPORT_LIST_SCHEMA = 'settings/importlists/fetchImportListSchema'; -export const SELECT_IMPORT_LIST_SCHEMA = 'settings/importlists/selectImportListSchema'; -export const SET_IMPORT_LIST_VALUE = 'settings/importlists/setImportListValue'; -export const SET_IMPORT_LIST_FIELD_VALUE = 'settings/importlists/setImportListFieldValue'; -export const SAVE_IMPORT_LIST = 'settings/importlists/saveImportList'; -export const CANCEL_SAVE_IMPORT_LIST = 'settings/importlists/cancelSaveImportList'; -export const DELETE_IMPORT_LIST = 'settings/importlists/deleteImportList'; -export const TEST_IMPORT_LIST = 'settings/importlists/testImportList'; -export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList'; -export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists'; -export const BULK_EDIT_IMPORT_LISTS = 'settings/importlists/bulkEditImportLists'; -export const BULK_DELETE_IMPORT_LISTS = 'settings/importlists/bulkDeleteImportLists'; +export const FETCH_IMPORT_LISTS = 'settings/importLists/fetchImportLists'; +export const FETCH_IMPORT_LIST_SCHEMA = 'settings/importLists/fetchImportListSchema'; +export const SELECT_IMPORT_LIST_SCHEMA = 'settings/importLists/selectImportListSchema'; +export const SET_IMPORT_LIST_VALUE = 'settings/importLists/setImportListValue'; +export const SET_IMPORT_LIST_FIELD_VALUE = 'settings/importLists/setImportListFieldValue'; +export const SAVE_IMPORT_LIST = 'settings/importLists/saveImportList'; +export const CANCEL_SAVE_IMPORT_LIST = 'settings/importLists/cancelSaveImportList'; +export const DELETE_IMPORT_LIST = 'settings/importLists/deleteImportList'; +export const TEST_IMPORT_LIST = 'settings/importLists/testImportList'; +export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportList'; +export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists'; +export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists'; +export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists'; // // Action Creators diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs index 2a9f0a9ec..704472125 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions List<ImportListExclusion> All(); PagingSpec<ImportListExclusion> Paged(PagingSpec<ImportListExclusion> pagingSpec); void Delete(int id); + void Delete(List<int> ids); ImportListExclusion Get(int id); ImportListExclusion FindByTvdbId(int tvdbId); ImportListExclusion Update(ImportListExclusion importListExclusion); @@ -41,6 +42,11 @@ namespace NzbDrone.Core.ImportLists.Exclusions _repo.Delete(id); } + public void Delete(List<int> ids) + { + _repo.DeleteMany(ids); + } + public ImportListExclusion Get(int id) { return _repo.Get(id); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 8d7d90087..3ffc97af5 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -363,10 +363,12 @@ "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRootFolder": "Delete Root Folder", "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{path}'?", + "DeleteSelected": "Delete Selected", "DeleteSelectedDownloadClients": "Delete Download Client(s)", "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?", "DeleteSelectedEpisodeFiles": "Delete Selected Episode Files", "DeleteSelectedEpisodeFilesHelpText": "Are you sure you want to delete the selected episode files?", + "DeleteSelectedImportListExclusionsMessageText": "Are you sure you want to delete the selected import list exclusions?", "DeleteSelectedImportLists": "Delete Import List(s)", "DeleteSelectedImportListsMessageText": "Are you sure you want to delete {count} selected import list(s)?", "DeleteSelectedIndexers": "Delete Indexer(s)", @@ -1787,8 +1789,8 @@ "SeasonPremieresOnly": "Season Premieres Only", "Seasons": "Seasons", "SeasonsMonitoredAll": "All", - "SeasonsMonitoredPartial": "Partial", "SeasonsMonitoredNone": "None", + "SeasonsMonitoredPartial": "Partial", "SeasonsMonitoredStatus": "Seasons Monitored", "SecretToken": "Secret Token", "Security": "Security", diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs new file mode 100644 index 000000000..c257d35ad --- /dev/null +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Sonarr.Api.V3.ImportLists +{ + public class ImportListExclusionBulkResource + { + public HashSet<int> Ids { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 58742ad79..8efb44e18 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ImportLists.Exclusions; @@ -65,9 +66,18 @@ namespace Sonarr.Api.V3.ImportLists } [RestDeleteById] - public void DeleteImportListExclusionResource(int id) + public void DeleteImportListExclusion(int id) { _importListExclusionService.Delete(id); } + + [HttpDelete("bulk")] + [Produces("application/json")] + public object DeleteImportListExclusions([FromBody] ImportListExclusionBulkResource resource) + { + _importListExclusionService.Delete(resource.Ids.ToList()); + + return new { }; + } } } From cf921480ec5bde79979327603394f9dc230bde05 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 11 Aug 2024 14:43:06 -0700 Subject: [PATCH 453/762] New: Support for releases with absolute episode number and air date --- .../ParserTests/DailyEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 6107308b5..0611355cb 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)] [TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)] [TestCase("Series Title - 30-04-2024 HDTV 1080p H264 AAC", "Series Title", 2024, 4, 30)] + [TestCase("Series On TitleClub E76 2024 08 08 1080p WEB H264-RnB96 [TJET]", "Series On TitleClub", 2024, 8, 8)] // [TestCase("", "", 0, 0, 0)] public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 54e32470e..bf21da1fd 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -192,6 +192,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9]).+?(?:s?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+)))(?:[ex](?<episode>(?<!\d+)(?:\d{1,3})(?!\d+)))", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes with absolute episode number AND airdate (TJET Wrestling) + new Regex(@"^(?<title>.+?)?[-_. ](?:e\d{2,3}(?!\d+))[-_. ](?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9]).+?(?:\[[a-z]+\])", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Single or multi episode releases with multiple titles, then season and episode numbers after the last title. (Title1 / Title2 / ... / S1E1-2 of 6) 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[ ._](?<episodecount>\d{1,2}))?\)?[ ._][\(\[]", RegexOptions.IgnoreCase | RegexOptions.Compiled), From d4bd7865f6c9a9725cceb31ca84aaaac76b2c718 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 12 Aug 2024 00:15:38 +0000 Subject: [PATCH 454/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 736c79a9e..b84561ca1 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -11050,6 +11050,10 @@ "type": "integer", "format": "int32" }, + "imdbId": { + "type": "string", + "nullable": true + }, "rejections": { "type": "array", "items": { From 9af2f137f41867a29d544fad77551672a79f24b6 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:20:25 +0300 Subject: [PATCH 455/762] Skip duplicate import list exclusions --- .../ImportLists/ImportListExclusionController.cs | 5 ++++- .../ImportLists}/ImportListExclusionExistsValidator.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) rename src/{NzbDrone.Core/ImportLists/Exclusions => Sonarr.Api.V3/ImportLists}/ImportListExclusionExistsValidator.cs (65%) diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 8efb44e18..c48604bc8 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -21,7 +21,10 @@ namespace Sonarr.Api.V3.ImportLists { _importListExclusionService = importListExclusionService; - SharedValidator.RuleFor(c => c.TvdbId).NotEmpty().SetValidator(importListExclusionExistsValidator); + SharedValidator.RuleFor(c => c.TvdbId).Cascade(CascadeMode.Stop) + .NotEmpty() + .SetValidator(importListExclusionExistsValidator); + SharedValidator.RuleFor(c => c.Title).NotEmpty(); } diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionExistsValidator.cs similarity index 65% rename from src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs rename to src/Sonarr.Api.V3/ImportLists/ImportListExclusionExistsValidator.cs index 88b4c0026..0196f169b 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionExistsValidator.cs @@ -1,6 +1,7 @@ using FluentValidation.Validators; +using NzbDrone.Core.ImportLists.Exclusions; -namespace NzbDrone.Core.ImportLists.Exclusions +namespace Sonarr.Api.V3.ImportLists { public class ImportListExclusionExistsValidator : PropertyValidator { @@ -20,7 +21,12 @@ namespace NzbDrone.Core.ImportLists.Exclusions return true; } - return !_importListExclusionService.All().Exists(s => s.TvdbId == (int)context.PropertyValue); + if (context.InstanceToValidate is not ImportListExclusionResource listExclusionResource) + { + return true; + } + + return !_importListExclusionService.All().Exists(v => v.TvdbId == (int)context.PropertyValue && v.Id != listExclusionResource.Id); } } } From 9b144e9adea33fdb6ac7254435655e3d38a2d252 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:20:58 +0300 Subject: [PATCH 456/762] New: Increase max size limit for quality definitions Closes #7084 --- frontend/src/Settings/Quality/Definition/QualityDefinition.js | 2 +- frontend/src/Store/Actions/Settings/qualityDefinitions.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js index eb49d4ca4..0d46d30dd 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -13,7 +13,7 @@ import QualityDefinitionLimits from './QualityDefinitionLimits'; import styles from './QualityDefinition.css'; const MIN = 0; -const MAX = 400; +const MAX = 1000; const MIN_DISTANCE = 1; const slider = { diff --git a/frontend/src/Store/Actions/Settings/qualityDefinitions.js b/frontend/src/Store/Actions/Settings/qualityDefinitions.js index 09317ca07..c1ac33e6a 100644 --- a/frontend/src/Store/Actions/Settings/qualityDefinitions.js +++ b/frontend/src/Store/Actions/Settings/qualityDefinitions.js @@ -77,7 +77,7 @@ export default { const promise = createAjaxRequest({ method: 'PUT', - url: '/qualityDefinition/update', + url: '/qualitydefinition/update', data: JSON.stringify(upatedDefinitions), contentType: 'application/json', dataType: 'json' From be5b449de48bb8bd810f4a64a5a06a53ac3e6605 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:22:05 +0300 Subject: [PATCH 457/762] Fixed: Don't display multiple languages if no languages were parsed --- frontend/src/Activity/History/HistoryRow.tsx | 3 +- ...isodeLanguages.js => EpisodeLanguages.tsx} | 41 +++++++------------ 2 files changed, 16 insertions(+), 28 deletions(-) rename frontend/src/Episode/{EpisodeLanguages.js => EpisodeLanguages.tsx} (60%) diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx index c24f6a286..ce4b00647 100644 --- a/frontend/src/Activity/History/HistoryRow.tsx +++ b/frontend/src/Activity/History/HistoryRow.tsx @@ -15,6 +15,7 @@ import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; import useEpisode from 'Episode/useEpisode'; import usePrevious from 'Helpers/Hooks/usePrevious'; import { icons, tooltipPositions } from 'Helpers/Props'; +import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import useSeries from 'Series/useSeries'; @@ -31,7 +32,7 @@ interface HistoryRowProps { id: number; episodeId: number; seriesId: number; - languages: object[]; + languages: Language[]; quality: QualityModel; customFormats?: CustomFormat[]; customFormatScore: number; diff --git a/frontend/src/Episode/EpisodeLanguages.js b/frontend/src/Episode/EpisodeLanguages.tsx similarity index 60% rename from frontend/src/Episode/EpisodeLanguages.js rename to frontend/src/Episode/EpisodeLanguages.tsx index 66f278897..1812d0394 100644 --- a/frontend/src/Episode/EpisodeLanguages.js +++ b/frontend/src/Episode/EpisodeLanguages.tsx @@ -1,18 +1,21 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Label from 'Components/Label'; import Popover from 'Components/Tooltip/Popover'; import { kinds, tooltipPositions } from 'Helpers/Props'; +import Language from 'Language/Language'; import translate from 'Utilities/String/translate'; -function EpisodeLanguages(props) { - const { - className, - languages, - isCutoffNotMet - } = props; +interface EpisodeLanguagesProps { + className?: string; + languages: Language[]; + isCutoffNotMet?: boolean; +} - if (!languages) { +function EpisodeLanguages(props: EpisodeLanguagesProps) { + const { className, languages, isCutoffNotMet = true } = props; + + // TODO: Typescript - Remove once everything is converted + if (!languages || languages.length === 0) { return null; } @@ -41,15 +44,9 @@ function EpisodeLanguages(props) { title={translate('Languages')} body={ <ul> - { - languages.map((language) => { - return ( - <li key={language.id}> - {language.name} - </li> - ); - }) - } + {languages.map((language) => ( + <li key={language.id}>{language.name}</li> + ))} </ul> } position={tooltipPositions.LEFT} @@ -57,14 +54,4 @@ function EpisodeLanguages(props) { ); } -EpisodeLanguages.propTypes = { - className: PropTypes.string, - languages: PropTypes.arrayOf(PropTypes.object), - isCutoffNotMet: PropTypes.bool -}; - -EpisodeLanguages.defaultProps = { - isCutoffNotMet: true -}; - export default EpisodeLanguages; From 592b6f7f7cde36d5ff2c10dcf91bc9cdbb0edf2a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:48:30 +0300 Subject: [PATCH 458/762] Fixed: Persist selected custom filter for interactive searches --- frontend/src/Store/Actions/releaseActions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index c7c8ce0e4..1c67d41b9 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -269,8 +269,9 @@ export const defaultState = { }; export const persistState = [ - 'releases.selectedFilterKey', + 'releases.episode.selectedFilterKey', 'releases.episode.customFilters', + 'releases.season.selectedFilterKey', 'releases.season.customFilters' ]; From ef829c6ace8ac372c3cb559fad6aaf983ef9e646 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 14 Aug 2024 20:22:37 -0700 Subject: [PATCH 459/762] New: Parse DarQ release group Closes #7083 --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 5d4984954..130ffdbb1 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -87,6 +87,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] + [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index bf21da1fd..9af9f8f26 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -556,7 +556,7 @@ namespace NzbDrone.Core.Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ)\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); From 12ac123d5add71926ac2dd670064b0dbc06ee963 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 13 Aug 2024 21:00:10 -0700 Subject: [PATCH 460/762] Fixed: Prefer episode runtime when determining whether a file is a sample Closes #7086 --- .../EpisodeImport/DetectSampleFixture.cs | 76 +++++++++++++----- .../NotSampleSpecificationFixture.cs | 2 +- .../MediaFiles/EpisodeImport/DetectSample.cs | 79 ++++++++++++++----- .../Specifications/NotSampleSpecification.cs | 2 +- 4 files changed, 119 insertions(+), 40 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/DetectSampleFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/DetectSampleFixture.cs index e7dd0f903..cb5e45de5 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/DetectSampleFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/DetectSampleFixture.cs @@ -39,22 +39,29 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", Episodes = episodes, Series = _series, - Quality = new QualityModel(Quality.HDTV720p) + Quality = new QualityModel(Quality.HDTV720p), }; } private void GivenRuntime(int seconds) { + var runtime = new TimeSpan(0, 0, seconds); + Mocker.GetMock<IVideoFileInfoReader>() .Setup(s => s.GetRunTime(It.IsAny<string>())) - .Returns(new TimeSpan(0, 0, seconds)); + .Returns(runtime); + + _localEpisode.MediaInfo = Builder<MediaInfoModel>.CreateNew().With(m => m.RunTime = runtime).Build(); } [Test] public void should_return_false_if_season_zero() { _localEpisode.Episodes[0].SeasonNumber = 0; - ShouldBeNotSample(); + + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } [Test] @@ -62,7 +69,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { _localEpisode.Path = @"C:\Test\some.show.s01e01.flv"; - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never()); } @@ -72,7 +81,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { _localEpisode.Path = @"C:\Test\some.show.s01e01.strm"; - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never()); } @@ -94,7 +105,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenRuntime(60); - ShouldBeSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample); } [Test] @@ -102,7 +115,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenRuntime(600); - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } [Test] @@ -111,7 +126,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _series.Runtime = 6; GivenRuntime(299); - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } [Test] @@ -120,7 +137,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _series.Runtime = 2; GivenRuntime(60); - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } [Test] @@ -129,7 +148,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _series.Runtime = 2; GivenRuntime(10); - ShouldBeSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample); } [Test] @@ -152,7 +173,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenRuntime(600); _series.SeriesType = SeriesTypes.Daily; _localEpisode.Episodes[0].SeasonNumber = 0; - ShouldBeNotSample(); + + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } [Test] @@ -161,21 +185,33 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _series.SeriesType = SeriesTypes.Anime; _localEpisode.Episodes[0].SeasonNumber = 0; - ShouldBeNotSample(); + Subject.IsSample(_localEpisode.Series, + _localEpisode.Path, + _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); } - private void ShouldBeSample() + [Test] + public void should_use_runtime_from_media_info() { - Subject.IsSample(_localEpisode.Series, - _localEpisode.Path, - _localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample); + GivenRuntime(120); + + _localEpisode.Series.Runtime = 30; + _localEpisode.Episodes.First().Runtime = 30; + + Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample); + + Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<string>()), Times.Never()); } - private void ShouldBeNotSample() + [Test] + public void should_use_runtime_from_episode_over_series() { - Subject.IsSample(_localEpisode.Series, - _localEpisode.Path, - _localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample); + GivenRuntime(120); + + _localEpisode.Series.Runtime = 5; + _localEpisode.Episodes.First().Runtime = 30; + + Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index 149239632..fb47e37db 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index b7c225eb2..e718840d1 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.IO; using NLog; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -9,6 +10,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IDetectSample { DetectSampleResult IsSample(Series series, string path, bool isSpecial); + DetectSampleResult IsSample(LocalEpisode localEpisode); } public class DetectSample : IDetectSample @@ -23,6 +25,51 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } public DetectSampleResult IsSample(Series series, string path, bool isSpecial) + { + var extensionResult = IsSample(path, isSpecial); + + if (extensionResult != DetectSampleResult.Indeterminate) + { + return extensionResult; + } + + var fileRuntime = _videoFileInfoReader.GetRunTime(path); + + if (!fileRuntime.HasValue) + { + _logger.Error("Failed to get runtime from the file, make sure ffprobe is available"); + return DetectSampleResult.Indeterminate; + } + + return IsSample(path, fileRuntime.Value, series.Runtime); + } + + public DetectSampleResult IsSample(LocalEpisode localEpisode) + { + var extensionResult = IsSample(localEpisode.Path, localEpisode.IsSpecial); + + if (extensionResult != DetectSampleResult.Indeterminate) + { + return extensionResult; + } + + var runtime = 0; + + foreach (var episode in localEpisode.Episodes) + { + runtime += episode.Runtime > 0 ? episode.Runtime : localEpisode.Series.Runtime; + } + + if (localEpisode.MediaInfo == null) + { + _logger.Error("Failed to get runtime from the file, make sure ffprobe is available"); + return DetectSampleResult.Indeterminate; + } + + return IsSample(localEpisode.Path, localEpisode.MediaInfo.RunTime, runtime); + } + + private DetectSampleResult IsSample(string path, bool isSpecial) { if (isSpecial) { @@ -44,49 +91,45 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return DetectSampleResult.NotSample; } - // TODO: Use MediaInfo from the import process, no need to re-process the file again here - var runTime = _videoFileInfoReader.GetRunTime(path); + return DetectSampleResult.Indeterminate; + } - if (!runTime.HasValue) - { - _logger.Error("Failed to get runtime from the file, make sure ffprobe is available"); - return DetectSampleResult.Indeterminate; - } + private DetectSampleResult IsSample(string path, TimeSpan fileRuntime, int expectedRuntime) + { + var minimumRuntime = GetMinimumAllowedRuntime(expectedRuntime); - var minimumRuntime = GetMinimumAllowedRuntime(series); - - if (runTime.Value.TotalMinutes.Equals(0)) + if (fileRuntime.TotalMinutes.Equals(0)) { _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path); return DetectSampleResult.Sample; } - if (runTime.Value.TotalSeconds < minimumRuntime) + if (fileRuntime.TotalSeconds < minimumRuntime) { - _logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime); + _logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, fileRuntime, minimumRuntime); return DetectSampleResult.Sample; } - _logger.Debug("[{0}] does not appear to be a sample. Runtime {1} seconds is more than minimum of {2} seconds", path, runTime, minimumRuntime); + _logger.Debug("[{0}] does not appear to be a sample. Runtime {1} seconds is more than minimum of {2} seconds", path, fileRuntime, minimumRuntime); return DetectSampleResult.NotSample; } - private int GetMinimumAllowedRuntime(Series series) + private int GetMinimumAllowedRuntime(int runtime) { // Anime short - 15 seconds - if (series.Runtime <= 3) + if (runtime <= 3) { return 15; } // Webisodes - 90 seconds - if (series.Runtime <= 10) + if (runtime <= 10) { return 90; } // 30 minute episodes - 5 minutes - if (series.Runtime <= 30) + if (runtime <= 30) { return 300; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index 7c2b2169e..5d748f0f1 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications try { - var sample = _detectSample.IsSample(localEpisode.Series, localEpisode.Path, localEpisode.IsSpecial); + var sample = _detectSample.IsSample(localEpisode); if (sample == DetectSampleResult.Sample) { From 84338f4c50331f08bf5ddf3f25d87586f6a41c82 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:24:49 +0300 Subject: [PATCH 461/762] Fixed: Stale formats score after changing quality profile for series --- frontend/src/Components/SignalRConnector.js | 2 ++ frontend/src/Series/Details/SeriesDetailsConnector.js | 2 +- frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index a928105d8..918f53fa5 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -212,6 +212,8 @@ class SignalRConnector extends Component { if (action === 'updated') { this.props.dispatchUpdateItem({ section, ...body.resource }); + + repopulatePage('seriesUpdated'); } else if (action === 'deleted') { this.props.dispatchRemoveItem({ section, id: body.resource.id }); } diff --git a/frontend/src/Series/Details/SeriesDetailsConnector.js b/frontend/src/Series/Details/SeriesDetailsConnector.js index 017ed4389..1d0acf58d 100644 --- a/frontend/src/Series/Details/SeriesDetailsConnector.js +++ b/frontend/src/Series/Details/SeriesDetailsConnector.js @@ -152,7 +152,7 @@ class SeriesDetailsConnector extends Component { // Lifecycle componentDidMount() { - registerPagePopulator(this.populate); + registerPagePopulator(this.populate, ['seriesUpdated']); this.populate(); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index ef8468403..002365f3a 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -49,7 +49,7 @@ class CutoffUnmetConnector extends Component { gotoCutoffUnmetFirstPage } = this.props; - registerPagePopulator(this.repopulate, ['episodeFileUpdated', 'episodeFileDeleted']); + registerPagePopulator(this.repopulate, ['seriesUpdated', 'episodeFileUpdated', 'episodeFileDeleted']); if (useCurrentPage) { fetchCutoffUnmet(); From dc7a16a03ae7d1f2492e7cca26de5a0ecbdde96b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 15 Aug 2024 01:14:59 +0300 Subject: [PATCH 462/762] Sort quality profiles by name in custom filters --- .../Filter/Builder/FilterBuilderRow.js | 4 +-- .../QualityProfileFilterBuilderRowValue.tsx | 30 +++++++++++++++++++ ...tyProfileFilterBuilderRowValueConnector.js | 28 ----------------- 3 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx delete mode 100644 frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index e12f8c40f..7110dddf3 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -12,7 +12,7 @@ import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValu import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; -import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue'; import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; @@ -78,7 +78,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return QualityFilterBuilderRowValueConnector; case filterBuilderValueTypes.QUALITY_PROFILE: - return QualityProfileFilterBuilderRowValueConnector; + return QualityProfileFilterBuilderRowValue; case filterBuilderValueTypes.SEASONS_MONITORED_STATUS: return SeasonsMonitoredStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx new file mode 100644 index 000000000..50036cb90 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; +import sortByProp from 'Utilities/Array/sortByProp'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createQualityProfilesSelector() { + return createSelector( + (state: AppState) => state.settings.qualityProfiles.items, + (qualityProfiles) => { + return qualityProfiles; + } + ); +} + +function QualityProfileFilterBuilderRowValue( + props: FilterBuilderRowValueProps +) { + const qualityProfiles = useSelector(createQualityProfilesSelector()); + + const tagList = qualityProfiles + .map(({ id, name }) => ({ id, name })) + .sort(sortByProp('name')); + + return <FilterBuilderRowValue {...props} tagList={tagList} />; +} + +export default QualityProfileFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js deleted file mode 100644 index 4a8b82283..000000000 --- a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.qualityProfiles, - (qualityProfiles) => { - const tagList = qualityProfiles.items.map((qualityProfile) => { - const { - id, - name - } = qualityProfile; - - return { - id, - name - }; - }); - - return { - tagList - }; - } - ); -} - -export default connect(createMapStateToProps)(FilterBuilderRowValue); From cd3a1c18ab4f8b644ddea3e6358ec23eb3069526 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Thu, 15 Aug 2024 03:23:15 +0000 Subject: [PATCH 463/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index b84561ca1..bbc5732b9 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -3301,6 +3301,37 @@ } } }, + "/api/v3/importlistexclusion/bulk": { + "delete": { + "tags": [ + "ImportListExclusion" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionBulkResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionBulkResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v3/indexer": { "get": { "tags": [ @@ -9013,6 +9044,21 @@ }, "additionalProperties": false }, + "ImportListExclusionBulkResource": { + "type": "object", + "properties": { + "ids": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "ImportListExclusionResource": { "type": "object", "properties": { From 8484a8bebac38def457d428257888b7824ffb72f Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:52:04 +0200 Subject: [PATCH 464/762] Convert First Run to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 4 +- .../FirstRun/AuthenticationRequiredModal.js | 34 --- .../FirstRun/AuthenticationRequiredModal.tsx | 27 +++ .../AuthenticationRequiredModalContent.js | 170 --------------- .../AuthenticationRequiredModalContent.tsx | 194 ++++++++++++++++++ ...enticationRequiredModalContentConnector.js | 86 -------- frontend/src/typings/inputs.ts | 6 +- 7 files changed, 228 insertions(+), 293 deletions(-) delete mode 100644 frontend/src/FirstRun/AuthenticationRequiredModal.js create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModal.tsx delete mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContent.js create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx delete mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index ddca5b2ba..ac08aa127 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -24,7 +24,9 @@ export interface DownloadClientAppState isTestingAll: boolean; } -export type GeneralAppState = AppSectionItemState<General>; +export interface GeneralAppState + extends AppSectionItemState<General>, + AppSectionSaveState {} export interface ImportListAppState extends AppSectionState<ImportList>, diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.js b/frontend/src/FirstRun/AuthenticationRequiredModal.js deleted file mode 100644 index caa855cb7..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModal(props) { - const { - isOpen - } = props; - - return ( - <Modal - size={sizes.MEDIUM} - isOpen={isOpen} - closeOnBackgroundClick={false} - onModalClose={onModalClose} - > - <AuthenticationRequiredModalContentConnector - onModalClose={onModalClose} - /> - </Modal> - ); -} - -AuthenticationRequiredModal.propTypes = { - isOpen: PropTypes.bool.isRequired -}; - -export default AuthenticationRequiredModal; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.tsx b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx new file mode 100644 index 000000000..1b4b519ce --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; + +function onModalClose() { + // No-op +} + +interface AuthenticationRequiredModalProps { + isOpen: boolean; +} + +export default function AuthenticationRequiredModal({ + isOpen, +}: AuthenticationRequiredModalProps) { + return ( + <Modal + size={sizes.MEDIUM} + isOpen={isOpen} + closeOnBackgroundClick={false} + onModalClose={onModalClose} + > + <AuthenticationRequiredModalContent /> + </Modal> + ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js deleted file mode 100644 index f3646bc96..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js +++ /dev/null @@ -1,170 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; -import Alert from 'Components/Alert'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -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 { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; -import translate from 'Utilities/String/translate'; -import styles from './AuthenticationRequiredModalContent.css'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModalContent(props) { - const { - isPopulated, - error, - isSaving, - settings, - onInputChange, - onSavePress, - dispatchFetchStatus - } = props; - - const { - authenticationMethod, - authenticationRequired, - username, - password, - passwordConfirmation - } = settings; - - const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; - - const didMount = useRef(false); - - useEffect(() => { - if (!isSaving && didMount.current) { - dispatchFetchStatus(); - } - - didMount.current = true; - }, [isSaving, dispatchFetchStatus]); - - return ( - <ModalContent - showCloseButton={false} - onModalClose={onModalClose} - > - <ModalHeader> - {translate('AuthenticationRequired')} - </ModalHeader> - - <ModalBody> - <Alert - className={styles.authRequiredAlert} - kind={kinds.WARNING} - > - {translate('AuthenticationRequiredWarning')} - </Alert> - - { - isPopulated && !error ? - <div> - <FormGroup> - <FormLabel>{translate('AuthenticationMethod')}</FormLabel> - - <FormInputGroup - type={inputTypes.SELECT} - name="authenticationMethod" - values={authenticationMethodOptions} - helpText={translate('AuthenticationMethodHelpText')} - helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined} - helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication" - onChange={onInputChange} - {...authenticationMethod} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('AuthenticationRequired')}</FormLabel> - - <FormInputGroup - type={inputTypes.SELECT} - name="authenticationRequired" - values={authenticationRequiredOptions} - helpText={translate('AuthenticationRequiredHelpText')} - onChange={onInputChange} - {...authenticationRequired} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('Username')}</FormLabel> - - <FormInputGroup - type={inputTypes.TEXT} - name="username" - onChange={onInputChange} - helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')} - {...username} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('Password')}</FormLabel> - - <FormInputGroup - type={inputTypes.PASSWORD} - name="password" - onChange={onInputChange} - helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')} - {...password} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('PasswordConfirmation')}</FormLabel> - - <FormInputGroup - type={inputTypes.PASSWORD} - name="passwordConfirmation" - onChange={onInputChange} - helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')} - {...passwordConfirmation} - /> - </FormGroup> - </div> : - null - } - - { - !isPopulated && !error ? <LoadingIndicator /> : null - } - </ModalBody> - - <ModalFooter> - <SpinnerButton - kind={kinds.PRIMARY} - isSpinning={isSaving} - isDisabled={!authenticationEnabled} - onPress={onSavePress} - > - {translate('Save')} - </SpinnerButton> - </ModalFooter> - </ModalContent> - ); -} - -AuthenticationRequiredModalContent.propTypes = { - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default AuthenticationRequiredModalContent; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx new file mode 100644 index 000000000..092406aaf --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx @@ -0,0 +1,194 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +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 { + authenticationMethodOptions, + authenticationRequiredOptions, +} from 'Settings/General/SecuritySettings'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchGeneralSettings, + saveGeneralSettings, + setGeneralSettingsValue, +} from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './AuthenticationRequiredModalContent.css'; + +const SECTION = 'general'; + +const selector = createSettingsSectionSelector(SECTION); + +function onModalClose() { + // No-op +} + +export default function AuthenticationRequiredModalContent() { + const { isPopulated, error, isSaving, settings } = useSelector(selector); + const dispatch = useDispatch(); + + const { + authenticationMethod, + authenticationRequired, + username, + password, + passwordConfirmation, + } = settings; + + const wasSaving = usePrevious(isSaving); + + useEffect(() => { + dispatch(fetchGeneralSettings()); + + return () => { + dispatch(clearPendingChanges()); + }; + }, [dispatch]); + + const onInputChange = useCallback( + (args: InputChanged) => { + // @ts-expect-error Actions aren't typed + dispatch(setGeneralSettingsValue(args)); + }, + [dispatch] + ); + + const authenticationEnabled = + authenticationMethod && authenticationMethod.value !== 'none'; + + useEffect(() => { + if (isSaving || !wasSaving) { + return; + } + + dispatch(fetchStatus()); + }, [isSaving, wasSaving, dispatch]); + + const onPress = useCallback(() => { + dispatch(saveGeneralSettings()); + }, [dispatch]); + + return ( + <ModalContent showCloseButton={false} onModalClose={onModalClose}> + <ModalHeader>{translate('AuthenticationRequired')}</ModalHeader> + + <ModalBody> + <Alert className={styles.authRequiredAlert} kind={kinds.WARNING}> + {translate('AuthenticationRequiredWarning')} + </Alert> + + {isPopulated && !error ? ( + <div> + <FormGroup> + <FormLabel>{translate('AuthenticationMethod')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="authenticationMethod" + values={authenticationMethodOptions} + helpText={translate('AuthenticationMethodHelpText')} + helpTextWarning={ + authenticationMethod.value === 'none' + ? translate('AuthenticationMethodHelpTextWarning') + : undefined + } + helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication" + onChange={onInputChange} + {...authenticationMethod} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('AuthenticationRequired')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="authenticationRequired" + values={authenticationRequiredOptions} + helpText={translate('AuthenticationRequiredHelpText')} + onChange={onInputChange} + {...authenticationRequired} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('Username')}</FormLabel> + + <FormInputGroup + type={inputTypes.TEXT} + name="username" + helpTextWarning={ + username?.value + ? undefined + : translate('AuthenticationRequiredUsernameHelpTextWarning') + } + onChange={onInputChange} + {...username} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('Password')}</FormLabel> + + <FormInputGroup + type={inputTypes.PASSWORD} + name="password" + helpTextWarning={ + password?.value + ? undefined + : translate('AuthenticationRequiredPasswordHelpTextWarning') + } + onChange={onInputChange} + {...password} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('PasswordConfirmation')}</FormLabel> + + <FormInputGroup + type={inputTypes.PASSWORD} + name="passwordConfirmation" + helpTextWarning={ + passwordConfirmation?.value + ? undefined + : translate( + 'AuthenticationRequiredPasswordConfirmationHelpTextWarning' + ) + } + onChange={onInputChange} + {...passwordConfirmation} + /> + </FormGroup> + </div> + ) : null} + + {!isPopulated && !error ? <LoadingIndicator /> : null} + </ModalBody> + + <ModalFooter> + <SpinnerButton + kind={kinds.PRIMARY} + isSpinning={isSaving} + isDisabled={!authenticationEnabled} + onPress={onPress} + > + {translate('Save')} + </SpinnerButton> + </ModalFooter> + </ModalContent> + ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js deleted file mode 100644 index 6653a9d34..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js +++ /dev/null @@ -1,86 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; - -const SECTION = 'general'; - -function createMapStateToProps() { - return createSelector( - createSettingsSectionSelector(SECTION), - (sectionSettings) => { - return { - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchClearPendingChanges: clearPendingChanges, - dispatchSetGeneralSettingsValue: setGeneralSettingsValue, - dispatchSaveGeneralSettings: saveGeneralSettings, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchFetchStatus: fetchStatus -}; - -class AuthenticationRequiredModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchGeneralSettings(); - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetGeneralSettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveGeneralSettings(); - }; - - // - // Render - - render() { - const { - dispatchClearPendingChanges, - dispatchFetchGeneralSettings, - dispatchSetGeneralSettingsValue, - dispatchSaveGeneralSettings, - ...otherProps - } = this.props; - - return ( - <AuthenticationRequiredModalContent - {...otherProps} - onInputChange={this.onInputChange} - onSavePress={this.onSavePress} - /> - ); - } -} - -AuthenticationRequiredModalContentConnector.propTypes = { - dispatchClearPendingChanges: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchSetGeneralSettingsValue: PropTypes.func.isRequired, - dispatchSaveGeneralSettings: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector); diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index c0fda305c..cf91149b6 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -1,4 +1,6 @@ -export type CheckInputChanged = { +export type InputChanged<T = unknown> = { name: string; - value: boolean; + value: T; }; + +export type CheckInputChanged = InputChanged<boolean>; From 3eca63a67c898256b711d37607f07cbabb9ed323 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:54:30 +0200 Subject: [PATCH 465/762] Convert Label to TypeScript --- frontend/src/Components/Label.js | 48 ------------------- frontend/src/Components/Label.tsx | 31 ++++++++++++ .../src/Helpers/Props/{kinds.js => kinds.ts} | 4 +- .../src/Helpers/Props/{sizes.js => sizes.ts} | 10 +++- .../src/System/Status/DiskSpace/DiskSpace.tsx | 2 +- 5 files changed, 43 insertions(+), 52 deletions(-) delete mode 100644 frontend/src/Components/Label.js create mode 100644 frontend/src/Components/Label.tsx rename frontend/src/Helpers/Props/{kinds.js => kinds.ts} (95%) rename frontend/src/Helpers/Props/{sizes.js => sizes.ts} (66%) diff --git a/frontend/src/Components/Label.js b/frontend/src/Components/Label.js deleted file mode 100644 index 844da8165..000000000 --- a/frontend/src/Components/Label.js +++ /dev/null @@ -1,48 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { kinds, sizes } from 'Helpers/Props'; -import styles from './Label.css'; - -function Label(props) { - const { - className, - kind, - size, - outline, - children, - ...otherProps - } = props; - - return ( - <span - className={classNames( - className, - styles[kind], - styles[size], - outline && styles.outline - )} - {...otherProps} - > - {children} - </span> - ); -} - -Label.propTypes = { - className: PropTypes.string.isRequired, - title: PropTypes.string, - kind: PropTypes.oneOf(kinds.all).isRequired, - size: PropTypes.oneOf(sizes.all).isRequired, - outline: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired -}; - -Label.defaultProps = { - className: styles.label, - kind: kinds.DEFAULT, - size: sizes.SMALL, - outline: false -}; - -export default Label; diff --git a/frontend/src/Components/Label.tsx b/frontend/src/Components/Label.tsx new file mode 100644 index 000000000..411cefddf --- /dev/null +++ b/frontend/src/Components/Label.tsx @@ -0,0 +1,31 @@ +import classNames from 'classnames'; +import React, { ComponentProps, ReactNode } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import styles from './Label.css'; + +export interface LabelProps extends ComponentProps<'span'> { + kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; + size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + outline?: boolean; + children: ReactNode; +} + +export default function Label({ + className = styles.label, + kind = kinds.DEFAULT, + size = sizes.SMALL, + outline = false, + ...otherProps +}: LabelProps) { + return ( + <span + className={classNames( + className, + styles[kind], + styles[size], + outline && styles.outline + )} + {...otherProps} + /> + ); +} diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.ts similarity index 95% rename from frontend/src/Helpers/Props/kinds.js rename to frontend/src/Helpers/Props/kinds.ts index fd2c17f7b..5d4d53057 100644 --- a/frontend/src/Helpers/Props/kinds.js +++ b/frontend/src/Helpers/Props/kinds.ts @@ -19,5 +19,5 @@ export const all = [ PRIMARY, PURPLE, SUCCESS, - WARNING -]; + WARNING, +] as const; diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.ts similarity index 66% rename from frontend/src/Helpers/Props/sizes.js rename to frontend/src/Helpers/Props/sizes.ts index 6ac15f3bd..809f0397a 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.ts @@ -4,4 +4,12 @@ export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; export const EXTRA_EXTRA_LARGE = 'extraExtraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; + +export const all = [ + EXTRA_SMALL, + SMALL, + MEDIUM, + LARGE, + EXTRA_LARGE, + EXTRA_EXTRA_LARGE, +] as const; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx index 4a19cf1c9..2174e5b1e 100644 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx @@ -67,7 +67,7 @@ function DiskSpace() { const { freeSpace, totalSpace } = item; const diskUsage = 100 - (freeSpace / totalSpace) * 100; - let diskUsageKind = kinds.PRIMARY; + let diskUsageKind: (typeof kinds.all)[number] = kinds.PRIMARY; if (diskUsage > 90) { diskUsageKind = kinds.DANGER; From e92a67ad78007a7d67af7c600fafeaeb3f7f81c7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 19 Aug 2024 04:55:26 +0300 Subject: [PATCH 466/762] New: Show indicator on poster for deleted series --- frontend/src/Series/Details/SeriesDetails.css | 1 + .../src/Series/Details/SeriesDetails.css.d.ts | 1 + frontend/src/Series/Details/SeriesDetails.js | 5 +++-- .../Index/Overview/SeriesIndexOverview.css | 11 +++++++++-- .../Index/Overview/SeriesIndexOverview.css.d.ts | 2 ++ .../Index/Overview/SeriesIndexOverview.tsx | 17 ++++++++++++++--- .../Series/Index/Posters/SeriesIndexPoster.css | 11 +++++++++-- .../Index/Posters/SeriesIndexPoster.css.d.ts | 2 ++ .../Series/Index/Posters/SeriesIndexPoster.tsx | 13 ++++++++++++- .../ProgressBar/SeriesIndexProgressBar.tsx | 3 ++- .../src/Series/Index/Table/SeriesStatusCell.tsx | 3 ++- frontend/src/Series/Series.ts | 4 +++- .../Series/{SeriesStatus.js => SeriesStatus.ts} | 13 ++++++------- .../src/Utilities/Series/getProgressBarKind.ts | 3 ++- 14 files changed, 68 insertions(+), 21 deletions(-) rename frontend/src/Series/{SeriesStatus.js => SeriesStatus.ts} (65%) diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index f62568a1d..cdda349f0 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -130,6 +130,7 @@ .sizeOnDisk, .qualityProfileName, .originalLanguageName, +.statusName, .network, .links, .tags { diff --git a/frontend/src/Series/Details/SeriesDetails.css.d.ts b/frontend/src/Series/Details/SeriesDetails.css.d.ts index ea026f8de..9dbf4d792 100644 --- a/frontend/src/Series/Details/SeriesDetails.css.d.ts +++ b/frontend/src/Series/Details/SeriesDetails.css.d.ts @@ -24,6 +24,7 @@ interface CssExports { 'seriesNavigationButton': string; 'seriesNavigationButtons': string; 'sizeOnDisk': string; + 'statusName': string; 'tags': string; 'title': string; 'titleContainer': string; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 2871212ea..116ce5d2f 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -230,7 +230,7 @@ class SeriesDetails extends Component { } = this.state; const statusDetails = getSeriesStatusDetails(status); - const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(lastAired)}` : `${year}-`; + const runningYears = status === 'ended' ? `${year}-${getDateYear(lastAired)}` : `${year}-`; let episodeFilesCountMessage = translate('SeriesDetailsNoEpisodeFiles'); @@ -509,13 +509,14 @@ class SeriesDetails extends Component { className={styles.detailsLabel} title={statusDetails.message} size={sizes.LARGE} + kind={status === 'deleted' ? kinds.INVERSE : undefined} > <div> <Icon name={statusDetails.icon} size={17} /> - <span className={styles.qualityProfileName}> + <span className={styles.statusName}> {statusDetails.title} </span> </div> diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css index 999f15a41..f8254dda8 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css @@ -25,7 +25,7 @@ $hoverScale: 1.05; } } -.ended { +.status { position: absolute; top: 0; right: 0; @@ -34,8 +34,15 @@ $hoverScale: 1.05; height: 0; border-width: 0 25px 25px 0; border-style: solid; - border-color: transparent var(--dangerColor) transparent transparent; color: var(--white); + + &.ended { + border-color: transparent var(--dangerColor) transparent transparent; + } + + &.deleted { + border-color: transparent var(--gray) transparent transparent; + } } .info { diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts index 5dfbab8ee..7a7226805 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'actions': string; 'content': string; + 'deleted': string; 'details': string; 'ended': string; 'info': string; @@ -11,6 +12,7 @@ interface CssExports { 'overviewContainer': string; 'poster': string; 'posterContainer': string; + 'status': string; 'tags': string; 'title': string; 'titleRow': string; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx index f7d7c3b50..5be820f87 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import TextTruncate from 'react-text-truncate'; @@ -146,9 +147,19 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) { <SeriesIndexPosterSelect seriesId={seriesId} /> ) : null} - {status === 'ended' && ( - <div className={styles.ended} title={translate('Ended')} /> - )} + {status === 'ended' ? ( + <div + className={classNames(styles.status, styles.ended)} + title={translate('Ended')} + /> + ) : null} + + {status === 'deleted' ? ( + <div + className={classNames(styles.status, styles.deleted)} + title={translate('Deleted')} + /> + ) : null} <Link className={styles.link} style={elementStyle} to={link}> <SeriesPoster diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css index bc708f6cd..132f3cbe8 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css @@ -71,7 +71,7 @@ $hoverScale: 1.05; overflow: hidden; } -.ended { +.status { position: absolute; top: 0; right: 0; @@ -80,8 +80,15 @@ $hoverScale: 1.05; height: 0; border-width: 0 25px 25px 0; border-style: solid; - border-color: transparent var(--dangerColor) transparent transparent; color: var(--white); + + &.ended { + border-color: transparent var(--dangerColor) transparent transparent; + } + + &.deleted { + border-color: transparent var(--gray) transparent transparent; + } } .controls { diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts index ad1ccb597..fdd66238b 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.css.d.ts @@ -5,11 +5,13 @@ interface CssExports { 'container': string; 'content': string; 'controls': string; + 'deleted': string; 'ended': string; 'link': string; 'nextAiring': string; 'overlayTitle': string; 'posterContainer': string; + 'status': string; 'tags': string; 'tagsList': string; 'title': string; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index a5d5d4978..148fefc91 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames'; @@ -161,7 +162,17 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { </Label> {status === 'ended' ? ( - <div className={styles.ended} title={translate('Ended')} /> + <div + className={classNames(styles.status, styles.ended)} + title={translate('Ended')} + /> + ) : null} + + {status === 'deleted' ? ( + <div + className={classNames(styles.status, styles.deleted)} + title={translate('Deleted')} + /> ) : null} <Link className={styles.link} style={elementStyle} to={link}> diff --git a/frontend/src/Series/Index/ProgressBar/SeriesIndexProgressBar.tsx b/frontend/src/Series/Index/ProgressBar/SeriesIndexProgressBar.tsx index 2c2fb525e..6f710e37a 100644 --- a/frontend/src/Series/Index/ProgressBar/SeriesIndexProgressBar.tsx +++ b/frontend/src/Series/Index/ProgressBar/SeriesIndexProgressBar.tsx @@ -5,6 +5,7 @@ import { sizes } from 'Helpers/Props'; import createSeriesQueueItemsDetailsSelector, { SeriesQueueDetails, } from 'Series/Index/createSeriesQueueDetailsSelector'; +import { SeriesStatus } from 'Series/Series'; import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; import translate from 'Utilities/String/translate'; import styles from './SeriesIndexProgressBar.css'; @@ -13,7 +14,7 @@ interface SeriesIndexProgressBarProps { seriesId: number; seasonNumber?: number; monitored: boolean; - status: string; + status: SeriesStatus; episodeCount: number; episodeFileCount: number; totalEpisodeCount: number; diff --git a/frontend/src/Series/Index/Table/SeriesStatusCell.tsx b/frontend/src/Series/Index/Table/SeriesStatusCell.tsx index 7e190bc9f..8de28bdab 100644 --- a/frontend/src/Series/Index/Table/SeriesStatusCell.tsx +++ b/frontend/src/Series/Index/Table/SeriesStatusCell.tsx @@ -4,6 +4,7 @@ import Icon from 'Components/Icon'; import MonitorToggleButton from 'Components/MonitorToggleButton'; import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; import { icons } from 'Helpers/Props'; +import { SeriesStatus } from 'Series/Series'; import { getSeriesStatusDetails } from 'Series/SeriesStatus'; import { toggleSeriesMonitored } from 'Store/Actions/seriesActions'; import translate from 'Utilities/String/translate'; @@ -13,7 +14,7 @@ interface SeriesStatusCellProps { className: string; seriesId: number; monitored: boolean; - status: string; + status: SeriesStatus; isSelectMode: boolean; isSaving: boolean; component?: React.ElementType; diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index c93ccf3ff..be2215f7e 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -15,6 +15,8 @@ export type SeriesMonitor = | 'unmonitorSpecials' | 'none'; +export type SeriesStatus = 'continuing' | 'ended' | 'upcoming' | 'deleted'; + export type MonitorNewItems = 'all' | 'none'; export interface Image { @@ -86,7 +88,7 @@ interface Series extends ModelBase { seriesType: SeriesType; sortTitle: string; statistics: Statistics; - status: string; + status: SeriesStatus; tags: number[]; title: string; titleSlug: string; diff --git a/frontend/src/Series/SeriesStatus.js b/frontend/src/Series/SeriesStatus.ts similarity index 65% rename from frontend/src/Series/SeriesStatus.js rename to frontend/src/Series/SeriesStatus.ts index da3bc68c3..e6174740c 100644 --- a/frontend/src/Series/SeriesStatus.js +++ b/frontend/src/Series/SeriesStatus.ts @@ -1,32 +1,31 @@ - import { icons } from 'Helpers/Props'; +import { SeriesStatus } from 'Series/Series'; import translate from 'Utilities/String/translate'; -export function getSeriesStatusDetails(status) { - +export function getSeriesStatusDetails(status: SeriesStatus) { let statusDetails = { icon: icons.SERIES_CONTINUING, title: translate('Continuing'), - message: translate('ContinuingSeriesDescription') + message: translate('ContinuingSeriesDescription'), }; if (status === 'deleted') { statusDetails = { icon: icons.SERIES_DELETED, title: translate('Deleted'), - message: translate('DeletedSeriesDescription') + message: translate('DeletedSeriesDescription'), }; } else if (status === 'ended') { statusDetails = { icon: icons.SERIES_ENDED, title: translate('Ended'), - message: translate('EndedSeriesDescription') + message: translate('EndedSeriesDescription'), }; } else if (status === 'upcoming') { statusDetails = { icon: icons.SERIES_CONTINUING, title: translate('Upcoming'), - message: translate('UpcomingSeriesDescription') + message: translate('UpcomingSeriesDescription'), }; } diff --git a/frontend/src/Utilities/Series/getProgressBarKind.ts b/frontend/src/Utilities/Series/getProgressBarKind.ts index f45387024..331c39998 100644 --- a/frontend/src/Utilities/Series/getProgressBarKind.ts +++ b/frontend/src/Utilities/Series/getProgressBarKind.ts @@ -1,7 +1,8 @@ import { kinds } from 'Helpers/Props'; +import { SeriesStatus } from 'Series/Series'; function getProgressBarKind( - status: string, + status: SeriesStatus, monitored: boolean, progress: number, isDownloading: boolean From ee693517339c9817e2e472bccf76de38e94d8763 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:47:49 +0300 Subject: [PATCH 467/762] Fixed: Switch to series rating for Discord notifications --- src/NzbDrone.Core/Notifications/Discord/Discord.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 08d21ca0c..266329c67 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -77,7 +77,7 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordGrabFieldType.Rating: discordField.Name = "Rating"; - discordField.Value = episodes.First().Ratings.Value.ToString(); + discordField.Value = series.Ratings.Value.ToString(CultureInfo.InvariantCulture); break; case DiscordGrabFieldType.Genres: discordField.Name = "Genres"; @@ -180,7 +180,7 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordImportFieldType.Rating: discordField.Name = "Rating"; - discordField.Value = episodes.First().Ratings.Value.ToString(); + discordField.Value = series.Ratings.Value.ToString(CultureInfo.InvariantCulture); break; case DiscordImportFieldType.Genres: discordField.Name = "Genres"; @@ -286,7 +286,7 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordImportFieldType.Rating: discordField.Name = "Rating"; - discordField.Value = episodes.First().Ratings.Value.ToString(); + discordField.Value = series.Ratings.Value.ToString(CultureInfo.InvariantCulture); break; case DiscordImportFieldType.Genres: discordField.Name = "Genres"; @@ -571,7 +571,7 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordManualInteractionFieldType.Rating: discordField.Name = "Rating"; - discordField.Value = episodes.FirstOrDefault()?.Ratings?.Value.ToString(CultureInfo.InvariantCulture); + discordField.Value = series?.Ratings?.Value.ToString(CultureInfo.InvariantCulture); break; case DiscordManualInteractionFieldType.Genres: discordField.Name = "Genres"; From 093a239e77ddfb4ec6281be673743b39812b6641 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 19 Aug 2024 01:52:10 +0000 Subject: [PATCH 468/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Gabriel Markowski <gmarkowski62@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: YangForever88 <1026097197@qq.com> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 10 +++-- src/NzbDrone.Core/Localization/Core/pl.json | 38 ++++++++++++++++++- .../Localization/Core/pt_BR.json | 6 ++- src/NzbDrone.Core/Localization/Core/ro.json | 3 +- .../Localization/Core/zh_CN.json | 6 +-- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 687303ac5..f57b48844 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1401,7 +1401,7 @@ "NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc", "NotificationsEmailSettingsName": "E-mail", "NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario", - "NotificationsEmbySettingsSendNotificationsHelpText": "Hace que Emby envíe notificaciones a los proveedores configurados. No soportado en Jellyfin.", + "NotificationsEmbySettingsSendNotificationsHelpText": "Hacer que Emby envíe notificaciones a los proveedores configurados. No soportado en Jellyfin.", "NotificationsGotifySettingsAppToken": "Token de app", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje", "NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo", @@ -1839,7 +1839,7 @@ "Titles": "Títulos", "ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar", "TotalFileSize": "Tamaño total de archivo", - "UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización: {version}", + "UpdateAvailableHealthCheckMessage": "Una nueva actualización está disponible: {version}", "UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado", "UrlBase": "URL base", "UseSsl": "Usar SSL", @@ -1919,7 +1919,7 @@ "NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord", "NotificationsEmailSettingsCcAddress": "Dirección(es) CC", "NotificationsEmbySettingsSendNotifications": "Enviar notificaciones", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Actualiza biblioteca al importar, renombrar o borrar", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Actualiza la biblioteca al 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", @@ -2100,5 +2100,7 @@ "NoBlocklistItems": "Ningún elemento en la lista de bloqueo", "SeasonsMonitoredPartial": "Parcial", "NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones", - "NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos" + "NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos", + "DeleteSelected": "Borrar seleccionados", + "DeleteSelectedImportListExclusionsMessageText": "¿Estás seguro que quieres borrar las exclusiones de listas de importación seleccionadas?" } diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 416bb34ac..3949ac8ad 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -18,7 +18,7 @@ "AddAutoTagError": "Nie można dodać nowego tagu automatycznego, spróbuj ponownie.", "AddConditionError": "Nie można dodać nowego warunku, spróbuj ponownie.", "AddConnection": "Dodaj połączenie", - "AddCustomFilter": "Dodaj spersonalizowany filtr", + "AddCustomFilter": "Dodaj niestandardowy filtr", "Close": "Zamknij", "AddDelayProfile": "Dodaj profil opóźnienia", "AddDownloadClient": "Dodaj klienta pobierania", @@ -42,5 +42,39 @@ "AddNewSeriesError": "Nie udało się załadować wyników wyszukiwania, spróbuj ponownie.", "AddConditionImplementation": "Dodaj condition - {implementationName}", "AddConnectionImplementation": "Dodaj Connection - {implementationName}", - "AddDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}" + "AddDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}", + "AbsoluteEpisodeNumber": "Absolutny Numer Odcinka", + "AddImportList": "Dodaj listę importu", + "AddQualityProfileError": "Nie udało się dodać nowego profilu jakości, spróbuj później.", + "AddReleaseProfile": "Dodaj Profil Wydania", + "AddRemotePathMapping": "Dodaj mapowanie ścieżek zdalnych", + "AuthenticationMethod": "Metoda Autoryzacji", + "AuthenticationMethodHelpTextWarning": "Wybierz prawidłową metodę autoryzacji", + "CutoffUnmet": "Odcięcie niespełnione", + "AgeWhenGrabbed": "Wiek (przy złapaniu)", + "AppDataDirectory": "Katalog AppData", + "BindAddressHelpText": "Prawidłowy adres IP, localhost lub '*' dla wszystkich interfejsów", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Używasz Dockera; klient pobierania {downloadClientName} umieszcza pobrane pliki w {path}, lecz nie jest to poprawna ścieżka {osName}. Sprawdź mapowanie ścieżek zdalnych i ustawienia klienta pobierania.", + "YesterdayAt": "Wczoraj o {time}", + "UpdateMechanismHelpText": "Użyj wbudowanego aktualizatora {appName} lub skryptu", + "AuthenticationRequired": "Wymagana Autoryzacja", + "AudioLanguages": "Języki Dźwięku", + "RemoveFromDownloadClient": "Usuń z Klienta Pobierania", + "AddANewPath": "Dodaj nową ścieżkę", + "Absolute": "Absolutny", + "AddImportListImplementation": "Dodaj Listę Importu - {implementationName}", + "AddNotificationError": "Nie udało się dodać nowego powiadomienia, spróbuj później.", + "AddNewSeriesSearchForMissingEpisodes": "Zacznij szukać brakujących odcinków", + "AddQualityProfile": "Dodaj profil jakości", + "AppUpdated": "{appName} Zaktualizowany", + "CalendarOptions": "Opcje kalendarza", + "AddNewSeries": "Dodaj nowy serial", + "DeleteIndexerMessageText": "Czy na pewno chcesz usunąć indeksator „{name}”?", + "DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{name}”?", + "DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{name}”?", + "AddDelayProfileError": "Nie można dodać nowego profilu opóźnienia, spróbuj później.", + "AddIndexerImplementation": "Dodaj indeks - {implementationName}", + "AddNewSeriesHelpText": "Latwo dodać nowy serial, po prostu zacznij pisać nazwę serialu który chcesz dodać.", + "Any": "Dowolny", + "StartupDirectory": "Katalog Startowy" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 55c078246..fff64bcb6 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -51,7 +51,7 @@ "SizeOnDisk": "Tamanho no disco", "SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido", "Unmonitored": "Não monitorado", - "UpdateAvailableHealthCheckMessage": "Nova atualização disponível: {version}", + "UpdateAvailableHealthCheckMessage": "Nova atualização está disponível: {version}", "Added": "Adicionado", "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "RemoveCompletedDownloads": "Remover downloads concluídos", @@ -2100,5 +2100,7 @@ "SeasonsMonitoredStatus": "Temporadas monitoradas", "NoBlocklistItems": "Sem itens na lista de bloqueio", "NotificationsTelegramSettingsMetadataLinks": "Links de Metadados", - "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações" + "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações", + "DeleteSelected": "Excluir Selecionado", + "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja excluir as listas de importação selecionadas das exclusões?" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 183427ba3..bd8b3b7b6 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -200,5 +200,6 @@ "WeekColumnHeader": "Antetul coloanei săptămânii", "TimeFormat": "Format ora", "CustomFilter": "Filtru personalizat", - "CustomFilters": "Filtre personalizate" + "CustomFilters": "Filtre personalizate", + "UpdateAvailableHealthCheckMessage": "O nouă versiune este disponibilă: {version}" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index fcd8dff8d..8d54b972a 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -456,8 +456,8 @@ "Custom": "自定义", "CreateGroup": "创建组", "CustomFilters": "自定义过滤器", - "CustomFormatUnknownCondition": "未知自定义格式条件 '{implementation}'", - "CustomFormatUnknownConditionOption": "未知的条件“{key}”的选项“{implementation}”", + "CustomFormatUnknownCondition": "未知自定义格式条件'{0}'", + "CustomFormatUnknownConditionOption": "未知的条件“{1}”的选项“{0}”", "CustomFormatsLoadError": "无法加载自定义格式", "CustomFormatsSettings": "自定义格式设置", "CustomFormatsSettingsSummary": "自定义格式和设置", @@ -1198,7 +1198,7 @@ "UseSeasonFolder": "使用季文件夹", "UseProxy": "使用代理", "Username": "用户名", - "UsenetDelayTime": "Usenet延时:{usenetDelay}", + "UsenetDelayTime": "Usenet延时:{0}", "UsenetDisabled": "Usenet已关闭", "UtcAirDate": "UTC 播出日期", "VersionNumber": "版本 {version}", From 84710a31bde67b6d33086fc645b7c3679c965b06 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Mon, 19 Aug 2024 03:57:04 +0200 Subject: [PATCH 469/762] New: Track Kometa metadata files Closes #6851 --- .../Kometa/FindMetadataFileFixture.cs | 76 +++++++ .../Consumers/Kometa/KometaMetadata.cs | 193 ++++++++++++++++++ .../Kometa/KometaMetadataSettings.cs | 39 ++++ 3 files changed, 308 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs create mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs create mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs new file mode 100644 index 000000000..4734d90bd --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs @@ -0,0 +1,76 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Kometa; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Kometa +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest<KometaMetadata> + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder<Series>.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Season00")] + [TestCase("Season01")] + [TestCase("Season02")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [TestCase(".jpg", MetadataType.EpisodeImage)] + public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) + { + var path = Path.Combine(_series.Path, "s01e01" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".jpg")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_not_return_metadata_if_image_file_is_a_thumb() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, "poster.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs new file mode 100644 index 000000000..4a0bd6c2f --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa +{ + public class KometaMetadata : MetadataBase<KometaMetadataSettings> + { + private readonly Logger _logger; + private readonly IMapCoversToLocal _mediaCoverService; + + public KometaMetadata(IMapCoversToLocal mediaCoverService, + Logger logger) + { + _mediaCoverService = mediaCoverService; + _logger = logger; + } + + private static readonly Regex SeriesImagesRegex = new Regex(@"^(?<type>poster)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^Season(?<season>\d{2,})\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex EpisodeImageRegex = new Regex(@"^S(?<season>\d{2,})E(?<episode>\d{2,})\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public override string Name => "Kometa"; + + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + { + if (metadataFile.Type == MetadataType.EpisodeImage) + { + return GetEpisodeImageFilename(series, episodeFile); + } + + _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(series.Path, metadataFile.RelativePath); + } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + var filename = Path.GetFileName(path); + + if (filename == null) + { + return null; + } + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + RelativePath = series.Path.GetRelativePath(path) + }; + + if (SeriesImagesRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.SeriesImage; + return metadata; + } + + var seasonMatch = SeasonImagesRegex.Match(filename); + + if (seasonMatch.Success) + { + metadata.Type = MetadataType.SeasonImage; + + var seasonNumberMatch = seasonMatch.Groups["season"].Value; + + if (int.TryParse(seasonNumberMatch, out var seasonNumber)) + { + metadata.SeasonNumber = seasonNumber; + } + else + { + return null; + } + + return metadata; + } + + if (EpisodeImageRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.EpisodeImage; + return metadata; + } + + return null; + } + + public override MetadataFileResult SeriesMetadata(Series series) + { + return null; + } + + public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + { + return null; + } + + public override List<ImageFileResult> SeriesImages(Series series) + { + if (!Settings.SeriesImages) + { + return new List<ImageFileResult>(); + } + + return ProcessSeriesImages(series).ToList(); + } + + public override List<ImageFileResult> SeasonImages(Series series, Season season) + { + if (!Settings.SeasonImages) + { + return new List<ImageFileResult>(); + } + + return ProcessSeasonImages(series, season).ToList(); + } + + public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeImages) + { + return new List<ImageFileResult>(); + } + + try + { + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + if (screenshot == null) + { + _logger.Debug("Episode screenshot not available"); + return new List<ImageFileResult>(); + } + + return new List<ImageFileResult> + { + new ImageFileResult(GetEpisodeImageFilename(series, episodeFile), screenshot.RemoteUrl) + }; + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); + + return new List<ImageFileResult>(); + } + } + + private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) + { + foreach (var image in series.Images) + { + if (image.CoverType == MediaCoverTypes.Poster) + { + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = image.CoverType + Path.GetExtension(source); + + yield return new ImageFileResult(destination, source); + } + } + } + + private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) + { + foreach (var image in season.Images) + { + if (image.CoverType == MediaCoverTypes.Poster) + { + var filename = string.Format("Season{0:00}.jpg", season.SeasonNumber); + + if (season.SeasonNumber == 0) + { + filename = "Season00.jpg"; + } + + yield return new ImageFileResult(filename, image.RemoteUrl); + } + } + } + + private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile) + { + var filename = string.Format("S{0:00}E{1:00}.jpg", episodeFile.SeasonNumber, episodeFile.Episodes.Value.FirstOrDefault()?.EpisodeNumber); + return Path.Combine(series.Path, filename); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs new file mode 100644 index 000000000..8b84954f6 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa +{ + public class KometaSettingsValidator : AbstractValidator<KometaMetadataSettings> + { + } + + public class KometaMetadataSettings : IProviderConfig + { + private static readonly KometaSettingsValidator Validator = new KometaSettingsValidator(); + + public KometaMetadataSettings() + { + SeriesImages = true; + SeasonImages = true; + EpisodeImages = true; + } + + [FieldDefinition(0, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Poster.jpg")] + public bool SeriesImages { get; set; } + + [FieldDefinition(1, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##.jpg")] + public bool SeasonImages { get; set; } + + [FieldDefinition(2, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "S##E##.jpg")] + public bool EpisodeImages { get; set; } + + public bool IsValid => true; + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} From e16ace54a8120cd98007a09fe1e6136be3e699fc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 16 Aug 2024 22:17:06 -0700 Subject: [PATCH 470/762] New: Optionally include Custom Format Score for Discord On File Import notifications --- src/NzbDrone.Core/Notifications/Discord/Discord.cs | 8 ++++++++ .../Notifications/Discord/DiscordFieldType.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 266329c67..11ae1127f 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -224,6 +224,14 @@ namespace NzbDrone.Core.Notifications.Discord discordField.Name = "Links"; discordField.Value = GetLinksString(series); break; + case DiscordImportFieldType.CustomFormats: + discordField.Name = "Custom Formats"; + discordField.Value = string.Join("|", message.EpisodeInfo.CustomFormats); + break; + case DiscordImportFieldType.CustomFormatScore: + discordField.Name = "Custom Format Score"; + discordField.Value = message.EpisodeInfo.CustomFormatScore.ToString(); + break; } if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs index d0d9e8860..4853b736a 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs @@ -31,7 +31,9 @@ namespace NzbDrone.Core.Notifications.Discord Links, Release, Poster, - Fanart + Fanart, + CustomFormats, + CustomFormatScore } public enum DiscordManualInteractionFieldType From 911a3d4c1eed8ae663c28c634186fb77fe01e47e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 16 Aug 2024 22:16:03 -0700 Subject: [PATCH 471/762] New: Parse spanish multi-episode releases --- .../ParserTests/MultiEpisodeParserFixture.cs | 1 + .../ParserTests/SingleEpisodeParserFixture.cs | 3 +++ src/NzbDrone.Core/Parser/Parser.cs | 8 ++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index 22c607198..c72873c0c 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -78,6 +78,7 @@ namespace NzbDrone.Core.Test.ParserTests [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("Series falls - Temporada 1 [HDTV][Cap.111_120]", "Series falls", 1, new[] { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 })] // [TestCase("", "", , new [] { })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index dd146b0f4..42d4782d8 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -172,6 +172,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[ReleaseGroup] SeriesTitle S01E1 Webdl 1080p", "SeriesTitle", 1, 1)] [TestCase("[SubsPlus+] Series no Chill - S02E01 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)] [TestCase("[SubsPlus+] Series no Chill - S02E01v2 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)] + [TestCase("Series - Temporada 1 - [HDTV 1080p][Cap.101](wolfmax4k.com)", "Series", 1, 1)] + [TestCase("Series [HDTV 1080p][Cap.101](wolfmax4k.com)", "Series", 1, 1)] + [TestCase("Series [HDTV 1080p][Cap. 101](wolfmax4k.com).mkv", "Series", 1, 1)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9af9f8f26..4ea515ef2 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -342,6 +342,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{4}(?![-_. ]?\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Spanish tracker releases + new Regex(@"^(?<title>.+?)(?:(?:[-_. ]+?Temporada.+?|\[.+?\])\[Cap)(?:[-_. ]+(?<season>(?<!\d+)\d{1,2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])))+(?:\])", + 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), @@ -399,10 +403,6 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Spanish tracker releases - new Regex(@"^(?<title>.+?)(?:(?:[-_. ]+?Temporada.+?|\[.+?\])\[Cap[-_.])(?<season>(?<!\d+)\d{1,2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9]))(?:\])", - 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}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From f45713bff815b2a49a5cdad4afe62a53bbdf6a6e Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 19 Aug 2024 04:58:10 +0300 Subject: [PATCH 472/762] Remove provider status on provider deletion --- .../ThingiProvider/Status/ProviderStatusRepository.cs | 6 ++++++ .../ThingiProvider/Status/ProviderStatusServiceBase.cs | 7 +------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs index de947ad21..366f42bdc 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.ThingiProvider.Status where TModel : ProviderStatusBase, new() { TModel FindByProviderId(int providerId); + void DeleteByProviderId(int providerId); } public class ProviderStatusRepository<TModel> : BasicRepository<TModel>, IProviderStatusRepository<TModel> @@ -22,5 +23,10 @@ namespace NzbDrone.Core.ThingiProvider.Status { return Query(c => c.ProviderId == providerId).SingleOrDefault(); } + + public void DeleteByProviderId(int providerId) + { + Delete(c => c.ProviderId == providerId); + } } } diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs index 6279c6e35..dc9f6e807 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs @@ -151,12 +151,7 @@ namespace NzbDrone.Core.ThingiProvider.Status public virtual void HandleAsync(ProviderDeletedEvent<TProvider> message) { - var providerStatus = _providerStatusRepository.FindByProviderId(message.ProviderId); - - if (providerStatus != null) - { - _providerStatusRepository.Delete(providerStatus); - } + _providerStatusRepository.DeleteByProviderId(message.ProviderId); } } } From aedcd046fc4fc621dae4b231cc80d4b269a69177 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 18 Aug 2024 18:58:29 -0700 Subject: [PATCH 473/762] Fixed: PWA Manifest with URL base Closes #7107 --- frontend/build/webpack.config.js | 6 ++ .../Content/Images/Icons/browserconfig.xml | 9 --- .../src/Content/Images/Icons/manifest.json | 19 ------ frontend/src/Content/browserconfig.xml | 11 ++++ frontend/src/Content/manifest.json | 19 ++++++ frontend/src/index.ejs | 4 +- frontend/src/login.html | 59 +++++++++---------- .../Frontend/Mappers/BrowserConfig.cs | 19 ++---- .../Frontend/Mappers/ManifestMapper.cs | 19 ++---- .../Frontend/Mappers/StaticResourceMapper.cs | 4 +- .../UrlBaseReplacementResourceMapperBase.cs | 58 ++++++++++++++++++ 11 files changed, 138 insertions(+), 89 deletions(-) delete mode 100644 frontend/src/Content/Images/Icons/browserconfig.xml delete mode 100644 frontend/src/Content/Images/Icons/manifest.json create mode 100644 frontend/src/Content/browserconfig.xml create mode 100644 frontend/src/Content/manifest.json create mode 100644 src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 616ee5637..85056b3cd 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -134,6 +134,12 @@ module.exports = (env) => { { source: 'frontend/src/Content/robots.txt', destination: path.join(distFolder, 'Content/robots.txt') + }, + + // manifest.json and browserconfig.xml + { + source: 'frontend/src/Content/*.(json|xml)', + destination: path.join(distFolder, 'Content') } ] } diff --git a/frontend/src/Content/Images/Icons/browserconfig.xml b/frontend/src/Content/Images/Icons/browserconfig.xml deleted file mode 100644 index 993924968..000000000 --- a/frontend/src/Content/Images/Icons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<browserconfig> - <msapplication> - <tile> - <square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/> - <TileColor>#00ccff</TileColor> - </tile> - </msapplication> -</browserconfig> diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json deleted file mode 100644 index c7bd44495..000000000 --- a/frontend/src/Content/Images/Icons/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Sonarr", - "icons": [ - { - "src": "android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": "../../../../", - "theme_color": "#3a3f51", - "background_color": "#3a3f51", - "display": "standalone" -} diff --git a/frontend/src/Content/browserconfig.xml b/frontend/src/Content/browserconfig.xml new file mode 100644 index 000000000..646112d06 --- /dev/null +++ b/frontend/src/Content/browserconfig.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> + <msapplication> + <tile> + <square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" /> + <TileColor> + #00ccff + </TileColor> + </tile> + </msapplication> +</browserconfig> diff --git a/frontend/src/Content/manifest.json b/frontend/src/Content/manifest.json new file mode 100644 index 000000000..42a38e13e --- /dev/null +++ b/frontend/src/Content/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Sonarr", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "__URL_BASE__/", + "theme_color": "#3a3f51", + "background_color": "#3a3f51", + "display": "standalone" +} diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 3f5ec6f2a..c86078033 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -33,7 +33,7 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - <link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" /> + <link rel="manifest" href="/Content/manifest.json" crossorigin="use-credentials" /> <link rel="mask-icon" href="/Content/Images/Icons/safari-pinned-tab.svg" @@ -47,7 +47,7 @@ /> <meta name="msapplication-config" - content="/Content/Images/Icons/browserconfig.xml" + content="/Content/browserconfig.xml" /> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css"> diff --git a/frontend/src/login.html b/frontend/src/login.html index 8ec1aef51..e2be93a74 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -11,8 +11,11 @@ <!-- Android/Apple Phone --> <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> - <meta name="format-detection" content="telephone=no"> + <meta + name="apple-mobile-web-app-status-bar-style" + content="black-translucent" + /> + <meta name="format-detection" content="telephone=no" /> <meta name="description" content="Sonarr" /> @@ -33,7 +36,11 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - <link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" /> + <link + rel="manifest" + href="/Content/manifest.json" + crossorigin="use-credentials" + /> <link rel="mask-icon" href="/Content/Images/Icons/safari-pinned-tab.svg" @@ -45,10 +52,7 @@ href="/favicon.ico" data-no-hash /> - <meta - name="msapplication-config" - content="/Content/Images/Icons/browserconfig.xml" - /> + <meta name="msapplication-config" content="/Content/browserconfig.xml" /> <link rel="stylesheet" type="text/css" href="/Content/styles.css" /> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" /> @@ -59,7 +63,7 @@ body { background-color: var(--pageBackground); color: var(--textColor); - font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, + font-family: 'Roboto', 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -209,9 +213,7 @@ </div> <div class="panel-body"> - <div class="sign-in"> - SIGN IN TO CONTINUE - </div> + <div class="sign-in">SIGN IN TO CONTINUE</div> <form role="form" @@ -230,8 +232,8 @@ pattern=".{1,}" required title="User name is required" - autoFocus="true" - autoCapitalize="false" + autofocus="true" + autocapitalize="false" /> </div> @@ -282,16 +284,16 @@ </body> <script type="text/javascript"> - var yearSpan = document.getElementById("year"); - yearSpan.innerHTML = "2010-" + new Date().getFullYear(); + var yearSpan = document.getElementById('year'); + yearSpan.innerHTML = '2010-' + new Date().getFullYear(); - var copyDiv = document.getElementById("copy"); - copyDiv.classList.remove("hidden"); + var copyDiv = document.getElementById('copy'); + copyDiv.classList.remove('hidden'); - if (window.location.search.indexOf("loginFailed=true") > -1) { - var loginFailedDiv = document.getElementById("login-failed"); + if (window.location.search.indexOf('loginFailed=true') > -1) { + var loginFailedDiv = document.getElementById('login-failed'); - loginFailedDiv.classList.remove("hidden"); + loginFailedDiv.classList.remove('hidden'); } var light = { @@ -311,7 +313,7 @@ primaryHoverBorderColor: '#3483e7', failedColor: '#f05050', forgotPasswordColor: '#909fa7', - forgotPasswordAltColor: '#748690' + forgotPasswordAltColor: '#748690', }; var dark = { @@ -331,21 +333,16 @@ primaryHoverBorderColor: '#3483e7', failedColor: '#f05050', forgotPasswordColor: '#737d83', - forgotPasswordAltColor: '#546067' + forgotPasswordAltColor: '#546067', }; - var theme = "_THEME_"; + var theme = '_THEME_'; var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ? - dark : - light; + var finalTheme = + theme === 'dark' || (theme === 'auto' && defaultDark) ? dark : light; Object.entries(finalTheme).forEach(([key, value]) => { - document.documentElement.style.setProperty( - `--${key}`, - value - ); + document.documentElement.style.setProperty(`--${key}`, value); }); - </script> </html> diff --git a/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs index 50eb6103f..3cb1488df 100644 --- a/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs +++ b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -6,29 +6,22 @@ using NzbDrone.Core.Configuration; namespace Sonarr.Http.Frontend.Mappers { - public class BrowserConfig : StaticResourceMapperBase + public class BrowserConfig : UrlBaseReplacementResourceMapperBase { - private readonly IAppFolderInfo _appFolderInfo; - private readonly IConfigFileProvider _configFileProvider; - public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) - : base(diskProvider, logger) + : base(diskProvider, configFileProvider, logger) { - _appFolderInfo = appFolderInfo; - _configFileProvider = configFileProvider; + FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "browserconfig.xml"); } public override string Map(string resourceUrl) { - var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); - path = path.Trim(Path.DirectorySeparatorChar); - - return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml"); + return FilePath; } public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/content/images/icons/browserconfig"); + return resourceUrl.StartsWith("/Content/browserconfig"); } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs index 4af375b97..424af1cda 100644 --- a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -6,29 +6,22 @@ using NzbDrone.Core.Configuration; namespace Sonarr.Http.Frontend.Mappers { - public class ManifestMapper : StaticResourceMapperBase + public class ManifestMapper : UrlBaseReplacementResourceMapperBase { - private readonly IAppFolderInfo _appFolderInfo; - private readonly IConfigFileProvider _configFileProvider; - public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) - : base(diskProvider, logger) + : base(diskProvider, configFileProvider, logger) { - _appFolderInfo = appFolderInfo; - _configFileProvider = configFileProvider; + FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "manifest.json"); } public override string Map(string resourceUrl) { - var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); - path = path.Trim(Path.DirectorySeparatorChar); - - return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json"); + return FilePath; } public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/Content/Images/Icons/manifest"); + return resourceUrl.StartsWith("/Content/manifest"); } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 32b8ecb42..e3dbabcb0 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -30,8 +30,8 @@ namespace Sonarr.Http.Frontend.Mappers { resourceUrl = resourceUrl.ToLowerInvariant(); - if (resourceUrl.StartsWith("/content/images/icons/manifest") || - resourceUrl.StartsWith("/content/images/icons/browserconfig")) + if (resourceUrl.StartsWith("/content/manifest") || + resourceUrl.StartsWith("/content/browserconfig")) { return false; } diff --git a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs new file mode 100644 index 000000000..c79d16464 --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs @@ -0,0 +1,58 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend.Mappers +{ + public abstract class UrlBaseReplacementResourceMapperBase : StaticResourceMapperBase + { + private readonly IDiskProvider _diskProvider; + private readonly string _urlBase; + + private string _generatedContent; + + public UrlBaseReplacementResourceMapperBase(IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _diskProvider = diskProvider; + _urlBase = configFileProvider.UrlBase; + } + + protected string FilePath; + + public override string Map(string resourceUrl) + { + return FilePath; + } + + protected override Stream GetContentStream(string filePath) + { + var text = GetFileText(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + return stream; + } + + protected virtual string GetFileText() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var text = _diskProvider.ReadAllText(FilePath); + + text = text.Replace("__URL_BASE__", _urlBase); + + _generatedContent = text; + + return _generatedContent; + } + } +} From 35baebaf7280749d5dfe5440e28b425e45a22d21 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:59:43 +0200 Subject: [PATCH 474/762] New: Configure log file size limit in UI --- .../src/Settings/General/GeneralSettings.js | 1 + .../src/Settings/General/LoggingSettings.js | 23 ++++++++++++++++++- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ .../Config/HostConfigController.cs | 2 ++ .../Config/HostConfigResource.cs | 2 ++ src/Sonarr.Api.V3/openapi.json | 4 ++++ 6 files changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index 3c8154538..e67a572e8 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -157,6 +157,7 @@ class GeneralSettings extends Component { /> <LoggingSettings + advancedSettings={advancedSettings} settings={settings} onInputChange={onInputChange} /> diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 60b651dca..2be2a8ffb 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -30,12 +30,14 @@ const logLevelOptions = [ function LoggingSettings(props) { const { + advancedSettings, settings, onInputChange } = props; const { - logLevel + logLevel, + logSizeLimit } = settings; return ( @@ -52,11 +54,30 @@ function LoggingSettings(props) { {...logLevel} /> </FormGroup> + + <FormGroup + advancedSettings={advancedSettings} + isAdvanced={true} + > + <FormLabel>{translate('LogSizeLimit')}</FormLabel> + + <FormInputGroup + type={inputTypes.NUMBER} + name="logSizeLimit" + min={1} + max={10} + unit="MB" + helpText={translate('LogSizeLimitHelpText')} + onChange={onInputChange} + {...logSizeLimit} + /> + </FormGroup> </FieldSet> ); } LoggingSettings.propTypes = { + advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 3ffc97af5..d3cfbebeb 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1097,6 +1097,8 @@ "LogLevel": "Log Level", "LogLevelTraceHelpTextWarning": "Trace logging should only be enabled temporarily", "LogOnly": "Log Only", + "LogSizeLimit": "Log Size Limit", + "LogSizeLimitHelpText": "Maximum log file size in MB before archiving. Default is 1MB.", "Logging": "Logging", "Logout": "Logout", "Logs": "Logs", diff --git a/src/Sonarr.Api.V3/Config/HostConfigController.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs index 38fab24e7..3aea78c45 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigController.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -61,6 +61,8 @@ namespace Sonarr.Api.V3.Config .Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password") .When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.LogSizeLimit).InclusiveBetween(1, 10); + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'main' is the default"); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs index 1b400800b..f03bdce33 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.Config public string Password { get; set; } public string PasswordConfirmation { get; set; } public string LogLevel { get; set; } + public int LogSizeLimit { get; set; } public string ConsoleLogLevel { get; set; } public string Branch { get; set; } public string ApiKey { get; set; } @@ -65,6 +66,7 @@ namespace Sonarr.Api.V3.Config // Username // Password LogLevel = model.LogLevel, + LogSizeLimit = model.LogSizeLimit, ConsoleLogLevel = model.ConsoleLogLevel, Branch = model.Branch, ApiKey = model.ApiKey, diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index bbc5732b9..eb6e539e2 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8858,6 +8858,10 @@ "type": "string", "nullable": true }, + "logSizeLimit": { + "type": "integer", + "nullable": true + }, "consoleLogLevel": { "type": "string", "nullable": true From 47a05ecb36e5c960b4f6ca7d279df7c281114611 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:13:20 +0300 Subject: [PATCH 475/762] Use autoprefixer in UI build --- frontend/postcss.config.js | 1 + package.json | 8 +-- yarn.lock | 110 +++++++++++++++++++++++++++---------- 3 files changed, 85 insertions(+), 34 deletions(-) diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index f657adf28..89db00f8c 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,6 +16,7 @@ const mixinsFiles = [ module.exports = { plugins: [ + 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/package.json b/package.json index 20f467965..523b704bc 100644 --- a/package.json +++ b/package.json @@ -102,11 +102,11 @@ "@types/webpack-livereload-plugin": "^2.3.3", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", - "autoprefixer": "10.4.14", + "autoprefixer": "10.4.20", "babel-loader": "9.1.2", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.37.0", + "core-js": "3.38.0", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", "eslint": "8.57.0", @@ -123,11 +123,11 @@ "html-webpack-plugin": "5.5.1", "loader-utils": "^3.2.1", "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.38", + "postcss": "8.4.41", "postcss-color-function": "4.1.0", "postcss-loader": "7.3.0", "postcss-mixins": "9.0.4", - "postcss-nested": "6.0.1", + "postcss-nested": "6.2.0", "postcss-simple-vars": "7.0.1", "postcss-url": "10.1.3", "prettier": "2.8.8", diff --git a/yarn.lock b/yarn.lock index 3353b7d14..3b3f6be13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2114,16 +2114,16 @@ async@^3.2.4: resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== -autoprefixer@10.4.14: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== +autoprefixer@10.4.20: + version "10.4.20" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.0" + picocolors "^1.0.1" postcss-value-parser "^4.2.0" available-typed-arrays@^1.0.7: @@ -2259,7 +2259,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, browserslist@^4.21.5, browserslist@^4.22.2, browserslist@^4.23.0: +browserslist@^4.14.5, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -2269,6 +2269,16 @@ browserslist@^4.14.5, browserslist@^4.21.5, browserslist@^4.22.2, browserslist@^ node-releases "^2.0.14" update-browserslist-db "^1.0.13" +browserslist@^4.23.3: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -2335,10 +2345,10 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001587: - version "1.0.30001611" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz#4dbe78935b65851c2d2df1868af39f709a93a96e" - integrity sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q== +caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001646: + version "1.0.30001651" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" + integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -2547,10 +2557,10 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: dependencies: browserslist "^4.23.0" -core-js@3.37.0: - version "3.37.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb" - integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug== +core-js@3.38.0: + version "3.38.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.0.tgz#8acb7c050bf2ccbb35f938c0d040132f6110f636" + integrity sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug== core-js@^1.0.0: version "1.2.7" @@ -2911,6 +2921,11 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290" integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA== +electron-to-chromium@^1.5.4: + version "1.5.9" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.9.tgz#3e92950e3a409d109371b7a153a9c5712e2509fd" + integrity sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA== + element-class@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" @@ -3112,7 +3127,7 @@ es6-promise@^4.2.8: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== -escalade@^3.1.1: +escalade@^3.1.1, escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== @@ -3532,7 +3547,7 @@ fork-ts-checker-webpack-plugin@8.0.0: semver "^7.3.5" tapable "^2.2.1" -fraction.js@^4.2.0: +fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== @@ -4866,6 +4881,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -5131,6 +5151,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -5250,12 +5275,12 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== +postcss-nested@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== dependencies: - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.1.1" postcss-resolve-nested-selector@^0.1.1: version "0.1.1" @@ -5267,7 +5292,7 @@ postcss-safe-parser@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.16" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== @@ -5275,6 +5300,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-simple-vars@7.0.1, postcss-simple-vars@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz#836b3097a54dcd13dbd3c36a5dbdd512fad2954c" @@ -5305,13 +5338,13 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.38, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== +postcss@8.4.41: + version "8.4.41" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681" + integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== dependencies: nanoid "^3.3.7" - picocolors "^1.0.0" + picocolors "^1.0.1" source-map-js "^1.2.0" postcss@^6.0.23: @@ -5323,6 +5356,15 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -6888,6 +6930,14 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" From aa488019cf724a2995699796054a0cc82addc3ac Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:22:51 +0300 Subject: [PATCH 476/762] Bump babel packages --- package.json | 14 +- yarn.lock | 1370 +++++++++++++++++++++++++++----------------------- 2 files changed, 752 insertions(+), 632 deletions(-) diff --git a/package.json b/package.json index 523b704bc..cc098051c 100644 --- a/package.json +++ b/package.json @@ -84,13 +84,13 @@ "typescript": "5.1.6" }, "devDependencies": { - "@babel/core": "7.24.4", - "@babel/eslint-parser": "7.24.1", - "@babel/plugin-proposal-export-default-from": "7.24.1", + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.25.1", + "@babel/plugin-proposal-export-default-from": "7.24.7", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.24.4", - "@babel/preset-react": "7.24.1", - "@babel/preset-typescript": "7.24.1", + "@babel/preset-env": "7.25.3", + "@babel/preset-react": "7.24.7", + "@babel/preset-typescript": "7.24.7", "@types/lodash": "4.14.194", "@types/qs": "6.9.15", "@types/react-document-title": "2.0.9", @@ -103,7 +103,7 @@ "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "autoprefixer": "10.4.20", - "babel-loader": "9.1.2", + "babel-loader": "9.1.3", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", "core-js": "3.38.0", diff --git a/yarn.lock b/yarn.lock index 3b3f6be13..189a159cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,7 +20,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7": version "7.24.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== @@ -28,47 +28,60 @@ "@babel/highlight" "^7.24.2" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== -"@babel/core@7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" - integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== +"@babel/compat-data@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" + integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== + +"@babel/core@7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.24.2" - "@babel/generator" "^7.24.4" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.24.4" - "@babel/parser" "^7.24.4" - "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.1" - "@babel/types" "^7.24.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz#e27eee93ed1d271637165ef3a86e2b9332395c32" - integrity sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ== +"@babel/eslint-parser@7.25.1": + version "7.25.1" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz#469cee4bd18a88ff3edbdfbd227bd20e82aa9b82" + integrity sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.24.1", "@babel/generator@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498" - integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw== +"@babel/generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" + integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== dependencies: - "@babel/types" "^7.24.0" + "@babel/types" "^7.25.0" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" @@ -80,14 +93,22 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" - integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== dependencies: - "@babel/types" "^7.22.15" + "@babel/types" "^7.24.7" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": +"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" + integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-compilation-targets@^7.22.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== @@ -98,22 +119,31 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3" - integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ== +"@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-member-expression-to-functions" "^7.23.0" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.24.1" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": +"@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz#a109bf9c3d58dfed83aaf42e85633c89f43a6253" + integrity sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/traverse" "^7.25.0" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== @@ -122,6 +152,15 @@ regexpu-core "^5.3.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" + integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + regexpu-core "^5.3.1" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" @@ -133,134 +172,129 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== +"@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@babel/helper-member-expression-to-functions@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" - integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== +"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== dependencies: - "@babel/types" "^7.23.0" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" -"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.24.1": - version "7.24.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" - integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== dependencies: - "@babel/types" "^7.24.0" + "@babel/types" "^7.24.7" -"@babel/helper-module-transforms@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" - integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.20" - -"@babel/helper-optimise-call-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" - integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a" integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w== -"@babel/helper-remap-async-to-generator@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" - integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-wrap-function" "^7.22.20" +"@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== -"@babel/helper-replace-supers@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" - integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== +"@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" + integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-member-expression-to-functions" "^7.23.0" - "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-wrap-function" "^7.25.0" + "@babel/traverse" "^7.25.0" -"@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== +"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" + integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== dependencies: - "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/traverse" "^7.25.0" -"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" - integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" "@babel/helper-string-parser@^7.23.4": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== -"@babel/helper-wrap-function@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" - integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== - dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.15" - "@babel/types" "^7.22.19" +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== -"@babel/helpers@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" - integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== +"@babel/helper-wrap-function@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" + integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== dependencies: - "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.1" - "@babel/types" "^7.24.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/helpers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" + integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.0" "@babel/highlight@^7.24.2": version "7.24.2" @@ -272,50 +306,69 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" - integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" - integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA== +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" - integrity sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg== +"@babel/parser@^7.25.0", "@babel/parser@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" + integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/types" "^7.25.2" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz#da8261f2697f0f41b0855b91d3a20a1fbfd271d3" - integrity sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" + integrity sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.3" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz#1181d9685984c91d657b8ddf14f0487a6bab2988" - integrity sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz#cd0c583e01369ef51676bdb3d7b603e17d2b3f73" + integrity sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-proposal-export-default-from@7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.1.tgz#d242019488277c9a5a8035e5b70de54402644b89" - integrity sha512-+0hrgGGV3xyYIjOrD/bUZk/iUwOIGuoANfRfVg1cPhYBxF+TIXSEcc42DqzBICmWsnAQ+SfKedY0bj8QD+LuMg== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" + integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/plugin-syntax-export-default-from" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" + integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.7" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz#3a82a70e7cb7294ad2559465ebcb871dfbf078fb" + integrity sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.0" + +"@babel/plugin-proposal-export-default-from@7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.7.tgz#0b539c46b8ac804f694e338f803c8354c0f788b6" + integrity sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-export-default-from" "^7.24.7" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -350,12 +403,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-default-from@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.1.tgz#a92852e694910ae4295e6e51e87b83507ed5e6e8" - integrity sha512-cNXSxv9eTkGUtd0PsNMK8Yx5xeScxfpWOUAxE+ZPAXXEcAMOC3fk7LRdXq5fvpra2pLx2p1YtkAhpUbB2SwaRA== +"@babel/plugin-syntax-export-default-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.7.tgz#85dae9098933573aae137fb52141dd3ca52ae7ac" + integrity sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from@^7.8.3": version "7.8.3" @@ -364,19 +417,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-import-assertions@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz#db3aad724153a00eaac115a3fb898de544e34971" - integrity sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ== +"@babel/plugin-syntax-import-assertions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" + integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-syntax-import-attributes@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz#c66b966c63b714c4eec508fcf5763b1f2d381093" - integrity sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA== +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" + integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" @@ -392,12 +445,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" - integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== +"@babel/plugin-syntax-jsx@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -455,12 +508,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" - integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== +"@babel/plugin-syntax-typescript@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" + integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -470,457 +523,465 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz#2bf263617060c9cc45bcdbf492b8cc805082bf27" - integrity sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw== +"@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-async-generator-functions@^7.24.3": - version "7.24.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz#8fa7ae481b100768cc9842c8617808c5352b8b89" - integrity sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg== +"@babel/plugin-transform-async-generator-functions@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz#b785cf35d73437f6276b1e30439a57a50747bddf" + integrity sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q== dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-remap-async-to-generator" "^7.25.0" "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/traverse" "^7.25.0" -"@babel/plugin-transform-async-to-generator@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz#0e220703b89f2216800ce7b1c53cb0cf521c37f4" - integrity sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw== +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== dependencies: - "@babel/helper-module-imports" "^7.24.1" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" -"@babel/plugin-transform-block-scoped-functions@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz#1c94799e20fcd5c4d4589523bbc57b7692979380" - integrity sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg== +"@babel/plugin-transform-block-scoped-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-block-scoping@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012" - integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g== +"@babel/plugin-transform-block-scoping@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" + integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-class-properties@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz#bcbf1aef6ba6085cfddec9fc8d58871cf011fc29" - integrity sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g== +"@babel/plugin-transform-class-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" + integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.1" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-class-static-block@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4" - integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg== +"@babel/plugin-transform-class-static-block@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" + integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.4" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz#5bc8fc160ed96378184bc10042af47f50884dcb1" - integrity sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q== +"@babel/plugin-transform-classes@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz#63122366527d88e0ef61b612554fe3f8c793991e" + integrity sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-replace-supers" "^7.24.1" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/traverse" "^7.25.0" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz#bc7e787f8e021eccfb677af5f13c29a9934ed8a7" - integrity sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw== +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/template" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" -"@babel/plugin-transform-destructuring@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz#b1e8243af4a0206841973786292b8c8dd8447345" - integrity sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw== +"@babel/plugin-transform-destructuring@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-dotall-regex@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz#d56913d2f12795cc9930801b84c6f8c47513ac13" - integrity sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw== +"@babel/plugin-transform-dotall-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" + integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-duplicate-keys@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz#5347a797fe82b8d09749d10e9f5b83665adbca88" - integrity sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA== +"@babel/plugin-transform-duplicate-keys@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" + integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-dynamic-import@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz#2a5a49959201970dd09a5fca856cb651e44439dd" - integrity sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz#809af7e3339466b49c034c683964ee8afb3e2604" + integrity sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-regexp-features-plugin" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-dynamic-import@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" + integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz#6650ebeb5bd5c012d5f5f90a26613a08162e8ba4" - integrity sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw== +"@babel/plugin-transform-exponentiation-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" + integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-export-namespace-from@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz#f033541fc036e3efb2dcb58eedafd4f6b8078acd" - integrity sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ== +"@babel/plugin-transform-export-namespace-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" + integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-for-of@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz#67448446b67ab6c091360ce3717e7d3a59e202fd" - integrity sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg== +"@babel/plugin-transform-for-of@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" -"@babel/plugin-transform-function-name@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz#8cba6f7730626cc4dfe4ca2fa516215a0592b361" - integrity sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA== +"@babel/plugin-transform-function-name@^7.25.1": + version "7.25.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" + integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== dependencies: - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.1" -"@babel/plugin-transform-json-strings@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz#08e6369b62ab3e8a7b61089151b161180c8299f7" - integrity sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ== +"@babel/plugin-transform-json-strings@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" + integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz#0a1982297af83e6b3c94972686067df588c5c096" - integrity sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g== +"@babel/plugin-transform-literals@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" + integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-logical-assignment-operators@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz#719d8aded1aa94b8fb34e3a785ae8518e24cfa40" - integrity sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w== +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" + integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz#896d23601c92f437af8b01371ad34beb75df4489" - integrity sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg== +"@babel/plugin-transform-member-expression-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-amd@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz#b6d829ed15258536977e9c7cc6437814871ffa39" - integrity sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ== +"@babel/plugin-transform-modules-amd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" + integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-commonjs@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz#e71ba1d0d69e049a22bf90b3867e263823d3f1b9" - integrity sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw== +"@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" -"@babel/plugin-transform-modules-systemjs@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" - integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== +"@babel/plugin-transform-modules-systemjs@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz#8f46cdc5f9e5af74f3bd019485a6cbe59685ea33" + integrity sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw== dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-module-transforms" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.0" -"@babel/plugin-transform-modules-umd@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz#69220c66653a19cf2c0872b9c762b9a48b8bebef" - integrity sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg== +"@babel/plugin-transform-modules-umd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" + integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" - integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-new-target@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz#29c59988fa3d0157de1c871a28cd83096363cc34" - integrity sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug== +"@babel/plugin-transform-new-target@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" + integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-nullish-coalescing-operator@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz#0cd494bb97cb07d428bd651632cb9d4140513988" - integrity sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw== +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" + integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz#5bc019ce5b3435c1cadf37215e55e433d674d4e8" - integrity sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw== +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" + integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" - integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" + integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== dependencies: - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-parameters" "^7.24.7" -"@babel/plugin-transform-object-super@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz#e71d6ab13483cca89ed95a474f542bbfc20a0520" - integrity sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ== +"@babel/plugin-transform-object-super@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-replace-supers" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" -"@babel/plugin-transform-optional-catch-binding@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz#92a3d0efe847ba722f1a4508669b23134669e2da" - integrity sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA== +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" + integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz#26e588acbedce1ab3519ac40cc748e380c5291e6" - integrity sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg== +"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" - integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== +"@babel/plugin-transform-parameters@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-private-methods@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz#a0faa1ae87eff077e1e47a5ec81c3aef383dc15a" - integrity sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw== +"@babel/plugin-transform-private-methods@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" + integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.1" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-private-property-in-object@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz#756443d400274f8fb7896742962cc1b9f25c1f6a" - integrity sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg== +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" + integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.24.1" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz#d6a9aeab96f03749f4eebeb0b6ea8e90ec958825" - integrity sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA== +"@babel/plugin-transform-property-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-react-display-name@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz#554e3e1a25d181f040cf698b93fd289a03bfdcdb" - integrity sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw== +"@babel/plugin-transform-react-display-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" + integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-react-jsx-development@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" - integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== +"@babel/plugin-transform-react-jsx-development@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" + integrity sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ== dependencies: - "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/plugin-transform-react-jsx" "^7.24.7" -"@babel/plugin-transform-react-jsx@^7.22.5", "@babel/plugin-transform-react-jsx@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" - integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA== +"@babel/plugin-transform-react-jsx@^7.24.7": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz#e37e8ebfa77e9f0b16ba07fadcb6adb47412227a" + integrity sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.23.3" - "@babel/types" "^7.23.4" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/types" "^7.25.2" -"@babel/plugin-transform-react-pure-annotations@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz#c86bce22a53956331210d268e49a0ff06e392470" - integrity sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA== +"@babel/plugin-transform-react-pure-annotations@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" + integrity sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-regenerator@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz#625b7545bae52363bdc1fbbdc7252b5046409c8c" - integrity sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw== +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" + integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz#8de729f5ecbaaf5cf83b67de13bad38a21be57c1" - integrity sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg== +"@babel/plugin-transform-reserved-words@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" + integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-shorthand-properties@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz#ba9a09144cf55d35ec6b93a32253becad8ee5b55" - integrity sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA== +"@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-spread@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz#a1acf9152cbf690e4da0ba10790b3ac7d2b2b391" - integrity sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g== +"@babel/plugin-transform-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" -"@babel/plugin-transform-sticky-regex@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz#f03e672912c6e203ed8d6e0271d9c2113dc031b9" - integrity sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw== +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-template-literals@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz#15e2166873a30d8617e3e2ccadb86643d327aab7" - integrity sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g== +"@babel/plugin-transform-template-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-typeof-symbol@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz#6831f78647080dec044f7e9f68003d99424f94c7" - integrity sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA== +"@babel/plugin-transform-typeof-symbol@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-typescript@^7.24.1": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz#03e0492537a4b953e491f53f2bc88245574ebd15" - integrity sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g== +"@babel/plugin-transform-typescript@^7.24.7": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" + integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.24.4" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/plugin-syntax-typescript" "^7.24.1" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-syntax-typescript" "^7.24.7" -"@babel/plugin-transform-unicode-escapes@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz#fb3fa16676549ac7c7449db9b342614985c2a3a4" - integrity sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw== +"@babel/plugin-transform-unicode-escapes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" + integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-property-regex@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz#56704fd4d99da81e5e9f0c0c93cabd91dbc4889e" - integrity sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng== +"@babel/plugin-transform-unicode-property-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" + integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-regex@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz#57c3c191d68f998ac46b708380c1ce4d13536385" - integrity sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g== +"@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-sets-regex@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz#c1ea175b02afcffc9cf57a9c4658326625165b7f" - integrity sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA== +"@babel/plugin-transform-unicode-sets-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" + integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b" - integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A== +"@babel/preset-env@7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.3.tgz#0bf4769d84ac51d1073ab4a86f00f30a3a83c67c" + integrity sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g== dependencies: - "@babel/compat-data" "^7.24.4" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1" + "@babel/compat-data" "^7.25.2" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.3" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.0" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.0" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.0" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.24.1" - "@babel/plugin-syntax-import-attributes" "^7.24.1" + "@babel/plugin-syntax-import-assertions" "^7.24.7" + "@babel/plugin-syntax-import-attributes" "^7.24.7" "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -932,59 +993,60 @@ "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.24.1" - "@babel/plugin-transform-async-generator-functions" "^7.24.3" - "@babel/plugin-transform-async-to-generator" "^7.24.1" - "@babel/plugin-transform-block-scoped-functions" "^7.24.1" - "@babel/plugin-transform-block-scoping" "^7.24.4" - "@babel/plugin-transform-class-properties" "^7.24.1" - "@babel/plugin-transform-class-static-block" "^7.24.4" - "@babel/plugin-transform-classes" "^7.24.1" - "@babel/plugin-transform-computed-properties" "^7.24.1" - "@babel/plugin-transform-destructuring" "^7.24.1" - "@babel/plugin-transform-dotall-regex" "^7.24.1" - "@babel/plugin-transform-duplicate-keys" "^7.24.1" - "@babel/plugin-transform-dynamic-import" "^7.24.1" - "@babel/plugin-transform-exponentiation-operator" "^7.24.1" - "@babel/plugin-transform-export-namespace-from" "^7.24.1" - "@babel/plugin-transform-for-of" "^7.24.1" - "@babel/plugin-transform-function-name" "^7.24.1" - "@babel/plugin-transform-json-strings" "^7.24.1" - "@babel/plugin-transform-literals" "^7.24.1" - "@babel/plugin-transform-logical-assignment-operators" "^7.24.1" - "@babel/plugin-transform-member-expression-literals" "^7.24.1" - "@babel/plugin-transform-modules-amd" "^7.24.1" - "@babel/plugin-transform-modules-commonjs" "^7.24.1" - "@babel/plugin-transform-modules-systemjs" "^7.24.1" - "@babel/plugin-transform-modules-umd" "^7.24.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.24.1" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.1" - "@babel/plugin-transform-numeric-separator" "^7.24.1" - "@babel/plugin-transform-object-rest-spread" "^7.24.1" - "@babel/plugin-transform-object-super" "^7.24.1" - "@babel/plugin-transform-optional-catch-binding" "^7.24.1" - "@babel/plugin-transform-optional-chaining" "^7.24.1" - "@babel/plugin-transform-parameters" "^7.24.1" - "@babel/plugin-transform-private-methods" "^7.24.1" - "@babel/plugin-transform-private-property-in-object" "^7.24.1" - "@babel/plugin-transform-property-literals" "^7.24.1" - "@babel/plugin-transform-regenerator" "^7.24.1" - "@babel/plugin-transform-reserved-words" "^7.24.1" - "@babel/plugin-transform-shorthand-properties" "^7.24.1" - "@babel/plugin-transform-spread" "^7.24.1" - "@babel/plugin-transform-sticky-regex" "^7.24.1" - "@babel/plugin-transform-template-literals" "^7.24.1" - "@babel/plugin-transform-typeof-symbol" "^7.24.1" - "@babel/plugin-transform-unicode-escapes" "^7.24.1" - "@babel/plugin-transform-unicode-property-regex" "^7.24.1" - "@babel/plugin-transform-unicode-regex" "^7.24.1" - "@babel/plugin-transform-unicode-sets-regex" "^7.24.1" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.0" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoped-functions" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.25.0" + "@babel/plugin-transform-class-properties" "^7.24.7" + "@babel/plugin-transform-class-static-block" "^7.24.7" + "@babel/plugin-transform-classes" "^7.25.0" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-dotall-regex" "^7.24.7" + "@babel/plugin-transform-duplicate-keys" "^7.24.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.0" + "@babel/plugin-transform-dynamic-import" "^7.24.7" + "@babel/plugin-transform-exponentiation-operator" "^7.24.7" + "@babel/plugin-transform-export-namespace-from" "^7.24.7" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.25.1" + "@babel/plugin-transform-json-strings" "^7.24.7" + "@babel/plugin-transform-literals" "^7.25.2" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-member-expression-literals" "^7.24.7" + "@babel/plugin-transform-modules-amd" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-modules-systemjs" "^7.25.0" + "@babel/plugin-transform-modules-umd" "^7.24.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-new-target" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-object-super" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-property-literals" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-reserved-words" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-template-literals" "^7.24.7" + "@babel/plugin-transform-typeof-symbol" "^7.24.8" + "@babel/plugin-transform-unicode-escapes" "^7.24.7" + "@babel/plugin-transform-unicode-property-regex" "^7.24.7" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.10" babel-plugin-polyfill-corejs3 "^0.10.4" babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.31.0" + core-js-compat "^3.37.1" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -996,28 +1058,28 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.1.tgz#2450c2ac5cc498ef6101a6ca5474de251e33aa95" - integrity sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA== +"@babel/preset-react@7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" + integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-transform-react-display-name" "^7.24.1" - "@babel/plugin-transform-react-jsx" "^7.23.4" - "@babel/plugin-transform-react-jsx-development" "^7.22.5" - "@babel/plugin-transform-react-pure-annotations" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-transform-react-display-name" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.24.7" + "@babel/plugin-transform-react-jsx-development" "^7.24.7" + "@babel/plugin-transform-react-pure-annotations" "^7.24.7" -"@babel/preset-typescript@7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz#89bdf13a3149a17b3b2a2c9c62547f06db8845ec" - integrity sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ== +"@babel/preset-typescript@7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" + integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-syntax-jsx" "^7.24.1" - "@babel/plugin-transform-modules-commonjs" "^7.24.1" - "@babel/plugin-transform-typescript" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.24.7" "@babel/regjsgen@^0.8.0": version "0.8.0" @@ -1031,32 +1093,29 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15", "@babel/template@^7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" - integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== +"@babel/template@^7.24.7", "@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.24.0" - "@babel/types" "^7.24.0" + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" -"@babel/traverse@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" - integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== +"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" + integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== dependencies: - "@babel/code-frame" "^7.24.1" - "@babel/generator" "^7.24.1" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.24.1" - "@babel/types" "^7.24.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.3" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.2" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.24.0", "@babel/types@^7.4.4": +"@babel/types@^7.22.5", "@babel/types@^7.4.4": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== @@ -1065,6 +1124,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" + integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@csstools/css-parser-algorithms@^2.1.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd" @@ -2133,12 +2201,12 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -babel-loader@9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" - integrity sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA== +babel-loader@9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" + integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== dependencies: - find-cache-dir "^3.3.2" + find-cache-dir "^4.0.0" schema-utils "^4.0.0" babel-plugin-inline-classnames@2.0.1: @@ -2269,7 +2337,7 @@ browserslist@^4.14.5, browserslist@^4.22.2, browserslist@^4.23.0: node-releases "^2.0.14" update-browserslist-db "^1.0.13" -browserslist@^4.23.3: +browserslist@^4.23.1, browserslist@^4.23.3: version "4.23.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== @@ -2502,10 +2570,10 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== compress-commons@^4.1.2: version "4.1.2" @@ -2550,13 +2618,20 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -core-js-compat@^3.31.0, core-js-compat@^3.36.1: +core-js-compat@^3.36.1: version "3.37.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73" integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA== dependencies: browserslist "^4.23.0" +core-js-compat@^3.37.1: + version "3.38.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.0.tgz#d93393b1aa346b6ee683377b0c31172ccfe607aa" + integrity sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A== + dependencies: + browserslist "^4.23.3" + core-js@3.38.0: version "3.38.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.0.tgz#8acb7c050bf2ccbb35f938c0d040132f6110f636" @@ -3471,14 +3546,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== +find-cache-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" + integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" + common-path-prefix "^3.0.0" + pkg-dir "^7.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" @@ -3496,6 +3570,14 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +find-up@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + flat-cache@^3.0.4: version "3.2.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" @@ -4506,6 +4588,13 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +locate-path@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -4617,7 +4706,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.2, make-dir@~3.1.0: +make-dir@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -5037,6 +5126,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -5051,6 +5147,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -5106,6 +5209,11 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5166,13 +5274,20 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" +pkg-dir@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" + integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== + dependencies: + find-up "^6.3.0" + popper.js@^1.14.4: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -7262,6 +7377,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== + zip-stream@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" From 8af12cc4e7f71cf169392cd86ccf0eb81f6b375c Mon Sep 17 00:00:00 2001 From: kephasdev <160031725+kephasdev@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:00:55 -0400 Subject: [PATCH 477/762] Fixed: Calculating Custom Formats with languages in queue --- .../Aggregators/AggregateLanguagesFixture.cs | 83 ++++++++++++++++++- .../TrackedDownloadServiceFixture.cs | 76 +++++++++++++++++ .../Indexers/IndexerRepositoryFixture.cs | 44 ++++++++++ .../Aggregators/AggregateLanguages.cs | 13 ++- .../TrackedDownloadService.cs | 17 ++-- src/NzbDrone.Core/Indexers/IndexerFactory.cs | 8 ++ .../Indexers/IndexerRepository.cs | 9 +- 7 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs index 50d09a154..6a41dc76e 100644 --- a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.Download.Aggregation.Aggregators; using NzbDrone.Core.Indexers; @@ -65,11 +66,12 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators } [Test] - public void should_return_multi_languages_when_indexer_has_multi_languages_configuration() + public void should_return_multi_languages_when_indexer_id_has_multi_languages_configuration() { var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; var indexerDefinition = new IndexerDefinition { + Id = 1, Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } }; Mocker.GetMock<IIndexerFactory>() @@ -81,6 +83,67 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_from_indexer_with_id_when_indexer_id_and_name_are_set() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition1 = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer1", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + var indexerDefinition2 = new IndexerDefinition + { + Id = 2, + Name = "MyIndexer2", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } } + }; + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition1); + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.All()) + .Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 }); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Indexer = "MyIndexer2"; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_when_indexer_name_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer (Prowlarr)", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.FindByName("MyIndexer (Prowlarr)")) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.Indexer = "MyIndexer (Prowlarr)"; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer (Prowlarr)"), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } [Test] @@ -89,6 +152,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; var indexerDefinition = new IndexerDefinition { + Id = 1, Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } }; Mocker.GetMock<IIndexerFactory>() @@ -100,6 +164,8 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } [Test] @@ -108,6 +174,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; var indexerDefinition = new IndexerDefinition { + Id = 1, Settings = new TorrentRssIndexerSettings { } }; Mocker.GetMock<IIndexerFactory>() @@ -119,6 +186,20 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_original_when_no_indexer_value() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage }); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index 3e541c404..0c07b6386 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -7,6 +7,8 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.TorrentRss; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -84,6 +86,80 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(1); } + [Test] + public void should_set_indexer() + { + var episodeHistory = new EpisodeHistory() + { + DownloadId = "35238", + SourceTitle = "TV Series S01", + SeriesId = 5, + EpisodeId = 4, + EventType = EpisodeHistoryEventType.Grabbed, + }; + episodeHistory.Data.Add("indexer", "MyIndexer (Prowlarr)"); + Mocker.GetMock<IHistoryService>() + .Setup(s => s.FindByDownloadId(It.Is<string>(sr => sr == "35238"))) + .Returns(new List<EpisodeHistory>() + { + episodeHistory + }); + + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer (Prowlarr)", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Get(indexerDefinition.Id)) + .Returns(indexerDefinition); + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.All()) + .Returns(new List<IndexerDefinition>() { indexerDefinition }); + + var remoteEpisode = new RemoteEpisode + { + Series = new Series() { Id = 5 }, + Episodes = new List<Episode> { new Episode { Id = 4 } }, + ParsedEpisodeInfo = new ParsedEpisodeInfo() + { + SeriesTitle = "TV Series", + SeasonNumber = 1 + }, + MappedSeasonNumber = 1 + }; + + Mocker.GetMock<IParsingService>() + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null)) + .Returns(remoteEpisode); + + var client = new DownloadClientDefinition() + { + Id = 1, + Protocol = DownloadProtocol.Torrent + }; + + var item = new DownloadClientItem() + { + Title = "TV.Series.S01.MULTi.1080p.WEB.H265-RlsGroup", + DownloadId = "35238", + DownloadClientInfo = new DownloadClientItemClientInfo + { + Protocol = client.Protocol, + Id = client.Id, + Name = client.Name + } + }; + + var trackedDownload = Subject.TrackDownload(client, item); + + trackedDownload.Should().NotBeNull(); + trackedDownload.RemoteEpisode.Should().NotBeNull(); + trackedDownload.RemoteEpisode.Release.Should().NotBeNull(); + trackedDownload.RemoteEpisode.Release.Indexer.Should().Be("MyIndexer (Prowlarr)"); + } + [Test] public void should_parse_as_special_when_source_title_parsing_fails() { diff --git a/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs b/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs new file mode 100644 index 000000000..ebc8fb4c8 --- /dev/null +++ b/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs @@ -0,0 +1,44 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Indexers +{ + [TestFixture] + public class IndexerRepositoryFixture : DbTest<IndexerRepository, IndexerDefinition> + { + private void GivenIndexers() + { + var indexers = Builder<IndexerDefinition>.CreateListOfSize(2) + .All() + .With(c => c.Id = 0) + .TheFirst(1) + .With(x => x.Name = "MyIndexer (Prowlarr)") + .TheNext(1) + .With(x => x.Name = "My Second Indexer (Prowlarr)") + .BuildList(); + + Subject.InsertMany(indexers); + } + + [Test] + public void should_finds_with_name() + { + GivenIndexers(); + var found = Subject.FindByName("MyIndexer (Prowlarr)"); + found.Should().NotBeNull(); + found.Name.Should().Be("MyIndexer (Prowlarr)"); + found.Id.Should().Be(1); + } + + [Test] + public void should_not_find_with_incorrect_case_name() + { + GivenIndexers(); + var found = Subject.FindByName("myindexer (prowlarr)"); + found.Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs index 77c9c9170..2baf81907 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -76,9 +76,18 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators languages = languages.Except(languagesToRemove).ToList(); } - if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo is { IndexerId: > 0 } && releaseInfo.Title.IsNotNullOrWhiteSpace()) + if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo?.Title?.IsNotNullOrWhiteSpace() == true) { - var indexer = _indexerFactory.Get(releaseInfo.IndexerId); + IndexerDefinition indexer = null; + + if (releaseInfo is { IndexerId: > 0 }) + { + indexer = _indexerFactory.Get(releaseInfo.IndexerId); + } + else if (releaseInfo.Indexer?.IsNotNullOrWhiteSpace() == true) + { + indexer = _indexerFactory.FindByName(releaseInfo.Indexer); + } if (indexer?.Settings is IIndexerSettings settings && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(releaseInfo.Title)) { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 608221e70..bf5eda8f5 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -120,8 +120,6 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (parsedEpisodeInfo != null) { trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null); - - _aggregationService.Augment(trackedDownload.RemoteEpisode); } var downloadHistory = _downloadHistoryService.GetLatestDownloadHistoryItem(downloadItem.DownloadId); @@ -158,17 +156,24 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - if (trackedDownload.RemoteEpisode != null && - Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + if (trackedDownload.RemoteEpisode != null) { trackedDownload.RemoteEpisode.Release ??= new ReleaseInfo(); - trackedDownload.RemoteEpisode.Release.IndexerFlags = flags; + trackedDownload.RemoteEpisode.Release.Indexer = trackedDownload.Indexer; + trackedDownload.RemoteEpisode.Release.Title = trackedDownload.RemoteEpisode.ParsedEpisodeInfo?.ReleaseTitle; + + if (Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + trackedDownload.RemoteEpisode.Release.IndexerFlags = flags; + } } } - // Calculate custom formats if (trackedDownload.RemoteEpisode != null) { + _aggregationService.Augment(trackedDownload.RemoteEpisode); + + // Calculate custom formats trackedDownload.RemoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(trackedDownload.RemoteEpisode, downloadItem.TotalSize); } diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index d26ed6b56..620d36ed2 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -13,10 +13,12 @@ namespace NzbDrone.Core.Indexers List<IIndexer> RssEnabled(bool filterBlockedIndexers = true); List<IIndexer> AutomaticSearchEnabled(bool filterBlockedIndexers = true); List<IIndexer> InteractiveSearchEnabled(bool filterBlockedIndexers = true); + IndexerDefinition FindByName(string name); } public class IndexerFactory : ProviderFactory<IIndexer, IndexerDefinition>, IIndexerFactory { + private readonly IIndexerRepository _indexerRepository; private readonly IIndexerStatusService _indexerStatusService; private readonly Logger _logger; @@ -28,6 +30,7 @@ namespace NzbDrone.Core.Indexers Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { + _indexerRepository = providerRepository; _indexerStatusService = indexerStatusService; _logger = logger; } @@ -82,6 +85,11 @@ namespace NzbDrone.Core.Indexers return enabledIndexers.ToList(); } + public IndexerDefinition FindByName(string name) + { + return _indexerRepository.FindByName(name); + } + private IEnumerable<IIndexer> FilterBlockedIndexers(IEnumerable<IIndexer> indexers) { var blockedIndexers = _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); diff --git a/src/NzbDrone.Core/Indexers/IndexerRepository.cs b/src/NzbDrone.Core/Indexers/IndexerRepository.cs index c4858f415..dac9e0fb2 100644 --- a/src/NzbDrone.Core/Indexers/IndexerRepository.cs +++ b/src/NzbDrone.Core/Indexers/IndexerRepository.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; @@ -6,6 +7,7 @@ namespace NzbDrone.Core.Indexers { public interface IIndexerRepository : IProviderRepository<IndexerDefinition> { + IndexerDefinition FindByName(string name); } public class IndexerRepository : ProviderRepository<IndexerDefinition>, IIndexerRepository @@ -14,5 +16,10 @@ namespace NzbDrone.Core.Indexers : base(database, eventAggregator) { } + + public IndexerDefinition FindByName(string name) + { + return Query(i => i.Name == name).SingleOrDefault(); + } } } From 7dca9060ca4192b0f392ef392b17d2d8bd019661 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 19 Aug 2024 04:01:32 +0200 Subject: [PATCH 478/762] Convert SeriesTitleLink to TypeScript --- frontend/src/Series/SeriesTitleLink.js | 20 -------------------- frontend/src/Series/SeriesTitleLink.tsx | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) delete mode 100644 frontend/src/Series/SeriesTitleLink.js create mode 100644 frontend/src/Series/SeriesTitleLink.tsx diff --git a/frontend/src/Series/SeriesTitleLink.js b/frontend/src/Series/SeriesTitleLink.js deleted file mode 100644 index b91934a28..000000000 --- a/frontend/src/Series/SeriesTitleLink.js +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Link from 'Components/Link/Link'; - -function SeriesTitleLink({ titleSlug, title }) { - const link = `/series/${titleSlug}`; - - return ( - <Link to={link}> - {title} - </Link> - ); -} - -SeriesTitleLink.propTypes = { - titleSlug: PropTypes.string.isRequired, - title: PropTypes.string.isRequired -}; - -export default SeriesTitleLink; diff --git a/frontend/src/Series/SeriesTitleLink.tsx b/frontend/src/Series/SeriesTitleLink.tsx new file mode 100644 index 000000000..8b8b75c40 --- /dev/null +++ b/frontend/src/Series/SeriesTitleLink.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Link, { LinkProps } from 'Components/Link/Link'; + +export interface SeriesTitleLinkProps extends LinkProps { + titleSlug: string; + title: string; +} + +export default function SeriesTitleLink({ + titleSlug, + title, + ...linkProps +}: SeriesTitleLinkProps) { + const link = `/series/${titleSlug}`; + + return ( + <Link to={link} {...linkProps}> + {title} + </Link> + ); +} From ea331feb88e77f09a1df33918a0b4927c5ce87ab Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 19 Aug 2024 02:02:33 +0000 Subject: [PATCH 479/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index eb6e539e2..45de4e78c 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8860,7 +8860,7 @@ }, "logSizeLimit": { "type": "integer", - "nullable": true + "format": "int32" }, "consoleLogLevel": { "type": "string", From da7d17f5e826d5273dba0b4f73227ffc8ed8a6c7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 20 Aug 2024 14:33:44 -0700 Subject: [PATCH 480/762] Fixed: PWA Manifest images Closes #7125 --- frontend/src/Content/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Content/manifest.json b/frontend/src/Content/manifest.json index 42a38e13e..5cb58dc0d 100644 --- a/frontend/src/Content/manifest.json +++ b/frontend/src/Content/manifest.json @@ -2,12 +2,12 @@ "name": "Sonarr", "icons": [ { - "src": "android-chrome-192x192.png", + "src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "android-chrome-512x512.png", + "src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } From 14005d8d1054eafaba808337a109d5812f3e79e6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 20 Aug 2024 14:46:36 -0700 Subject: [PATCH 481/762] Fixed: Limit redirects after login to local paths --- src/Sonarr.Http/Authentication/AuthenticationController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Http/Authentication/AuthenticationController.cs b/src/Sonarr.Http/Authentication/AuthenticationController.cs index fbb9262b9..9724ff079 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationController.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationController.cs @@ -47,7 +47,7 @@ namespace Sonarr.Http.Authentication await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); - if (returnUrl.IsNullOrWhiteSpace()) + if (returnUrl.IsNullOrWhiteSpace() || !Url.IsLocalUrl(returnUrl)) { return Redirect(_configFileProvider.UrlBase + "/"); } From 860424ac22c577ec7b80d5e0a1ce61161af5bbf1 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 25 Aug 2024 09:25:18 +0000 Subject: [PATCH 482/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: Kerk en IT <info@kerkenit.nl> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 4 ++- src/NzbDrone.Core/Localization/Core/fr.json | 12 ++++++--- src/NzbDrone.Core/Localization/Core/nl.json | 27 ++++++++++++++++++- .../Localization/Core/pt_BR.json | 4 ++- src/NzbDrone.Core/Localization/Core/tr.json | 10 ++++--- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index f57b48844..0e836bf58 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2102,5 +2102,7 @@ "NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones", "NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos", "DeleteSelected": "Borrar seleccionados", - "DeleteSelectedImportListExclusionsMessageText": "¿Estás seguro que quieres borrar las exclusiones de listas de importación seleccionadas?" + "DeleteSelectedImportListExclusionsMessageText": "¿Estás seguro que quieres borrar las exclusiones de listas de importación seleccionadas?", + "LogSizeLimit": "Límite de tamaño de registro", + "LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 8394ff2d8..8cc2c0e61 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1176,7 +1176,7 @@ "Total": "Total", "Upcoming": "À venir", "UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour", - "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}", + "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}", "UpdateFiltered": "Mise à jour filtrée", "IconForSpecialsHelpText": "Afficher l'icône pour les épisodes spéciaux (saison 0)", "Ignored": "Ignoré", @@ -1724,8 +1724,8 @@ "NotificationsGotifySettingsPriorityHelpText": "Priorité de la notification", "NotificationsGotifySettingsAppTokenHelpText": "Le jeton d'application généré par Gotify", "NotificationsGotifySettingsAppToken": "Jeton d'app", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors de l'importation, du changement de nom ou de la suppression", - "NotificationsEmbySettingsSendNotificationsHelpText": "Demandez à Emby d'envoyer des notifications aux fournisseurs configurés. Non pris en charge sur Jellyfin.", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Mise à jour de la bibliothèque en cas d'importation, de renommage ou de suppression", + "NotificationsEmbySettingsSendNotificationsHelpText": "Demander à Emby d'envoyer des notifications aux fournisseurs configurés. Non supporté par Jellyfin.", "NotificationsEmbySettingsSendNotifications": "Envoyer des notifications", "NotificationsEmailSettingsServerHelpText": "Nom d'hôte ou adresse IP du serveur de courriel", "NotificationsEmailSettingsServer": "Serveur", @@ -2100,5 +2100,9 @@ "SeasonsMonitoredPartial": "Partielle", "SeasonsMonitoredNone": "Aucune", "SeasonsMonitoredStatus": "Saisons surveillées", - "NotificationsTelegramSettingsMetadataLinks": "Liens de métadonnées" + "NotificationsTelegramSettingsMetadataLinks": "Liens de métadonnées", + "LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo.", + "DeleteSelected": "Supprimer la sélection", + "LogSizeLimit": "Limite de taille du journal", + "DeleteSelectedImportListExclusionsMessageText": "Êtes-vous sûr de vouloir supprimer les exclusions de la liste d'importation sélectionnée ?" } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 7e7e23623..1da86873e 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -207,5 +207,30 @@ "ChangeCategory": "Verander categorie", "ChownGroup": "chown groep", "AutoTaggingSpecificationTag": "Tag", - "AddDelayProfileError": "Mislukt om vertragingsprofiel toe te voegen, probeer het later nog eens." + "AddDelayProfileError": "Mislukt om vertragingsprofiel toe te voegen, probeer het later nog eens.", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Schakel omleiding in als de release een score heeft die hoger is dan de geconfigureerde minimale aangepaste formaatscore", + "ConnectionSettingsUrlBaseHelpText": "Voegt een voorvoegsel toe aan de {connectionName} url, zoals {url}", + "CustomFormatsSpecificationRegularExpressionHelpText": "Aangepaste opmaak RegEx is hoofdletterongevoelig", + "CustomFormatsSpecificationRegularExpression": "Reguliere expressie", + "AutoTaggingRequiredHelpText": "Deze {implementationName} voorwaarde moet overeenkomen om de auto tagging regel toe te passen. Anders is een enkele {implementationName} voldoende.", + "BlackholeWatchFolder": "Bekijk map", + "CustomFormatsSpecificationFlag": "Vlag", + "BypassDelayIfAboveCustomFormatScore": "Omzeilen indien boven aangepaste opmaak score", + "BlocklistAndSearch": "Blokkeerlijst en zoeken", + "ChangeCategoryHint": "Verandert download naar de 'Post-Import Categorie' van Downloadclient", + "ChangeCategoryMultipleHint": "Wijzigt downloads naar de 'Post-Import Categorie' van Downloadclient", + "ClearBlocklist": "Blokkeerlijst wissen", + "Clone": "Kloon", + "BlocklistAndSearchHint": "Een vervanger zoeken na het blokkeren", + "BlocklistAndSearchMultipleHint": "Zoekopdrachten voor vervangers starten na het blokkeren van de lijst", + "BlocklistMultipleOnlyHint": "Blocklist zonder te zoeken naar vervangers", + "BlocklistOnly": "Alleen bloklijst", + "BlocklistOnlyHint": "Blokkeer lijst zonder te zoeken naar een vervanger", + "BlocklistFilterHasNoItems": "Het geselecteerde bloklijstfilter bevat geen items", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Minimumscore aangepast formaat vereist om vertraging voor het voorkeursprotocol te omzeilen", + "CountImportListsSelected": "{count} importeer lijst(en) geselecteerd", + "CustomFormatJson": "Aangepast formaat JSON", + "CustomFormatUnknownCondition": "Onbekende aangepaste formaatvoorwaarde '{implementation}'.", + "ClickToChangeIndexerFlags": "Klik om indexeringsvlaggen te wijzigen", + "CustomFormatsSettingsTriggerInfo": "Een Aangepast Formaat wordt toegepast op een uitgave of bestand als het overeenkomt met ten minste één van de verschillende condities die zijn gekozen." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index fff64bcb6..065b313f0 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2102,5 +2102,7 @@ "NotificationsTelegramSettingsMetadataLinks": "Links de Metadados", "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações", "DeleteSelected": "Excluir Selecionado", - "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja excluir as listas de importação selecionadas das exclusões?" + "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja excluir as listas de importação selecionadas das exclusões?", + "LogSizeLimit": "Limite de Tamanho do Registro", + "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB." } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index fc6919dce..b1f3d15a3 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -452,7 +452,7 @@ "NotificationsAppriseSettingsStatelessUrlsHelpText": "Bildirimin nereye gönderilmesi gerektiğini belirten, virgülle ayrılmış bir veya daha fazla URL. Kalıcı Depolama kullanılıyorsa boş bırakın.", "NotificationsDiscordSettingsOnManualInteractionFields": "Manuel Etkileşimlerde", "NotificationsEmbySettingsSendNotifications": "Bildirim Gönder", - "NotificationsEmbySettingsSendNotificationsHelpText": "MediaBrowser'ın yapılandırılmış sağlayıcılara bildirim göndermesini sağlayın", + "NotificationsEmbySettingsSendNotificationsHelpText": "Emby'nin yapılandırılmış sağlayıcılara bildirim göndermesini sağlayın. Jellyfin'de desteklenmiyor.", "NotificationsJoinSettingsDeviceIdsHelpText": "Kullanımdan kaldırıldı, bunun yerine Cihaz Adlarını kullanın. Bildirim göndermek istediğiniz Cihaz Kimliklerinin virgülle ayrılmış listesi. Ayarlanmadığı takdirde tüm cihazlar bildirim alacaktır.", "NotificationsMailgunSettingsApiKeyHelpText": "MailGun'dan oluşturulan API anahtarı", "NotificationsMailgunSettingsUseEuEndpointHelpText": "AB MailGun uç noktasını kullanmayı etkinleştirin", @@ -480,7 +480,7 @@ "NotificationsEmailSettingsCcAddressHelpText": "E-posta CC alıcılarının virgülle ayrılmış listesi", "NotificationsEmailSettingsCcAddress": "CC Adres(ler)i", "NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i", - "NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplık Güncellensin mi?", + "NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplığı Güncelleyin", "NotificationsGotifySettingsAppToken": "Uygulama Jetonu", "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", @@ -849,5 +849,9 @@ "UnableToImportAutomatically": "Otomatikman İçe Aktarılamıyor", "Any": "Herhangi", "ShowTags": "Etiketleri göster", - "ShowTagsHelpText": "Etiketleri posterin altında göster" + "ShowTagsHelpText": "Etiketleri posterin altında göster", + "DeleteSelected": "Seçileni Sil", + "LogSizeLimit": "Log Boyutu Sınırı", + "LogSizeLimitHelpText": "Arşivlemeden önce MB cinsinden maksimum log dosya boyutu. Varsayılan 1 MB'tır.", + "ProgressBarProgress": "İlerleme Çubuğu %{progress} seviyesinde" } From 45665886d6a6720a960d67ccde4e953069eb2cfc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 25 Aug 2024 16:51:07 -0700 Subject: [PATCH 483/762] Bump version to 4.0.9 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 480c12e0f..6339833c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.8 + VERSION: 4.0.9 jobs: backend: From 63b4998c8e51d0d2b8b51133cbb1fd928394a7e6 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:20:52 +0200 Subject: [PATCH 484/762] Convert Button to TypeScript --- frontend/src/Components/Link/Button.js | 54 ------------------- frontend/src/Components/Link/Button.tsx | 35 ++++++++++++ .../src/Helpers/Props/{align.js => align.ts} | 0 3 files changed, 35 insertions(+), 54 deletions(-) delete mode 100644 frontend/src/Components/Link/Button.js create mode 100644 frontend/src/Components/Link/Button.tsx rename frontend/src/Helpers/Props/{align.js => align.ts} (100%) diff --git a/frontend/src/Components/Link/Button.js b/frontend/src/Components/Link/Button.js deleted file mode 100644 index cbe4691d4..000000000 --- a/frontend/src/Components/Link/Button.js +++ /dev/null @@ -1,54 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { align, kinds, sizes } from 'Helpers/Props'; -import Link from './Link'; -import styles from './Button.css'; - -class Button extends Component { - - // - // Render - - render() { - const { - className, - buttonGroupPosition, - kind, - size, - children, - ...otherProps - } = this.props; - - return ( - <Link - className={classNames( - className, - styles[kind], - styles[size], - buttonGroupPosition && styles[buttonGroupPosition] - )} - {...otherProps} - > - {children} - </Link> - ); - } - -} - -Button.propTypes = { - className: PropTypes.string.isRequired, - buttonGroupPosition: PropTypes.oneOf(align.all), - kind: PropTypes.oneOf(kinds.all), - size: PropTypes.oneOf(sizes.all), - children: PropTypes.node -}; - -Button.defaultProps = { - className: styles.button, - kind: kinds.DEFAULT, - size: sizes.MEDIUM -}; - -export default Button; diff --git a/frontend/src/Components/Link/Button.tsx b/frontend/src/Components/Link/Button.tsx new file mode 100644 index 000000000..c512b3a90 --- /dev/null +++ b/frontend/src/Components/Link/Button.tsx @@ -0,0 +1,35 @@ +import classNames from 'classnames'; +import React from 'react'; +import { align, kinds, sizes } from 'Helpers/Props'; +import Link, { LinkProps } from './Link'; +import styles from './Button.css'; + +export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> { + buttonGroupPosition?: Extract< + (typeof align.all)[number], + keyof typeof styles + >; + kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; + size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + children: Required<LinkProps['children']>; +} + +export default function Button({ + className = styles.button, + buttonGroupPosition, + kind = kinds.DEFAULT, + size = sizes.MEDIUM, + ...otherProps +}: ButtonProps) { + return ( + <Link + className={classNames( + className, + styles[kind], + styles[size], + buttonGroupPosition && styles[buttonGroupPosition] + )} + {...otherProps} + /> + ); +} diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.ts similarity index 100% rename from frontend/src/Helpers/Props/align.js rename to frontend/src/Helpers/Props/align.ts From ae7b187e412a080e30fc6826564ce9197ed2f329 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:21:06 +0200 Subject: [PATCH 485/762] Convert Icon to Typescript --- frontend/src/Activity/Queue/QueueStatus.tsx | 4 +- frontend/src/Components/Icon.js | 73 ------------------- frontend/src/Components/Icon.tsx | 59 +++++++++++++++ .../Interactive/InteractiveImportRow.tsx | 2 +- .../InteractiveSearchRow.tsx | 2 +- .../Overview/SeriesIndexOverviewInfoRow.tsx | 5 +- frontend/src/System/Status/Health/Health.tsx | 4 +- .../src/System/Tasks/Queued/QueuedTaskRow.tsx | 7 +- 8 files changed, 72 insertions(+), 84 deletions(-) delete mode 100644 frontend/src/Components/Icon.js create mode 100644 frontend/src/Components/Icon.tsx diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx index 64d802df8..2bd7f6d79 100644 --- a/frontend/src/Activity/Queue/QueueStatus.tsx +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Icon from 'Components/Icon'; +import Icon, { IconProps } from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds } from 'Helpers/Props'; import TooltipPosition from 'Helpers/Props/TooltipPosition'; @@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) { // status === 'downloading' let iconName = icons.DOWNLOADING; - let iconKind = kinds.DEFAULT; + let iconKind: IconProps['kind'] = kinds.DEFAULT; let title = translate('Downloading'); if (status === 'paused') { diff --git a/frontend/src/Components/Icon.js b/frontend/src/Components/Icon.js deleted file mode 100644 index d200b8c08..000000000 --- a/frontend/src/Components/Icon.js +++ /dev/null @@ -1,73 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; -import { kinds } from 'Helpers/Props'; -import styles from './Icon.css'; - -class Icon extends PureComponent { - - // - // Render - - render() { - const { - containerClassName, - className, - name, - kind, - size, - title, - isSpinning, - ...otherProps - } = this.props; - - const icon = ( - <FontAwesomeIcon - className={classNames( - className, - styles[kind] - )} - icon={name} - spin={isSpinning} - style={{ - fontSize: `${size}px` - }} - {...otherProps} - /> - ); - - if (title) { - return ( - <span - className={containerClassName} - title={typeof title === 'function' ? title() : title} - > - {icon} - </span> - ); - } - - return icon; - } -} - -Icon.propTypes = { - containerClassName: PropTypes.string, - className: PropTypes.string, - name: PropTypes.object.isRequired, - kind: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - isSpinning: PropTypes.bool.isRequired, - fixedWidth: PropTypes.bool.isRequired -}; - -Icon.defaultProps = { - kind: kinds.DEFAULT, - size: 14, - isSpinning: false, - fixedWidth: false -}; - -export default Icon; diff --git a/frontend/src/Components/Icon.tsx b/frontend/src/Components/Icon.tsx new file mode 100644 index 000000000..86ff57a30 --- /dev/null +++ b/frontend/src/Components/Icon.tsx @@ -0,0 +1,59 @@ +import { + FontAwesomeIcon, + FontAwesomeIconProps, +} from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import React, { ComponentProps } from 'react'; +import { kinds } from 'Helpers/Props'; +import styles from './Icon.css'; + +export interface IconProps + extends Omit< + FontAwesomeIconProps, + 'icon' | 'spin' | 'name' | 'title' | 'size' + > { + containerClassName?: ComponentProps<'span'>['className']; + name: FontAwesomeIconProps['icon']; + kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; + size?: number; + isSpinning?: FontAwesomeIconProps['spin']; + title?: string | (() => string); +} + +export default function Icon({ + containerClassName, + className, + name, + kind = kinds.DEFAULT, + size = 14, + title, + isSpinning = false, + fixedWidth = false, + ...otherProps +}: IconProps) { + const icon = ( + <FontAwesomeIcon + className={classNames(className, styles[kind])} + icon={name} + spin={isSpinning} + fixedWidth={fixedWidth} + style={{ + fontSize: `${size}px`, + }} + {...otherProps} + /> + ); + + if (title) { + return ( + <span + className={containerClassName} + title={typeof title === 'function' ? title() : title} + > + {icon} + </span> + ); + } + + return icon; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 402be769a..d4f234fa3 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -525,7 +525,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { <> {indexerFlags ? ( <Popover - anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />} + anchor={<Icon name={icons.FLAG} />} title={translate('IndexerFlags')} body={<IndexerFlags indexerFlags={indexerFlags} />} position={tooltipPositions.LEFT} diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index 49b8d7823..d860b7fb9 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -264,7 +264,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { <TableRowCell className={styles.indexerFlags}> {indexerFlags ? ( <Popover - anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />} + anchor={<Icon name={icons.FLAG} />} title={translate('IndexerFlags')} body={<IndexerFlags indexerFlags={indexerFlags} />} position={tooltipPositions.LEFT} diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx index 11ab1b7f7..b23b915c8 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx @@ -1,11 +1,10 @@ -import { IconDefinition } from '@fortawesome/free-regular-svg-icons'; import React from 'react'; -import Icon from 'Components/Icon'; +import Icon, { IconProps } from 'Components/Icon'; import styles from './SeriesIndexOverviewInfoRow.css'; interface SeriesIndexOverviewInfoRowProps { title?: string; - iconName?: IconDefinition; + iconName: IconProps['name']; label: string | null; } diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx index 281d95ac6..087146740 100644 --- a/frontend/src/System/Status/Health/Health.tsx +++ b/frontend/src/System/Status/Health/Health.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; +import Icon, { IconProps } from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -97,7 +97,7 @@ function Health() { {items.map((item) => { const source = item.source; - let kind = kinds.WARNING; + let kind: IconProps['kind'] = kinds.WARNING; switch (item.type.toLowerCase()) { case 'error': kind = kinds.DANGER; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx index 4511bcbf4..66d762039 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx @@ -2,7 +2,7 @@ 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 Icon, { IconProps } from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -19,7 +19,10 @@ import translate from 'Utilities/String/translate'; import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; import styles from './QueuedTaskRow.css'; -function getStatusIconProps(status: string, message: string | undefined) { +function getStatusIconProps( + status: string, + message: string | undefined +): IconProps { const title = titleCase(status); switch (status) { From a2e06e9e650642518b926a61f624a2c7a49c0988 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:21:50 +0200 Subject: [PATCH 486/762] Link polymorphic static typing --- frontend/src/Components/Link/Link.tsx | 133 ++++++++++++-------------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx index 5015a1fe3..d6d731106 100644 --- a/frontend/src/Components/Link/Link.tsx +++ b/frontend/src/Components/Link/Link.tsx @@ -1,96 +1,89 @@ import classNames from 'classnames'; import React, { - ComponentClass, - FunctionComponent, + ComponentPropsWithoutRef, + ElementType, SyntheticEvent, useCallback, } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import styles from './Link.css'; -interface ReactRouterLinkProps { - to?: string; -} +export type LinkProps<C extends ElementType = 'button'> = + ComponentPropsWithoutRef<C> & { + component?: C; + to?: string; + target?: string; + isDisabled?: LinkProps<C>['disabled']; + noRouter?: boolean; + onPress?(event: SyntheticEvent): void; + }; -export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> { - className?: string; - component?: - | string - | FunctionComponent<LinkProps> - | ComponentClass<LinkProps, unknown>; - to?: string; - target?: string; - isDisabled?: boolean; - noRouter?: boolean; - onPress?(event: SyntheticEvent): void; -} -function Link(props: LinkProps) { - const { - className, - component = 'button', - to, - target, - type, - isDisabled, - noRouter = false, - onPress, - ...otherProps - } = props; +export default function Link<C extends ElementType = 'button'>({ + className, + component, + to, + target, + type, + isDisabled, + noRouter, + onPress, + ...otherProps +}: LinkProps<C>) { + const Component = component || 'button'; const onClick = useCallback( (event: SyntheticEvent) => { - if (!isDisabled && onPress) { - onPress(event); + if (isDisabled) { + return; } + + onPress?.(event); }, [isDisabled, onPress] ); - const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = { - target, - }; - let el = component; - - if (to) { - if (/\w+?:\/\//.test(to)) { - el = 'a'; - linkProps.href = to; - linkProps.target = target || '_blank'; - linkProps.rel = 'noreferrer'; - } else if (noRouter) { - el = 'a'; - linkProps.href = to; - linkProps.target = target || '_self'; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - el = RouterLink; - linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`; - linkProps.target = target; - } - } - - if (el === 'button' || el === 'input') { - linkProps.type = type || 'button'; - linkProps.disabled = isDisabled; - } - - linkProps.className = classNames( + const linkClass = classNames( className, styles.link, to && styles.to, isDisabled && 'isDisabled' ); - const elementProps = { - ...otherProps, - type, - ...linkProps, - }; + if (to) { + const toLink = /\w+?:\/\//.test(to); - elementProps.onClick = onClick; + if (toLink || noRouter) { + return ( + <a + href={to} + target={target || (toLink ? '_blank' : '_self')} + rel={toLink ? 'noreferrer' : undefined} + className={linkClass} + onClick={onClick} + {...otherProps} + /> + ); + } - return React.createElement(el, elementProps); + return ( + <RouterLink + to={`${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`} + target={target} + className={linkClass} + onClick={onClick} + {...otherProps} + /> + ); + } + + return ( + <Component + type={type || 'button'} + target={target} + className={linkClass} + disabled={isDisabled} + onClick={onClick} + {...otherProps} + /> + ); } - -export default Link; From 8af4246ff9baee4c291550102769a1186f65dc29 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:22:42 +0200 Subject: [PATCH 487/762] Updated code action fixall value for VSCode --- frontend/.vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index edb88e0e7..8da95337f 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -9,7 +9,7 @@ "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "typescript.preferences.quoteStyle": "single", From 8ceb306bf181665ba6a57182f6852bddf8671124 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 26 Aug 2024 03:23:24 +0300 Subject: [PATCH 488/762] Fixed: Ensure Root Folder exists when Adding Series --- .../Paths/RootFolderExistsValidator.cs | 3 ++- src/Sonarr.Api.V3/Series/SeriesController.cs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs index 8b4c4e7a0..d879af0d9 100644 --- a/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/RootFolderExistsValidator.cs @@ -1,4 +1,5 @@ using FluentValidation.Validators; +using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.RootFolders; @@ -19,7 +20,7 @@ namespace NzbDrone.Core.Validation.Paths { context.MessageFormatter.AppendArgument("path", context.PropertyValue?.ToString()); - return context.PropertyValue == null || _rootFolderService.All().Exists(r => r.Path.PathEquals(context.PropertyValue.ToString())); + return context.PropertyValue == null || _rootFolderService.All().Exists(r => r.Path.IsPathValid(PathValidationType.CurrentOs) && r.Path.PathEquals(context.PropertyValue.ToString())); } } } diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 57ff76bc4..48d377e40 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -59,6 +59,7 @@ namespace Sonarr.Api.V3.Series SeriesAncestorValidator seriesAncestorValidator, SystemFolderValidator systemFolderValidator, QualityProfileExistsValidator qualityProfileExistsValidator, + RootFolderExistsValidator rootFolderExistsValidator, SeriesFolderAsRootFolderValidator seriesFolderAsRootFolderValidator) : base(signalRBroadcaster) { @@ -88,6 +89,7 @@ namespace Sonarr.Api.V3.Series PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath) .IsValidPath() + .SetValidator(rootFolderExistsValidator) .SetValidator(seriesFolderAsRootFolderValidator) .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty(); @@ -156,6 +158,7 @@ namespace Sonarr.Api.V3.Series [RestPostById] [Consumes("application/json")] + [Produces("application/json")] public ActionResult<SeriesResource> AddSeries([FromBody] SeriesResource seriesResource) { var series = _addSeriesService.AddSeries(seriesResource.ToModel()); @@ -165,6 +168,7 @@ namespace Sonarr.Api.V3.Series [RestPutById] [Consumes("application/json")] + [Produces("application/json")] public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false) { var series = _seriesService.GetSeries(seriesResource.Id); @@ -175,12 +179,12 @@ namespace Sonarr.Api.V3.Series var destinationPath = seriesResource.Path; _commandQueueManager.Push(new MoveSeriesCommand - { - SeriesId = series.Id, - SourcePath = sourcePath, - DestinationPath = destinationPath, - Trigger = CommandTrigger.Manual - }); + { + SeriesId = series.Id, + SourcePath = sourcePath, + DestinationPath = destinationPath, + Trigger = CommandTrigger.Manual + }); } var model = seriesResource.ToModel(series); From dde28cbd7e16b85f78d38c8dde7cf6bbb6119bb3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:20:05 +0300 Subject: [PATCH 489/762] Fix disabled style for monitor toggle button --- frontend/src/Components/MonitorToggleButton.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/Components/MonitorToggleButton.css b/frontend/src/Components/MonitorToggleButton.css index 59376129d..38f26adea 100644 --- a/frontend/src/Components/MonitorToggleButton.css +++ b/frontend/src/Components/MonitorToggleButton.css @@ -3,9 +3,9 @@ padding: 0; font-size: inherit; -} -.isDisabled { - color: var(--disabledColor); - cursor: not-allowed; + &.isDisabled { + color: var(--disabledColor); + cursor: not-allowed; + } } From 846333ddf0d9da775c80d004fdb9b41e700ef359 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:24:16 -0500 Subject: [PATCH 490/762] Fixed: Trim spaces and empty values in Proxy Bypass List --- src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs index 022d8adee..c80044d29 100644 --- a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs +++ b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs @@ -30,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy { if (!string.IsNullOrWhiteSpace(BypassFilter)) { - var hostlist = BypassFilter.Split(','); + var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for (var i = 0; i < hostlist.Length; i++) { if (hostlist[i].StartsWith("*")) From 402db9128c214d4c5af6583643cb49d3aa7a28b5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:55:16 +0300 Subject: [PATCH 491/762] New: Bypass IP addresses ranges in proxies --- src/NzbDrone.Common/Sonarr.Common.csproj | 1 + .../Http/HttpProxySettingsProviderFixture.cs | 4 +++- src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs | 12 +++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 6778f7fb5..4994793cf 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -5,6 +5,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> + <PackageReference Include="IPAddressRange" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> diff --git a/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs b/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs index 067149904..2beeb16f9 100644 --- a/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http { private HttpProxySettings GetProxySettings() { - return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null); + return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null); } [Test] @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Http Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue(); + Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue(); } [Test] @@ -31,6 +32,7 @@ namespace NzbDrone.Core.Test.Http var settings = GetProxySettings(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse(); + Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs b/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs index edf77b31f..1c69bbec5 100644 --- a/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs +++ b/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Net; +using NetTools; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration; @@ -52,7 +54,15 @@ namespace NzbDrone.Core.Http // We are utilising the WebProxy implementation here to save us having to reimplement it. This way we use Microsofts implementation var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray); - return proxy.IsBypassed((Uri)url); + return proxy.IsBypassed((Uri)url) || IsBypassedByIpAddressRange(proxySettings.BypassListAsArray, url.Host); + } + + private static bool IsBypassedByIpAddressRange(string[] bypassList, string host) + { + return bypassList.Any(bypass => + IPAddressRange.TryParse(bypass, out var ipAddressRange) && + IPAddress.TryParse(host, out var ipAddress) && + ipAddressRange.Contains(ipAddress)); } } } From 50d7e8fed4f9a43b501551f84471656f8bb19458 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:33:52 +0300 Subject: [PATCH 492/762] Fixed: Hide reboot and shutdown UI buttons on docker --- .../src/Components/Page/Header/PageHeader.js | 5 +- .../Page/Header/PageHeaderActionsMenu.js | 90 ------------------- .../Page/Header/PageHeaderActionsMenu.tsx | 87 ++++++++++++++++++ .../Header/PageHeaderActionsMenuConnector.js | 56 ------------ 4 files changed, 90 insertions(+), 148 deletions(-) delete mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenu.js create mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx delete mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js index 2af052015..7d77488b7 100644 --- a/frontend/src/Components/Page/Header/PageHeader.js +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -6,7 +6,7 @@ import Link from 'Components/Link/Link'; import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import KeyboardShortcutsModal from './KeyboardShortcutsModal'; -import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; import SeriesSearchInputConnector from './SeriesSearchInputConnector'; import styles from './PageHeader.css'; @@ -83,7 +83,8 @@ class PageHeader extends Component { size={14} title={translate('Donate')} /> - <PageHeaderActionsMenuConnector + + <PageHeaderActionsMenu onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal} /> </div> diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js deleted file mode 100644 index 88a974f71..000000000 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import Menu from 'Components/Menu/Menu'; -import MenuButton from 'Components/Menu/MenuButton'; -import MenuContent from 'Components/Menu/MenuContent'; -import MenuItem from 'Components/Menu/MenuItem'; -import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; -import { align, icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './PageHeaderActionsMenu.css'; - -function PageHeaderActionsMenu(props) { - const { - formsAuth, - onKeyboardShortcutsPress, - onRestartPress, - onShutdownPress - } = props; - - return ( - <div> - <Menu alignMenu={align.RIGHT}> - <MenuButton className={styles.menuButton} aria-label="Menu Button"> - <Icon - name={icons.INTERACTIVE} - title={translate('Menu')} - /> - </MenuButton> - - <MenuContent> - <MenuItem onPress={onKeyboardShortcutsPress}> - <Icon - className={styles.itemIcon} - name={icons.KEYBOARD} - /> - {translate('KeyboardShortcuts')} - </MenuItem> - - <MenuItemSeparator /> - - <MenuItem onPress={onRestartPress}> - <Icon - className={styles.itemIcon} - name={icons.RESTART} - /> - {translate('Restart')} - </MenuItem> - - <MenuItem onPress={onShutdownPress}> - <Icon - className={styles.itemIcon} - name={icons.SHUTDOWN} - kind={kinds.DANGER} - /> - {translate('Shutdown')} - </MenuItem> - - { - formsAuth && - <div className={styles.separator} /> - } - - { - formsAuth && - <MenuItem - to={`${window.Sonarr.urlBase}/logout`} - noRouter={true} - > - <Icon - className={styles.itemIcon} - name={icons.LOGOUT} - /> - {translate('Logout')} - </MenuItem> - } - </MenuContent> - </Menu> - </div> - ); -} - -PageHeaderActionsMenu.propTypes = { - formsAuth: PropTypes.bool.isRequired, - onKeyboardShortcutsPress: PropTypes.func.isRequired, - onRestartPress: PropTypes.func.isRequired, - onShutdownPress: PropTypes.func.isRequired -}; - -export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx new file mode 100644 index 000000000..939852673 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx @@ -0,0 +1,87 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; +import { align, icons, kinds } from 'Helpers/Props'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import translate from 'Utilities/String/translate'; +import styles from './PageHeaderActionsMenu.css'; + +interface PageHeaderActionsMenuProps { + onKeyboardShortcutsPress(): void; +} + +function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { + const { onKeyboardShortcutsPress } = props; + + const dispatch = useDispatch(); + + const { authentication, isDocker } = useSelector( + (state: AppState) => state.system.status.item + ); + + const formsAuth = authentication === 'forms'; + + const handleRestartPress = useCallback(() => { + dispatch(restart()); + }, [dispatch]); + + const handleShutdownPress = useCallback(() => { + dispatch(shutdown()); + }, [dispatch]); + + return ( + <div> + <Menu alignMenu={align.RIGHT}> + <MenuButton className={styles.menuButton} aria-label="Menu Button"> + <Icon name={icons.INTERACTIVE} title={translate('Menu')} /> + </MenuButton> + + <MenuContent> + <MenuItem onPress={onKeyboardShortcutsPress}> + <Icon className={styles.itemIcon} name={icons.KEYBOARD} /> + {translate('KeyboardShortcuts')} + </MenuItem> + + {isDocker ? null : ( + <> + <MenuItemSeparator /> + + <MenuItem onPress={handleRestartPress}> + <Icon className={styles.itemIcon} name={icons.RESTART} /> + {translate('Restart')} + </MenuItem> + + <MenuItem onPress={handleShutdownPress}> + <Icon + className={styles.itemIcon} + name={icons.SHUTDOWN} + kind={kinds.DANGER} + /> + {translate('Shutdown')} + </MenuItem> + </> + )} + + {formsAuth ? ( + <> + <MenuItemSeparator /> + + <MenuItem to={`${window.Sonarr.urlBase}/logout`} noRouter={true}> + <Icon className={styles.itemIcon} name={icons.LOGOUT} /> + {translate('Logout')} + </MenuItem> + </> + ) : null} + </MenuContent> + </Menu> + </div> + ); +} + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js deleted file mode 100644 index 3aba95065..000000000 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { restart, shutdown } from 'Store/Actions/systemActions'; -import PageHeaderActionsMenu from './PageHeaderActionsMenu'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.status, - (status) => { - return { - formsAuth: status.item.authentication === 'forms' - }; - } - ); -} - -const mapDispatchToProps = { - restart, - shutdown -}; - -class PageHeaderActionsMenuConnector extends Component { - - // - // Listeners - - onRestartPress = () => { - this.props.restart(); - }; - - onShutdownPress = () => { - this.props.shutdown(); - }; - - // - // Render - - render() { - return ( - <PageHeaderActionsMenu - {...this.props} - onRestartPress={this.onRestartPress} - onShutdownPress={this.onShutdownPress} - /> - ); - } -} - -PageHeaderActionsMenuConnector.propTypes = { - restart: PropTypes.func.isRequired, - shutdown: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); From a9b93dd9c686a11ee16e717ca4e8698629a44ef7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 26 Aug 2024 01:17:54 +0300 Subject: [PATCH 493/762] Fixed: Paths for renamed episode files in Custom Script and Webhook --- src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs | 2 +- src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index c5e2e2146..4a883bd98 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -253,7 +253,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); environmentVariables.Add("Sonarr_EpisodeFile_Ids", string.Join(",", renamedFiles.Select(e => e.EpisodeFile.Id))); environmentVariables.Add("Sonarr_EpisodeFile_RelativePaths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.RelativePath))); - environmentVariables.Add("Sonarr_EpisodeFile_Paths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.Path))); + environmentVariables.Add("Sonarr_EpisodeFile_Paths", string.Join("|", renamedFiles.Select(e => Path.Combine(series.Path, e.EpisodeFile.RelativePath)))); environmentVariables.Add("Sonarr_EpisodeFile_PreviousRelativePaths", string.Join("|", renamedFiles.Select(e => e.PreviousRelativePath))); environmentVariables.Add("Sonarr_EpisodeFile_PreviousPaths", string.Join("|", renamedFiles.Select(e => e.PreviousPath))); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs index deeb9db95..c0348931b 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Notifications.Webhook { Id = episodeFile.Id; RelativePath = episodeFile.RelativePath; - Path = episodeFile.Path; + Path = System.IO.Path.Combine(episodeFile.Series.Value.Path, episodeFile.RelativePath); Quality = episodeFile.Quality.Quality.Name; QualityVersion = episodeFile.Quality.Revision.Version; ReleaseGroup = episodeFile.ReleaseGroup; From 4e14ce022c94369e7d119af3ae05e00ec29ccea1 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 19 Aug 2024 02:55:13 +0300 Subject: [PATCH 494/762] New: Bulk manage custom formats --- frontend/src/App/State/SettingsAppState.ts | 7 + .../CustomFormatSettingsPage.tsx | 3 + .../Edit/ManageCustomFormatsEditModal.tsx | 28 ++ .../ManageCustomFormatsEditModalContent.css | 16 ++ ...nageCustomFormatsEditModalContent.css.d.ts | 8 + .../ManageCustomFormatsEditModalContent.tsx | 125 +++++++++ .../Manage/ManageCustomFormatsModal.tsx | 20 ++ .../ManageCustomFormatsModalContent.css | 16 ++ .../ManageCustomFormatsModalContent.css.d.ts | 9 + .../ManageCustomFormatsModalContent.tsx | 241 ++++++++++++++++++ .../Manage/ManageCustomFormatsModalRow.css | 6 + .../ManageCustomFormatsModalRow.css.d.ts | 8 + .../Manage/ManageCustomFormatsModalRow.tsx | 54 ++++ .../ManageCustomFormatsToolbarButton.tsx | 28 ++ .../ManageDownloadClientsModalContent.css | 2 +- .../ManageDownloadClientsModalContent.tsx | 4 +- .../Manage/ManageImportListsModalContent.css | 2 +- .../Manage/ManageImportListsModalContent.tsx | 4 +- .../Manage/ManageIndexersModalContent.css | 2 +- .../Manage/ManageIndexersModalContent.tsx | 4 +- .../Store/Actions/Settings/customFormats.js | 48 +++- .../Store/Actions/Settings/downloadClients.js | 4 +- .../src/Store/Actions/Settings/indexers.js | 4 +- frontend/src/typings/CustomFormat.ts | 6 +- .../CustomFormats/CustomFormatService.cs | 23 ++ src/NzbDrone.Core/Localization/Core/en.json | 6 + .../CustomFormats/CustomFormatBulkResource.cs | 10 + .../CustomFormats/CustomFormatController.cs | 39 ++- 28 files changed, 697 insertions(+), 30 deletions(-) create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx create mode 100644 src/Sonarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index ac08aa127..a3704d10e 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -6,6 +6,7 @@ import AppSectionState, { PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; +import CustomFormat from 'typings/CustomFormat'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import ImportListExclusion from 'typings/ImportListExclusion'; @@ -48,6 +49,11 @@ export interface QualityProfilesAppState extends AppSectionState<QualityProfile>, AppSectionItemSchemaState<QualityProfile> {} +export interface CustomFormatAppState + extends AppSectionState<CustomFormat>, + AppSectionDeleteState, + AppSectionSaveState {} + export interface ImportListOptionsSettingsAppState extends AppSectionItemState<ImportListOptionsSettings>, AppSectionSaveState {} @@ -66,6 +72,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>; interface SettingsAppState { advancedSettings: boolean; + customFormats: CustomFormatAppState; downloadClients: DownloadClientAppState; general: GeneralAppState; importListExclusions: ImportListExclusionsSettingsAppState; diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx index cc02a2a9a..66c208f9a 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -8,6 +8,7 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; +import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton'; function CustomFormatSettingsPage() { return ( @@ -21,6 +22,8 @@ function CustomFormatSettingsPage() { <PageToolbarSeparator /> <ParseToolbarButton /> + + <ManageCustomFormatsToolbarButton /> </> } /> diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx new file mode 100644 index 000000000..3ff5cfa37 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent'; + +interface ManageCustomFormatsEditModalProps { + isOpen: boolean; + customFormatIds: number[]; + onSavePress(payload: object): void; + onModalClose(): void; +} + +function ManageCustomFormatsEditModal( + props: ManageCustomFormatsEditModalProps +) { + const { isOpen, customFormatIds, onSavePress, onModalClose } = props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <ManageCustomFormatsEditModalContent + customFormatIds={customFormatIds} + onSavePress={onSavePress} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +export default ManageCustomFormatsEditModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css new file mode 100644 index 000000000..ea406894e --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css @@ -0,0 +1,16 @@ +.modalFooter { + composes: modalFooter from '~Components/Modal/ModalFooter.css'; + + justify-content: space-between; +} + +.selected { + font-weight: bold; +} + +@media only screen and (max-width: $breakpointExtraSmall) { + .modalFooter { + flex-direction: column; + gap: 10px; + } +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts new file mode 100644 index 000000000..cbf2d6328 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'modalFooter': string; + 'selected': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx new file mode 100644 index 000000000..25a2f85c2 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx @@ -0,0 +1,125 @@ +import React, { useCallback, useState } from 'react'; +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 } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './ManageCustomFormatsEditModalContent.css'; + +interface SavePayload { + includeCustomFormatWhenRenaming?: boolean; +} + +interface ManageCustomFormatsEditModalContentProps { + customFormatIds: number[]; + onSavePress(payload: object): void; + onModalClose(): void; +} + +const NO_CHANGE = 'noChange'; + +const enableOptions = [ + { + key: NO_CHANGE, + get value() { + return translate('NoChange'); + }, + isDisabled: true, + }, + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabled', + get value() { + return translate('Disabled'); + }, + }, +]; + +function ManageCustomFormatsEditModalContent( + props: ManageCustomFormatsEditModalContentProps +) { + const { customFormatIds, onSavePress, onModalClose } = props; + + const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] = + useState(NO_CHANGE); + + const save = useCallback(() => { + let hasChanges = false; + const payload: SavePayload = {}; + + if (includeCustomFormatWhenRenaming !== NO_CHANGE) { + hasChanges = true; + payload.includeCustomFormatWhenRenaming = + includeCustomFormatWhenRenaming === 'enabled'; + } + + if (hasChanges) { + onSavePress(payload); + } + + onModalClose(); + }, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]); + + const onInputChange = useCallback( + ({ name, value }: { name: string; value: string }) => { + switch (name) { + case 'includeCustomFormatWhenRenaming': + setIncludeCustomFormatWhenRenaming(value); + break; + default: + console.warn( + `EditCustomFormatsModalContent Unknown Input: '${name}'` + ); + } + }, + [] + ); + + const selectedCount = customFormatIds.length; + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('EditSelectedCustomFormats')}</ModalHeader> + + <ModalBody> + <FormGroup> + <FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="includeCustomFormatWhenRenaming" + value={includeCustomFormatWhenRenaming} + values={enableOptions} + onChange={onInputChange} + /> + </FormGroup> + </ModalBody> + + <ModalFooter className={styles.modalFooter}> + <div className={styles.selected}> + {translate('CountCustomFormatsSelected', { + count: selectedCount, + })} + </div> + + <div> + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <Button onPress={save}>{translate('ApplyChanges')}</Button> + </div> + </ModalFooter> + </ModalContent> + ); +} + +export default ManageCustomFormatsEditModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx new file mode 100644 index 000000000..dd3456437 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent'; + +interface ManageCustomFormatsModalProps { + isOpen: boolean; + onModalClose(): void; +} + +function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) { + const { isOpen, onModalClose } = props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <ManageCustomFormatsModalContent onModalClose={onModalClose} /> + </Modal> + ); +} + +export default ManageCustomFormatsModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css new file mode 100644 index 000000000..6ea04a0c8 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css @@ -0,0 +1,16 @@ +.leftButtons, +.rightButtons { + display: flex; + flex: 1 0 50%; + flex-wrap: wrap; +} + +.rightButtons { + justify-content: flex-end; +} + +.deleteButton { + composes: button from '~Components/Link/Button.css'; + + margin-right: 10px; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts new file mode 100644 index 000000000..7b392fff9 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'deleteButton': string; + 'leftButtons': string; + 'rightButtons': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx new file mode 100644 index 000000000..eab8a4d67 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx @@ -0,0 +1,241 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CustomFormatAppState } from 'App/State/SettingsAppState'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +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 Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import useSelectState from 'Helpers/Hooks/useSelectState'; +import { kinds } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; +import { + bulkDeleteCustomFormats, + bulkEditCustomFormats, + setManageCustomFormatsSort, +} from 'Store/Actions/settingsActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { SelectStateInputProps } from 'typings/props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal'; +import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow'; +import styles from './ManageCustomFormatsModalContent.css'; + +// TODO: This feels janky to do, but not sure of a better way currently +type OnSelectedChangeCallback = React.ComponentProps< + typeof ManageCustomFormatsModalRow +>['onSelectedChange']; + +const COLUMNS = [ + { + name: 'name', + label: () => translate('Name'), + isSortable: true, + isVisible: true, + }, + { + name: 'includeCustomFormatWhenRenaming', + label: () => translate('IncludeCustomFormatWhenRenaming'), + isSortable: true, + isVisible: true, + }, +]; + +interface ManageCustomFormatsModalContentProps { + onModalClose(): void; + sortKey?: string; + sortDirection?: SortDirection; +} + +function ManageCustomFormatsModalContent( + props: ManageCustomFormatsModalContentProps +) { + const { onModalClose } = props; + + const { + isFetching, + isPopulated, + isDeleting, + isSaving, + error, + items, + sortKey, + sortDirection, + }: CustomFormatAppState = useSelector( + createClientSideCollectionSelector('settings.customFormats') + ); + const dispatch = useDispatch(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + + const [selectState, setSelectState] = useSelectState(); + + const { allSelected, allUnselected, selectedState } = selectState; + + const selectedIds: number[] = useMemo(() => { + return getSelectedIds(selectedState); + }, [selectedState]); + + const selectedCount = selectedIds.length; + + const onSortPress = useCallback( + (value: string) => { + dispatch(setManageCustomFormatsSort({ sortKey: value })); + }, + [dispatch] + ); + + const onDeletePress = useCallback(() => { + setIsDeleteModalOpen(true); + }, [setIsDeleteModalOpen]); + + const onDeleteModalClose = useCallback(() => { + setIsDeleteModalOpen(false); + }, [setIsDeleteModalOpen]); + + const onEditPress = useCallback(() => { + setIsEditModalOpen(true); + }, [setIsEditModalOpen]); + + const onEditModalClose = useCallback(() => { + setIsEditModalOpen(false); + }, [setIsEditModalOpen]); + + const onConfirmDelete = useCallback(() => { + dispatch(bulkDeleteCustomFormats({ ids: selectedIds })); + setIsDeleteModalOpen(false); + }, [selectedIds, dispatch]); + + const onSavePress = useCallback( + (payload: object) => { + setIsEditModalOpen(false); + + dispatch( + bulkEditCustomFormats({ + ids: selectedIds, + ...payload, + }) + ); + }, + [selectedIds, dispatch] + ); + + const onSelectAllChange = useCallback( + ({ value }: SelectStateInputProps) => { + setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + }, + [items, setSelectState] + ); + + const onSelectedChange = useCallback<OnSelectedChangeCallback>( + ({ id, value, shiftKey = false }) => { + setSelectState({ + type: 'toggleSelected', + items, + id, + isSelected: value, + shiftKey, + }); + }, + [items, setSelectState] + ); + + const errorMessage = getErrorMessage(error, 'Unable to load custom formats.'); + const anySelected = selectedCount > 0; + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('ManageCustomFormats')}</ModalHeader> + <ModalBody> + {isFetching ? <LoadingIndicator /> : null} + + {error ? <div>{errorMessage}</div> : null} + + {isPopulated && !error && !items.length ? ( + <Alert kind={kinds.INFO}>{translate('NoCustomFormatsFound')}</Alert> + ) : null} + + {isPopulated && !!items.length && !isFetching && !isFetching ? ( + <Table + columns={COLUMNS} + horizontalScroll={true} + selectAll={true} + allSelected={allSelected} + allUnselected={allUnselected} + sortKey={sortKey} + sortDirection={sortDirection} + onSelectAllChange={onSelectAllChange} + onSortPress={onSortPress} + > + <TableBody> + {items.map((item) => { + return ( + <ManageCustomFormatsModalRow + key={item.id} + isSelected={selectedState[item.id]} + {...item} + columns={COLUMNS} + onSelectedChange={onSelectedChange} + /> + ); + })} + </TableBody> + </Table> + ) : null} + </ModalBody> + + <ModalFooter> + <div className={styles.leftButtons}> + <SpinnerButton + kind={kinds.DANGER} + isSpinning={isDeleting} + isDisabled={!anySelected} + onPress={onDeletePress} + > + {translate('Delete')} + </SpinnerButton> + + <SpinnerButton + isSpinning={isSaving} + isDisabled={!anySelected} + onPress={onEditPress} + > + {translate('Edit')} + </SpinnerButton> + </div> + + <Button onPress={onModalClose}>{translate('Close')}</Button> + </ModalFooter> + + <ManageCustomFormatsEditModal + isOpen={isEditModalOpen} + customFormatIds={selectedIds} + onModalClose={onEditModalClose} + onSavePress={onSavePress} + /> + + <ConfirmModal + isOpen={isDeleteModalOpen} + kind={kinds.DANGER} + title={translate('DeleteSelectedCustomFormats')} + message={translate('DeleteSelectedCustomFormatsMessageText', { + count: selectedIds.length, + })} + confirmLabel={translate('Delete')} + onConfirm={onConfirmDelete} + onCancel={onDeleteModalClose} + /> + </ModalContent> + ); +} + +export default ManageCustomFormatsModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css new file mode 100644 index 000000000..a7c85e340 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css @@ -0,0 +1,6 @@ +.name, +.includeCustomFormatWhenRenaming { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts new file mode 100644 index 000000000..906d2dc54 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'includeCustomFormatWhenRenaming': string; + 'name': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx new file mode 100644 index 000000000..32b135970 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import { SelectStateInputProps } from 'typings/props'; +import translate from 'Utilities/String/translate'; +import styles from './ManageCustomFormatsModalRow.css'; + +interface ManageCustomFormatsModalRowProps { + id: number; + name: string; + includeCustomFormatWhenRenaming: boolean; + columns: Column[]; + isSelected?: boolean; + onSelectedChange(result: SelectStateInputProps): void; +} + +function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { + const { + id, + isSelected, + name, + includeCustomFormatWhenRenaming, + onSelectedChange, + } = props; + + const onSelectedChangeWrapper = useCallback( + (result: SelectStateInputProps) => { + onSelectedChange({ + ...result, + }); + }, + [onSelectedChange] + ); + + return ( + <TableRow> + <TableSelectCell + id={id} + isSelected={isSelected} + onSelectedChange={onSelectedChangeWrapper} + /> + + <TableRowCell className={styles.name}>{name}</TableRowCell> + + <TableRowCell className={styles.includeCustomFormatWhenRenaming}> + {includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')} + </TableRowCell> + </TableRow> + ); +} + +export default ManageCustomFormatsModalRow; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx new file mode 100644 index 000000000..f27f9e503 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import ManageCustomFormatsModal from './ManageCustomFormatsModal'; + +function ManageCustomFormatsToolbarButton() { + const [isManageModalOpen, openManageModal, closeManageModal] = + useModalOpenState(false); + + return ( + <> + <PageToolbarButton + label={translate('ManageCustomFormats')} + iconName={icons.MANAGE} + onPress={openManageModal} + /> + + <ManageCustomFormatsModal + isOpen={isManageModalOpen} + onModalClose={closeManageModal} + /> + </> + ); +} + +export default ManageCustomFormatsToolbarButton; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css index c106388ab..6ea04a0c8 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} \ No newline at end of file +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index a788d824e..656f91ef6 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -220,9 +220,9 @@ function ManageDownloadClientsModalContent( {error ? <div>{errorMessage}</div> : null} - {isPopulated && !error && !items.length && ( + {isPopulated && !error && !items.length ? ( <Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert> - )} + ) : null} {isPopulated && !!items.length && !isFetching && !isFetching ? ( <Table diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css index c106388ab..6ea04a0c8 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} \ No newline at end of file +} diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 5cbad933a..4d7ce02a6 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -198,9 +198,9 @@ function ManageImportListsModalContent( {error ? <div>{errorMessage}</div> : null} - {isPopulated && !error && !items.length && ( + {isPopulated && !error && !items.length ? ( <Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert> - )} + ) : null} {isPopulated && !!items.length && !isFetching && !isFetching ? ( <Table diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css index c106388ab..6ea04a0c8 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} \ No newline at end of file +} diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index f03b39a21..7b0b3c0b2 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -215,9 +215,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { {error ? <div>{errorMessage}</div> : null} - {isPopulated && !error && !items.length && ( + {isPopulated && !error && !items.length ? ( <Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert> - )} + ) : null} {isPopulated && !!items.length && !isFetching && !isFetching ? ( <Table diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js index e7691c09f..3d42c8c2d 100644 --- a/frontend/src/Store/Actions/Settings/customFormats.js +++ b/frontend/src/Store/Actions/Settings/customFormats.js @@ -1,7 +1,12 @@ import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; +import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createSetClientSideCollectionSortReducer + from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; import getSectionState from 'Utilities/State/getSectionState'; @@ -22,6 +27,9 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat'; export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat'; export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue'; export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat'; +export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats'; +export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats'; +export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort'; // // Action Creators @@ -29,6 +37,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat'; export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS); export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT); export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT); +export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS); +export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS); +export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT); export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => { return { @@ -48,20 +59,30 @@ export default { // State defaultState: { - isSchemaFetching: false, - isSchemaPopulated: false, isFetching: false, isPopulated: false, + error: null, + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + items: [], + pendingChanges: {}, + + isSchemaFetching: false, + isSchemaPopulated: false, + schemaError: null, schema: { includeCustomFormatWhenRenaming: false }, - error: null, - isDeleting: false, - deleteError: null, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} + + sortKey: 'name', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + name: ({ name }) => { + return name.toLocaleLowerCase(); + } + } }, // @@ -83,7 +104,10 @@ export default { })); createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch); - } + }, + + [BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'), + [BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk') }, // @@ -103,7 +127,9 @@ export default { newState.pendingChanges = pendingChanges; return updateSectionState(state, section, newState); - } + }, + + [SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section) } }; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index aee945ef5..1113e7daf 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -96,8 +96,8 @@ export default { sortKey: 'name', sortDirection: sortDirections.ASCENDING, sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); + name: ({ name }) => { + return name.toLocaleLowerCase(); } } }, diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index 28aa9039d..a277e013f 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -101,8 +101,8 @@ export default { sortKey: 'name', sortDirection: sortDirections.ASCENDING, sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); + name: ({ name }) => { + return name.toLocaleLowerCase(); } } }, diff --git a/frontend/src/typings/CustomFormat.ts b/frontend/src/typings/CustomFormat.ts index 7cef9f6ef..b3cc2a845 100644 --- a/frontend/src/typings/CustomFormat.ts +++ b/frontend/src/typings/CustomFormat.ts @@ -1,12 +1,14 @@ +import ModelBase from 'App/ModelBase'; + export interface QualityProfileFormatItem { format: number; name: string; score: number; } -interface CustomFormat { - id: number; +interface CustomFormat extends ModelBase { name: string; + includeCustomFormatWhenRenaming: boolean; } export default CustomFormat; diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs index 5475ac5b8..f45a46810 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs @@ -9,10 +9,12 @@ namespace NzbDrone.Core.CustomFormats public interface ICustomFormatService { void Update(CustomFormat customFormat); + void Update(List<CustomFormat> customFormat); CustomFormat Insert(CustomFormat customFormat); List<CustomFormat> All(); CustomFormat GetById(int id); void Delete(int id); + void Delete(List<int> ids); } public class CustomFormatService : ICustomFormatService @@ -51,6 +53,12 @@ namespace NzbDrone.Core.CustomFormats _cache.Clear(); } + public void Update(List<CustomFormat> customFormat) + { + _formatRepository.UpdateMany(customFormat); + _cache.Clear(); + } + public CustomFormat Insert(CustomFormat customFormat) { // Add to DB then insert into profiles @@ -72,5 +80,20 @@ namespace NzbDrone.Core.CustomFormats _formatRepository.Delete(id); _cache.Clear(); } + + public void Delete(List<int> ids) + { + foreach (var id in ids) + { + var format = _formatRepository.Get(id); + + // Remove from profiles before removing from DB + _eventAggregator.PublishEvent(new CustomFormatDeletedEvent(format)); + + _formatRepository.Delete(id); + } + + _cache.Clear(); + } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index d3cfbebeb..1e83d3453 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -258,6 +258,7 @@ "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use {appName}'s rename function as a work around.", "CopyUsingHardlinksSeriesHelpText": "Hardlinks allow {appName} to import seeding torrents to the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", "CouldNotFindResults": "Couldn't find any results for '{term}'", + "CountCustomFormatsSelected": "{count} custom formats(s) selected", "CountDownloadClientsSelected": "{count} download client(s) selected", "CountImportListsSelected": "{count} import list(s) selected", "CountIndexersSelected": "{count} indexer(s) selected", @@ -364,6 +365,8 @@ "DeleteRootFolder": "Delete Root Folder", "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{path}'?", "DeleteSelected": "Delete Selected", + "DeleteSelectedCustomFormats": "Delete Custom Format(s)", + "DeleteSelectedCustomFormatsMessageText": "Are you sure you want to delete {count} selected custom format(s)?", "DeleteSelectedDownloadClients": "Delete Download Client(s)", "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?", "DeleteSelectedEpisodeFiles": "Delete Selected Episode Files", @@ -590,6 +593,7 @@ "EditReleaseProfile": "Edit Release Profile", "EditRemotePathMapping": "Edit Remote Path Mapping", "EditRestriction": "Edit Restriction", + "EditSelectedCustomFormats": "Edit Selected Custom Formats", "EditSelectedDownloadClients": "Edit Selected Download Clients", "EditSelectedImportLists": "Edit Selected Import Lists", "EditSelectedIndexers": "Edit Selected Indexers", @@ -1106,6 +1110,7 @@ "Lowercase": "Lowercase", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "ManageClients": "Manage Clients", + "ManageCustomFormats": "Manage Custom Formats", "ManageDownloadClients": "Manage Download Clients", "ManageEpisodes": "Manage Episodes", "ManageEpisodesSeason": "Manage Episodes files in this season", @@ -1255,6 +1260,7 @@ "NoBlocklistItems": "No blocklist items", "NoChange": "No Change", "NoChanges": "No Changes", + "NoCustomFormatsFound": "No custom formats found", "NoDelay": "No Delay", "NoDownloadClientsFound": "No download clients found", "NoEpisodeHistory": "No episode history", diff --git a/src/Sonarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs b/src/Sonarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs new file mode 100644 index 000000000..18cf16301 --- /dev/null +++ b/src/Sonarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Sonarr.Api.V3.CustomFormats +{ + public class CustomFormatBulkResource + { + public HashSet<int> Ids { get; set; } = new (); + public bool? IncludeCustomFormatWhenRenaming { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs b/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs index 4b560cd7e..3726e7480 100644 --- a/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs +++ b/src/Sonarr.Api.V3/CustomFormats/CustomFormatController.cs @@ -47,6 +47,13 @@ namespace Sonarr.Api.V3.CustomFormats return _formatService.GetById(id).ToResource(true); } + [HttpGet] + [Produces("application/json")] + public List<CustomFormatResource> GetAll() + { + return _formatService.All().ToResource(true); + } + [RestPostById] [Consumes("application/json")] public ActionResult<CustomFormatResource> Create([FromBody] CustomFormatResource customFormatResource) @@ -71,11 +78,26 @@ namespace Sonarr.Api.V3.CustomFormats return Accepted(model.Id); } - [HttpGet] + [HttpPut("bulk")] + [Consumes("application/json")] [Produces("application/json")] - public List<CustomFormatResource> GetAll() + public virtual ActionResult<CustomFormatResource> Update([FromBody] CustomFormatBulkResource resource) { - return _formatService.All().ToResource(true); + if (!resource.Ids.Any()) + { + throw new BadRequestException("ids must be provided"); + } + + var customFormats = resource.Ids.Select(id => _formatService.GetById(id)).ToList(); + + customFormats.ForEach(existing => + { + existing.IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming ?? existing.IncludeCustomFormatWhenRenaming; + }); + + _formatService.Update(customFormats); + + return Accepted(customFormats.ConvertAll(cf => cf.ToResource(true))); } [RestDeleteById] @@ -84,12 +106,21 @@ namespace Sonarr.Api.V3.CustomFormats _formatService.Delete(id); } + [HttpDelete("bulk")] + [Consumes("application/json")] + public virtual object DeleteFormats([FromBody] CustomFormatBulkResource resource) + { + _formatService.Delete(resource.Ids.ToList()); + + return new { }; + } + [HttpGet("schema")] public object GetTemplates() { var schema = _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList(); - var presets = GetPresets(); + var presets = GetPresets().ToList(); foreach (var item in schema) { From 4548dcdf97546f02a437a447e452e94ff50dca3a Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 26 Aug 2024 00:27:17 +0000 Subject: [PATCH 495/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 45de4e78c..bc562f2fe 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -6671,20 +6671,10 @@ "200": { "description": "OK", "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/SeriesResource" - } - }, "application/json": { "schema": { "$ref": "#/components/schemas/SeriesResource" } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/SeriesResource" - } } } } @@ -6763,20 +6753,10 @@ "200": { "description": "OK", "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/SeriesResource" - } - }, "application/json": { "schema": { "$ref": "#/components/schemas/SeriesResource" } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/SeriesResource" - } } } } From 041fdd3929ac14f56cc45e7478cc759e256e9c2f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 20 Aug 2024 05:59:32 +0300 Subject: [PATCH 496/762] Convert Episode and Season search to TypeScript Co-authored-by: Mark McDowall <markus.mcd5@gmail.com> --- frontend/src/App/State/AppState.ts | 6 +- frontend/src/App/State/WantedAppState.ts | 13 + frontend/src/Commands/Command.ts | 1 + frontend/src/Episode/Episode.ts | 1 + frontend/src/Episode/EpisodeDetailsModal.js | 60 ----- frontend/src/Episode/EpisodeDetailsModal.tsx | 52 ++++ .../src/Episode/EpisodeDetailsModalContent.js | 222 ------------------ .../Episode/EpisodeDetailsModalContent.tsx | 204 ++++++++++++++++ .../EpisodeDetailsModalContentConnector.js | 101 -------- frontend/src/Episode/EpisodeDetailsTab.ts | 3 + frontend/src/Episode/EpisodeFormats.js | 33 --- frontend/src/Episode/EpisodeFormats.tsx | 22 ++ frontend/src/Episode/EpisodeSearchCell.js | 86 ------- frontend/src/Episode/EpisodeSearchCell.tsx | 75 ++++++ .../src/Episode/EpisodeSearchCellConnector.js | 50 ---- .../{EpisodeStatus.js => EpisodeStatus.tsx} | 50 ++-- .../src/Episode/EpisodeStatusConnector.js | 53 ----- frontend/src/Episode/EpisodeTitleLink.tsx | 3 +- frontend/src/Episode/SceneInfo.js | 130 ---------- frontend/src/Episode/SceneInfo.tsx | 168 +++++++++++++ .../{EpisodeAiring.js => EpisodeAiring.tsx} | 65 ++--- .../Episode/Summary/EpisodeAiringConnector.js | 20 -- .../src/Episode/Summary/EpisodeFileRow.js | 205 ---------------- .../src/Episode/Summary/EpisodeFileRow.tsx | 149 ++++++++++++ .../src/Episode/Summary/EpisodeSummary.js | 198 ---------------- .../src/Episode/Summary/EpisodeSummary.tsx | 161 +++++++++++++ .../Summary/EpisodeSummaryConnector.js | 109 --------- ...{episodeEntities.js => episodeEntities.ts} | 4 +- frontend/src/Episode/useEpisode.ts | 34 ++- frontend/src/EpisodeFile/EpisodeFile.ts | 1 + frontend/src/EpisodeFile/useEpisodeFile.ts | 18 ++ .../Interactive/InteractiveImportRow.tsx | 5 +- .../InteractiveImport/InteractiveImport.ts | 3 +- .../InteractiveSearchConnector.js | 5 +- frontend/src/Series/Details/EpisodeRow.js | 10 +- .../src/Series/Details/SeriesDetailsSeason.js | 4 +- .../Search/SeasonInteractiveSearchModal.js | 38 --- .../Search/SeasonInteractiveSearchModal.tsx | 55 +++++ .../SeasonInteractiveSearchModalConnector.js | 59 ----- ...> SeasonInteractiveSearchModalContent.tsx} | 38 ++- frontend/src/Series/Series.ts | 1 + .../Selectors/createQueueItemSelector.ts | 13 + .../Store/Selectors/createSeriesSelector.js | 3 +- .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 8 +- frontend/src/Wanted/Missing/MissingRow.js | 8 +- package.json | 2 +- yarn.lock | 8 +- 47 files changed, 1082 insertions(+), 1475 deletions(-) create mode 100644 frontend/src/App/State/WantedAppState.ts delete mode 100644 frontend/src/Episode/EpisodeDetailsModal.js create mode 100644 frontend/src/Episode/EpisodeDetailsModal.tsx delete mode 100644 frontend/src/Episode/EpisodeDetailsModalContent.js create mode 100644 frontend/src/Episode/EpisodeDetailsModalContent.tsx delete mode 100644 frontend/src/Episode/EpisodeDetailsModalContentConnector.js create mode 100644 frontend/src/Episode/EpisodeDetailsTab.ts delete mode 100644 frontend/src/Episode/EpisodeFormats.js create mode 100644 frontend/src/Episode/EpisodeFormats.tsx delete mode 100644 frontend/src/Episode/EpisodeSearchCell.js create mode 100644 frontend/src/Episode/EpisodeSearchCell.tsx delete mode 100644 frontend/src/Episode/EpisodeSearchCellConnector.js rename frontend/src/Episode/{EpisodeStatus.js => EpisodeStatus.tsx} (68%) delete mode 100644 frontend/src/Episode/EpisodeStatusConnector.js delete mode 100644 frontend/src/Episode/SceneInfo.js create mode 100644 frontend/src/Episode/SceneInfo.tsx rename frontend/src/Episode/Summary/{EpisodeAiring.js => EpisodeAiring.tsx} (50%) delete mode 100644 frontend/src/Episode/Summary/EpisodeAiringConnector.js delete mode 100644 frontend/src/Episode/Summary/EpisodeFileRow.js create mode 100644 frontend/src/Episode/Summary/EpisodeFileRow.tsx delete mode 100644 frontend/src/Episode/Summary/EpisodeSummary.js create mode 100644 frontend/src/Episode/Summary/EpisodeSummary.tsx delete mode 100644 frontend/src/Episode/Summary/EpisodeSummaryConnector.js rename frontend/src/Episode/{episodeEntities.js => episodeEntities.ts} (91%) create mode 100644 frontend/src/EpisodeFile/useEpisodeFile.ts delete mode 100644 frontend/src/Series/Search/SeasonInteractiveSearchModal.js create mode 100644 frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx delete mode 100644 frontend/src/Series/Search/SeasonInteractiveSearchModalConnector.js rename frontend/src/Series/Search/{SeasonInteractiveSearchModalContent.js => SeasonInteractiveSearchModalContent.tsx} (59%) diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 212a24ad1..39520d971 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,10 +1,10 @@ -import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import BlocklistAppState from './BlocklistAppState'; import CalendarAppState from './CalendarAppState'; import CommandAppState from './CommandAppState'; import EpisodeFilesAppState from './EpisodeFilesAppState'; import EpisodesAppState from './EpisodesAppState'; import HistoryAppState from './HistoryAppState'; +import InteractiveImportAppState from './InteractiveImportAppState'; import ParseAppState from './ParseAppState'; import QueueAppState from './QueueAppState'; import RootFolderAppState from './RootFolderAppState'; @@ -12,6 +12,7 @@ import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; import SettingsAppState from './SettingsAppState'; import SystemAppState from './SystemAppState'; import TagsAppState from './TagsAppState'; +import WantedAppState from './WantedAppState'; interface FilterBuilderPropOption { id: string; @@ -62,8 +63,8 @@ interface AppState { blocklist: BlocklistAppState; calendar: CalendarAppState; commands: CommandAppState; - episodes: EpisodesAppState; episodeFiles: EpisodeFilesAppState; + episodes: EpisodesAppState; episodesSelection: EpisodesAppState; history: HistoryAppState; interactiveImport: InteractiveImportAppState; @@ -75,6 +76,7 @@ interface AppState { settings: SettingsAppState; system: SystemAppState; tags: TagsAppState; + wanted: WantedAppState; } export default AppState; diff --git a/frontend/src/App/State/WantedAppState.ts b/frontend/src/App/State/WantedAppState.ts new file mode 100644 index 000000000..18a0fbd33 --- /dev/null +++ b/frontend/src/App/State/WantedAppState.ts @@ -0,0 +1,13 @@ +import AppSectionState from 'App/State/AppSectionState'; +import Episode from 'Episode/Episode'; + +interface WantedCutoffUnmetAppState extends AppSectionState<Episode> {} + +interface WantedMissingAppState extends AppSectionState<Episode> {} + +interface WantedAppState { + cutoffUnmet: WantedCutoffUnmetAppState; + missing: WantedMissingAppState; +} + +export default WantedAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index ed0a449ab..cd875d56b 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -26,6 +26,7 @@ export interface CommandBody { seriesId?: number; seriesIds?: number[]; seasonNumber?: number; + episodeIds?: number[]; [key: string]: string | number | boolean | number[] | undefined; } diff --git a/frontend/src/Episode/Episode.ts b/frontend/src/Episode/Episode.ts index 5df98e889..87ae86657 100644 --- a/frontend/src/Episode/Episode.ts +++ b/frontend/src/Episode/Episode.ts @@ -19,6 +19,7 @@ interface Episode extends ModelBase { episodeFile?: object; hasFile: boolean; monitored: boolean; + grabbed?: boolean; unverifiedSceneNumbering: boolean; endTime?: string; grabDate?: string; diff --git a/frontend/src/Episode/EpisodeDetailsModal.js b/frontend/src/Episode/EpisodeDetailsModal.js deleted file mode 100644 index 0e9583e3a..000000000 --- a/frontend/src/Episode/EpisodeDetailsModal.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EpisodeDetailsModalContentConnector from './EpisodeDetailsModalContentConnector'; - -class EpisodeDetailsModal extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - closeOnBackgroundClick: props.selectedTab !== 'search' - }; - } - - // - // Listeners - - onTabChange = (isSearch) => { - this.setState({ closeOnBackgroundClick: !isSearch }); - }; - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - <Modal - isOpen={isOpen} - size={sizes.EXTRA_EXTRA_LARGE} - closeOnBackgroundClick={this.state.closeOnBackgroundClick} - onModalClose={onModalClose} - > - <EpisodeDetailsModalContentConnector - {...otherProps} - onTabChange={this.onTabChange} - onModalClose={onModalClose} - /> - </Modal> - ); - } -} - -EpisodeDetailsModal.propTypes = { - selectedTab: PropTypes.string, - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EpisodeDetailsModal; diff --git a/frontend/src/Episode/EpisodeDetailsModal.tsx b/frontend/src/Episode/EpisodeDetailsModal.tsx new file mode 100644 index 000000000..6bd1e32fb --- /dev/null +++ b/frontend/src/Episode/EpisodeDetailsModal.tsx @@ -0,0 +1,52 @@ +import React, { useCallback, useState } from 'react'; +import Modal from 'Components/Modal/Modal'; +import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab'; +import { EpisodeEntities } from 'Episode/useEpisode'; +import { sizes } from 'Helpers/Props'; +import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; + +interface EpisodeDetailsModalProps { + isOpen: boolean; + episodeId: number; + episodeEntity: EpisodeEntities; + seriesId: number; + episodeTitle: string; + isSaving?: boolean; + showOpenSeriesButton?: boolean; + selectedTab?: EpisodeDetailsTab; + startInteractiveSearch?: boolean; + onModalClose(): void; +} + +function EpisodeDetailsModal(props: EpisodeDetailsModalProps) { + const { selectedTab, isOpen, onModalClose, ...otherProps } = props; + + const [closeOnBackgroundClick, setCloseOnBackgroundClick] = useState( + selectedTab !== 'search' + ); + + const handleTabChange = useCallback( + (isSearch: boolean) => { + setCloseOnBackgroundClick(!isSearch); + }, + [setCloseOnBackgroundClick] + ); + + return ( + <Modal + isOpen={isOpen} + size={sizes.EXTRA_EXTRA_LARGE} + closeOnBackgroundClick={closeOnBackgroundClick} + onModalClose={onModalClose} + > + <EpisodeDetailsModalContent + {...otherProps} + selectedTab={selectedTab} + onTabChange={handleTabChange} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +export default EpisodeDetailsModal; diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.js b/frontend/src/Episode/EpisodeDetailsModalContent.js deleted file mode 100644 index cd5f37fab..000000000 --- a/frontend/src/Episode/EpisodeDetailsModalContent.js +++ /dev/null @@ -1,222 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; -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 MonitorToggleButton from 'Components/MonitorToggleButton'; -import episodeEntities from 'Episode/episodeEntities'; -import translate from 'Utilities/String/translate'; -import EpisodeHistoryConnector from './History/EpisodeHistoryConnector'; -import EpisodeSearchConnector from './Search/EpisodeSearchConnector'; -import SeasonEpisodeNumber from './SeasonEpisodeNumber'; -import EpisodeSummaryConnector from './Summary/EpisodeSummaryConnector'; -import styles from './EpisodeDetailsModalContent.css'; - -const tabs = [ - 'details', - 'history', - 'search' -]; - -class EpisodeDetailsModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - selectedTab: props.selectedTab - }; - } - - // - // Listeners - - onTabSelect = (index, lastIndex) => { - const selectedTab = tabs[index]; - this.props.onTabChange(selectedTab === 'search'); - this.setState({ selectedTab }); - }; - - // - // Render - - render() { - const { - episodeId, - episodeEntity, - episodeFileId, - seriesId, - seriesTitle, - titleSlug, - seriesMonitored, - seriesType, - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - episodeTitle, - airDate, - monitored, - isSaving, - showOpenSeriesButton, - startInteractiveSearch, - onMonitorEpisodePress, - onModalClose - } = this.props; - - const seriesLink = `/series/${titleSlug}`; - - return ( - <ModalContent - onModalClose={onModalClose} - > - <ModalHeader> - <MonitorToggleButton - className={styles.toggleButton} - id={episodeId} - monitored={monitored} - size={18} - isDisabled={!seriesMonitored} - isSaving={isSaving} - onPress={onMonitorEpisodePress} - /> - - <span className={styles.seriesTitle}> - {seriesTitle} - </span> - - <span className={styles.separator}>-</span> - - <SeasonEpisodeNumber - seasonNumber={seasonNumber} - episodeNumber={episodeNumber} - absoluteEpisodeNumber={absoluteEpisodeNumber} - airDate={airDate} - seriesType={seriesType} - /> - - <span className={styles.separator}>-</span> - - {episodeTitle} - </ModalHeader> - - <ModalBody> - <Tabs - className={styles.tabs} - selectedIndex={tabs.indexOf(this.state.selectedTab)} - onSelect={this.onTabSelect} - > - <TabList - className={styles.tabList} - > - <Tab - className={styles.tab} - selectedClassName={styles.selectedTab} - > - {translate('Details')} - </Tab> - - <Tab - className={styles.tab} - selectedClassName={styles.selectedTab} - > - {translate('History')} - </Tab> - - <Tab - className={styles.tab} - selectedClassName={styles.selectedTab} - > - {translate('Search')} - </Tab> - </TabList> - - <TabPanel> - <div className={styles.tabContent}> - <EpisodeSummaryConnector - episodeId={episodeId} - episodeEntity={episodeEntity} - episodeFileId={episodeFileId} - seriesId={seriesId} - /> - </div> - </TabPanel> - - <TabPanel> - <div className={styles.tabContent}> - <EpisodeHistoryConnector - episodeId={episodeId} - /> - </div> - </TabPanel> - - <TabPanel> - {/* Don't wrap in tabContent so we not have a top margin */} - <EpisodeSearchConnector - episodeId={episodeId} - startInteractiveSearch={startInteractiveSearch} - onModalClose={onModalClose} - /> - </TabPanel> - </Tabs> - </ModalBody> - - <ModalFooter> - { - showOpenSeriesButton && - <Button - className={styles.openSeriesButton} - to={seriesLink} - onPress={onModalClose} - > - {translate('OpenSeries')} - </Button> - } - - <Button - onPress={onModalClose} - > - {translate('Close')} - </Button> - </ModalFooter> - </ModalContent> - ); - } -} - -EpisodeDetailsModalContent.propTypes = { - episodeId: PropTypes.number.isRequired, - episodeEntity: PropTypes.string.isRequired, - episodeFileId: PropTypes.number, - seriesId: PropTypes.number.isRequired, - seriesTitle: PropTypes.string.isRequired, - titleSlug: PropTypes.string.isRequired, - seriesMonitored: PropTypes.bool.isRequired, - seriesType: PropTypes.string.isRequired, - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - airDate: PropTypes.string.isRequired, - episodeTitle: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - isSaving: PropTypes.bool, - showOpenSeriesButton: PropTypes.bool, - selectedTab: PropTypes.string.isRequired, - startInteractiveSearch: PropTypes.bool.isRequired, - onMonitorEpisodePress: PropTypes.func.isRequired, - onTabChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -EpisodeDetailsModalContent.defaultProps = { - selectedTab: 'details', - episodeEntity: episodeEntities.EPISODES, - startInteractiveSearch: false -}; - -export default EpisodeDetailsModalContent; diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.tsx b/frontend/src/Episode/EpisodeDetailsModalContent.tsx new file mode 100644 index 000000000..d049ab9f7 --- /dev/null +++ b/frontend/src/Episode/EpisodeDetailsModalContent.tsx @@ -0,0 +1,204 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; +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 MonitorToggleButton from 'Components/MonitorToggleButton'; +import Episode from 'Episode/Episode'; +import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab'; +import episodeEntities from 'Episode/episodeEntities'; +import useEpisode, { EpisodeEntities } from 'Episode/useEpisode'; +import Series from 'Series/Series'; +import useSeries from 'Series/useSeries'; +import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; +import { + cancelFetchReleases, + clearReleases, +} from 'Store/Actions/releaseActions'; +import translate from 'Utilities/String/translate'; +import EpisodeHistoryConnector from './History/EpisodeHistoryConnector'; +import EpisodeSearchConnector from './Search/EpisodeSearchConnector'; +import SeasonEpisodeNumber from './SeasonEpisodeNumber'; +import EpisodeSummary from './Summary/EpisodeSummary'; +import styles from './EpisodeDetailsModalContent.css'; + +const TABS: EpisodeDetailsTab[] = ['details', 'history', 'search']; + +export interface EpisodeDetailsModalContentProps { + episodeId: number; + episodeEntity: EpisodeEntities; + seriesId: number; + episodeTitle: string; + isSaving?: boolean; + showOpenSeriesButton?: boolean; + selectedTab?: EpisodeDetailsTab; + startInteractiveSearch?: boolean; + onTabChange(isSearch: boolean): void; + onModalClose(): void; +} + +function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) { + const { + episodeId, + episodeEntity = episodeEntities.EPISODES, + seriesId, + episodeTitle, + isSaving = false, + showOpenSeriesButton = false, + startInteractiveSearch = false, + selectedTab = 'details', + onTabChange, + onModalClose, + } = props; + + const dispatch = useDispatch(); + + const [currentlySelectedTab, setCurrentlySelectedTab] = useState(selectedTab); + + const { + title: seriesTitle, + titleSlug, + monitored: seriesMonitored, + seriesType, + } = useSeries(seriesId) as Series; + + const { + episodeFileId, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDate, + monitored, + } = useEpisode(episodeId, episodeEntity) as Episode; + + const handleTabSelect = useCallback( + (selectedIndex: number) => { + const tab = TABS[selectedIndex]; + onTabChange(tab === 'search'); + setCurrentlySelectedTab(tab); + }, + [onTabChange] + ); + + const handleMonitorEpisodePress = useCallback( + (monitored: boolean) => { + dispatch( + toggleEpisodeMonitored({ + episodeEntity, + episodeId, + monitored, + }) + ); + }, + [episodeEntity, episodeId, dispatch] + ); + + useEffect(() => { + return () => { + // Clear pending releases here, so we can reshow the search + // results even after switching tabs. + dispatch(cancelFetchReleases()); + dispatch(clearReleases()); + }; + }, [dispatch]); + + const seriesLink = `/series/${titleSlug}`; + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader> + <MonitorToggleButton + id={episodeId} + monitored={monitored} + size={18} + isDisabled={!seriesMonitored} + isSaving={isSaving} + onPress={handleMonitorEpisodePress} + /> + + <span className={styles.seriesTitle}>{seriesTitle}</span> + + <span className={styles.separator}>-</span> + + <SeasonEpisodeNumber + seasonNumber={seasonNumber} + episodeNumber={episodeNumber} + absoluteEpisodeNumber={absoluteEpisodeNumber} + airDate={airDate} + seriesType={seriesType} + /> + + <span className={styles.separator}>-</span> + + {episodeTitle} + </ModalHeader> + + <ModalBody> + <Tabs + className={styles.tabs} + selectedIndex={TABS.indexOf(currentlySelectedTab)} + onSelect={handleTabSelect} + > + <TabList className={styles.tabList}> + <Tab className={styles.tab} selectedClassName={styles.selectedTab}> + {translate('Details')} + </Tab> + + <Tab className={styles.tab} selectedClassName={styles.selectedTab}> + {translate('History')} + </Tab> + + <Tab className={styles.tab} selectedClassName={styles.selectedTab}> + {translate('Search')} + </Tab> + </TabList> + + <TabPanel> + <div className={styles.tabContent}> + <EpisodeSummary + episodeId={episodeId} + episodeEntity={episodeEntity} + episodeFileId={episodeFileId} + seriesId={seriesId} + /> + </div> + </TabPanel> + + <TabPanel> + <div className={styles.tabContent}> + <EpisodeHistoryConnector episodeId={episodeId} /> + </div> + </TabPanel> + + <TabPanel> + {/* Don't wrap in tabContent so we not have a top margin */} + <EpisodeSearchConnector + episodeId={episodeId} + startInteractiveSearch={startInteractiveSearch} + onModalClose={onModalClose} + /> + </TabPanel> + </Tabs> + </ModalBody> + + <ModalFooter> + {showOpenSeriesButton && ( + <Button + className={styles.openSeriesButton} + to={seriesLink} + onPress={onModalClose} + > + {translate('OpenSeries')} + </Button> + )} + + <Button onPress={onModalClose}>{translate('Close')}</Button> + </ModalFooter> + </ModalContent> + ); +} + +export default EpisodeDetailsModalContent; diff --git a/frontend/src/Episode/EpisodeDetailsModalContentConnector.js b/frontend/src/Episode/EpisodeDetailsModalContentConnector.js deleted file mode 100644 index 773933638..000000000 --- a/frontend/src/Episode/EpisodeDetailsModalContentConnector.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import episodeEntities from 'Episode/episodeEntities'; -import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; -import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; -import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; - -function createMapStateToProps() { - return createSelector( - createEpisodeSelector(), - createSeriesSelector(), - (episode, series) => { - const { - title: seriesTitle, - titleSlug, - monitored: seriesMonitored, - seriesType - } = series; - - return { - seriesTitle, - titleSlug, - seriesMonitored, - seriesType, - ...episode - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchCancelFetchReleases() { - dispatch(cancelFetchReleases()); - }, - - dispatchClearReleases() { - dispatch(clearReleases()); - }, - - onMonitorEpisodePress(monitored) { - const { - episodeId, - episodeEntity - } = props; - - dispatch(toggleEpisodeMonitored({ - episodeEntity, - episodeId, - monitored - })); - } - }; -} - -class EpisodeDetailsModalContentConnector extends Component { - - // - // Lifecycle - - componentWillUnmount() { - // Clear pending releases here, so we can reshow the search - // results even after switching tabs. - - this.props.dispatchCancelFetchReleases(); - this.props.dispatchClearReleases(); - } - - // - // Render - - render() { - const { - dispatchCancelFetchReleases, - dispatchClearReleases, - ...otherProps - } = this.props; - - return ( - <EpisodeDetailsModalContent {...otherProps} /> - ); - } -} - -EpisodeDetailsModalContentConnector.propTypes = { - episodeId: PropTypes.number.isRequired, - episodeEntity: PropTypes.string.isRequired, - seriesId: PropTypes.number.isRequired, - dispatchCancelFetchReleases: PropTypes.func.isRequired, - dispatchClearReleases: PropTypes.func.isRequired -}; - -EpisodeDetailsModalContentConnector.defaultProps = { - episodeEntity: episodeEntities.EPISODES -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeDetailsModalContentConnector); diff --git a/frontend/src/Episode/EpisodeDetailsTab.ts b/frontend/src/Episode/EpisodeDetailsTab.ts new file mode 100644 index 000000000..b568f24fa --- /dev/null +++ b/frontend/src/Episode/EpisodeDetailsTab.ts @@ -0,0 +1,3 @@ +type EpisodeDetailsTab = 'details' | 'history' | 'search'; + +export default EpisodeDetailsTab; diff --git a/frontend/src/Episode/EpisodeFormats.js b/frontend/src/Episode/EpisodeFormats.js deleted file mode 100644 index 1801767bd..000000000 --- a/frontend/src/Episode/EpisodeFormats.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import { kinds } from 'Helpers/Props'; - -function EpisodeFormats({ formats }) { - return ( - <div> - { - formats.map((format) => { - return ( - <Label - key={format.id} - kind={kinds.INFO} - > - {format.name} - </Label> - ); - }) - } - </div> - ); -} - -EpisodeFormats.propTypes = { - formats: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -EpisodeFormats.defaultProps = { - formats: [] -}; - -export default EpisodeFormats; diff --git a/frontend/src/Episode/EpisodeFormats.tsx b/frontend/src/Episode/EpisodeFormats.tsx new file mode 100644 index 000000000..d774ad907 --- /dev/null +++ b/frontend/src/Episode/EpisodeFormats.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Label from 'Components/Label'; +import { kinds } from 'Helpers/Props'; +import CustomFormat from 'typings/CustomFormat'; + +interface EpisodeFormatsProps { + formats: CustomFormat[]; +} + +function EpisodeFormats({ formats }: EpisodeFormatsProps) { + return ( + <div> + {formats.map(({ id, name }) => ( + <Label key={id} kind={kinds.INFO}> + {name} + </Label> + ))} + </div> + ); +} + +export default EpisodeFormats; diff --git a/frontend/src/Episode/EpisodeSearchCell.js b/frontend/src/Episode/EpisodeSearchCell.js deleted file mode 100644 index 3ec76d365..000000000 --- a/frontend/src/Episode/EpisodeSearchCell.js +++ /dev/null @@ -1,86 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EpisodeDetailsModal from './EpisodeDetailsModal'; -import styles from './EpisodeSearchCell.css'; - -class EpisodeSearchCell extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onManualSearchPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - // - // Render - - render() { - const { - episodeId, - seriesId, - episodeTitle, - isSearching, - onSearchPress, - ...otherProps - } = this.props; - - return ( - <TableRowCell className={styles.episodeSearchCell}> - <SpinnerIconButton - name={icons.SEARCH} - isSpinning={isSearching} - onPress={onSearchPress} - title={translate('AutomaticSearch')} - /> - - <IconButton - name={icons.INTERACTIVE} - onPress={this.onManualSearchPress} - title={translate('InteractiveSearch')} - /> - - <EpisodeDetailsModal - isOpen={this.state.isDetailsModalOpen} - episodeId={episodeId} - seriesId={seriesId} - episodeTitle={episodeTitle} - selectedTab="search" - startInteractiveSearch={true} - onModalClose={this.onDetailsModalClose} - {...otherProps} - /> - </TableRowCell> - ); - } -} - -EpisodeSearchCell.propTypes = { - episodeId: PropTypes.number.isRequired, - seriesId: PropTypes.number.isRequired, - episodeTitle: PropTypes.string.isRequired, - isSearching: PropTypes.bool.isRequired, - onSearchPress: PropTypes.func.isRequired -}; - -export default EpisodeSearchCell; diff --git a/frontend/src/Episode/EpisodeSearchCell.tsx b/frontend/src/Episode/EpisodeSearchCell.tsx new file mode 100644 index 000000000..65ceb5d3a --- /dev/null +++ b/frontend/src/Episode/EpisodeSearchCell.tsx @@ -0,0 +1,75 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { EPISODE_SEARCH } from 'Commands/commandNames'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import { EpisodeEntities } from 'Episode/useEpisode'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; +import translate from 'Utilities/String/translate'; +import EpisodeDetailsModal from './EpisodeDetailsModal'; +import styles from './EpisodeSearchCell.css'; + +interface EpisodeSearchCellProps { + episodeId: number; + episodeEntity: EpisodeEntities; + seriesId: number; + episodeTitle: string; +} + +function EpisodeSearchCell(props: EpisodeSearchCellProps) { + const { episodeId, episodeEntity, seriesId, episodeTitle } = props; + + const executingCommands = useSelector(createExecutingCommandsSelector()); + const isSearching = executingCommands.some(({ name, body }) => { + const { episodeIds = [] } = body; + return name === EPISODE_SEARCH && episodeIds.indexOf(episodeId) > -1; + }); + + const dispatch = useDispatch(); + + const [isDetailsModalOpen, setDetailsModalOpen, setDetailsModalClosed] = + useModalOpenState(false); + + const handleSearchPress = useCallback(() => { + dispatch( + executeCommand({ + name: EPISODE_SEARCH, + episodeIds: [episodeId], + }) + ); + }, [episodeId, dispatch]); + + return ( + <TableRowCell className={styles.episodeSearchCell}> + <SpinnerIconButton + name={icons.SEARCH} + isSpinning={isSearching} + title={translate('AutomaticSearch')} + onPress={handleSearchPress} + /> + + <IconButton + name={icons.INTERACTIVE} + title={translate('InteractiveSearch')} + onPress={setDetailsModalOpen} + /> + + <EpisodeDetailsModal + isOpen={isDetailsModalOpen} + episodeId={episodeId} + episodeEntity={episodeEntity} + seriesId={seriesId} + episodeTitle={episodeTitle} + selectedTab="search" + startInteractiveSearch={true} + onModalClose={setDetailsModalClosed} + /> + </TableRowCell> + ); +} + +export default EpisodeSearchCell; diff --git a/frontend/src/Episode/EpisodeSearchCellConnector.js b/frontend/src/Episode/EpisodeSearchCellConnector.js deleted file mode 100644 index f1fde7cd2..000000000 --- a/frontend/src/Episode/EpisodeSearchCellConnector.js +++ /dev/null @@ -1,50 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import { isCommandExecuting } from 'Utilities/Command'; -import EpisodeSearchCell from './EpisodeSearchCell'; - -function createMapStateToProps() { - return createSelector( - (state, { episodeId }) => episodeId, - (state, { sceneSeasonNumber }) => sceneSeasonNumber, - createSeriesSelector(), - createCommandsSelector(), - (episodeId, sceneSeasonNumber, series, commands) => { - const isSearching = commands.some((command) => { - const episodeSearch = command.name === commandNames.EPISODE_SEARCH; - - if (!episodeSearch) { - return false; - } - - return ( - isCommandExecuting(command) && - command.body.episodeIds.indexOf(episodeId) > -1 - ); - }); - - return { - seriesMonitored: series.monitored, - seriesType: series.seriesType, - isSearching - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onSearchPress(name, path) { - dispatch(executeCommand({ - name: commandNames.EPISODE_SEARCH, - episodeIds: [props.episodeId] - })); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSearchCell); diff --git a/frontend/src/Episode/EpisodeStatus.js b/frontend/src/Episode/EpisodeStatus.tsx similarity index 68% rename from frontend/src/Episode/EpisodeStatus.js rename to frontend/src/Episode/EpisodeStatus.tsx index a70d877b2..b56e32157 100644 --- a/frontend/src/Episode/EpisodeStatus.js +++ b/frontend/src/Episode/EpisodeStatus.tsx @@ -1,34 +1,44 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import { useSelector } from 'react-redux'; import QueueDetails from 'Activity/Queue/QueueDetails'; import Icon from 'Components/Icon'; import ProgressBar from 'Components/ProgressBar'; +import Episode from 'Episode/Episode'; +import useEpisode, { EpisodeEntities } from 'Episode/useEpisode'; +import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; import { icons, kinds, sizes } from 'Helpers/Props'; +import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector'; import isBefore from 'Utilities/Date/isBefore'; import translate from 'Utilities/String/translate'; import EpisodeQuality from './EpisodeQuality'; import styles from './EpisodeStatus.css'; -function EpisodeStatus(props) { +interface EpisodeStatusProps { + episodeId: number; + episodeEntity?: EpisodeEntities; + episodeFileId: number; +} + +function EpisodeStatus(props: EpisodeStatusProps) { + const { episodeId, episodeEntity = 'episodes', episodeFileId } = props; + const { airDateUtc, monitored, - grabbed, - queueItem, - episodeFile - } = props; + grabbed = false, + } = useEpisode(episodeId, episodeEntity) as Episode; + + const queueItem = useSelector(createQueueItemSelectorForHook(episodeId)); + const episodeFile = useEpisodeFile(episodeFileId); const hasEpisodeFile = !!episodeFile; const isQueued = !!queueItem; const hasAired = isBefore(airDateUtc); if (isQueued) { - const { - sizeleft, - size - } = queueItem; + const { sizeleft, size } = queueItem; - const progress = size ? (100 - sizeleft / size * 100) : 0; + const progress = size ? 100 - (sizeleft / size) * 100 : 0; return ( <div className={styles.center}> @@ -76,10 +86,7 @@ function EpisodeStatus(props) { if (!airDateUtc) { return ( <div className={styles.center}> - <Icon - name={icons.TBA} - title={translate('Tba')} - /> + <Icon name={icons.TBA} title={translate('Tba')} /> </div> ); } @@ -109,20 +116,9 @@ function EpisodeStatus(props) { return ( <div className={styles.center}> - <Icon - name={icons.NOT_AIRED} - title={translate('EpisodeHasNotAired')} - /> + <Icon name={icons.NOT_AIRED} title={translate('EpisodeHasNotAired')} /> </div> ); } -EpisodeStatus.propTypes = { - airDateUtc: PropTypes.string, - monitored: PropTypes.bool.isRequired, - grabbed: PropTypes.bool, - queueItem: PropTypes.object, - episodeFile: PropTypes.object -}; - export default EpisodeStatus; diff --git a/frontend/src/Episode/EpisodeStatusConnector.js b/frontend/src/Episode/EpisodeStatusConnector.js deleted file mode 100644 index ea2d7a1ea..000000000 --- a/frontend/src/Episode/EpisodeStatusConnector.js +++ /dev/null @@ -1,53 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; -import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; -import EpisodeStatus from './EpisodeStatus'; - -function createMapStateToProps() { - return createSelector( - createEpisodeSelector(), - createQueueItemSelector(), - createEpisodeFileSelector(), - (episode, queueItem, episodeFile) => { - const result = _.pick(episode, [ - 'airDateUtc', - 'monitored', - 'grabbed' - ]); - - result.queueItem = queueItem; - result.episodeFile = episodeFile; - - return result; - } - ); -} - -const mapDispatchToProps = { -}; - -class EpisodeStatusConnector extends Component { - - // - // Render - - render() { - return ( - <EpisodeStatus - {...this.props} - /> - ); - } -} - -EpisodeStatusConnector.propTypes = { - episodeId: PropTypes.number.isRequired, - episodeFileId: PropTypes.number.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeStatusConnector); diff --git a/frontend/src/Episode/EpisodeTitleLink.tsx b/frontend/src/Episode/EpisodeTitleLink.tsx index e7455312d..9df6dbf33 100644 --- a/frontend/src/Episode/EpisodeTitleLink.tsx +++ b/frontend/src/Episode/EpisodeTitleLink.tsx @@ -1,13 +1,14 @@ import React, { useCallback, useState } from 'react'; import Link from 'Components/Link/Link'; import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; +import { EpisodeEntities } from 'Episode/useEpisode'; import FinaleType from './FinaleType'; import styles from './EpisodeTitleLink.css'; interface EpisodeTitleLinkProps { episodeId: number; seriesId: number; - episodeEntity: string; + episodeEntity: EpisodeEntities; episodeTitle: string; finaleType?: string; showOpenSeriesButton: boolean; diff --git a/frontend/src/Episode/SceneInfo.js b/frontend/src/Episode/SceneInfo.js deleted file mode 100644 index dc700c98e..000000000 --- a/frontend/src/Episode/SceneInfo.js +++ /dev/null @@ -1,130 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; -import styles from './SceneInfo.css'; - -function SceneInfo(props) { - const { - seasonNumber, - episodeNumber, - sceneSeasonNumber, - sceneEpisodeNumber, - sceneAbsoluteEpisodeNumber, - alternateTitles, - seriesType - } = props; - - const reducedAlternateTitles = alternateTitles.map((alternateTitle) => { - let suffix = ''; - - const altSceneSeasonNumber = sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber; - const altSceneEpisodeNumber = sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber; - - const mappingSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : altSceneSeasonNumber; - const altSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined) ? alternateTitle.sceneSeasonNumber : mappingSeasonNumber; - const altEpisodeNumber = alternateTitle.sceneOrigin === 'tvdb' ? episodeNumber : altSceneEpisodeNumber; - - if (altEpisodeNumber !== altSceneEpisodeNumber) { - suffix = `S${padNumber(altSeasonNumber, 2)}E${padNumber(altEpisodeNumber, 2)}`; - } else if (altSeasonNumber !== altSceneSeasonNumber) { - suffix = `S${padNumber(altSeasonNumber, 2)}`; - } - - return { - alternateTitle, - title: alternateTitle.title, - suffix, - comment: alternateTitle.comment - }; - }); - - const groupedAlternateTitles = _.map(_.groupBy(reducedAlternateTitles, (item) => `${item.title} ${item.suffix}`), (group) => { - return { - title: group[0].title, - suffix: group[0].suffix, - comment: _.uniq(group.map((item) => item.comment)).join('/') - }; - }); - - return ( - <DescriptionList className={styles.descriptionList}> - { - sceneSeasonNumber !== undefined && - <DescriptionListItem - titleClassName={styles.title} - descriptionClassName={styles.description} - title={translate('Season')} - data={sceneSeasonNumber} - /> - } - - { - sceneEpisodeNumber !== undefined && - <DescriptionListItem - titleClassName={styles.title} - descriptionClassName={styles.description} - title={translate('Episode')} - data={sceneEpisodeNumber} - /> - } - - { - seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined && - <DescriptionListItem - titleClassName={styles.title} - descriptionClassName={styles.description} - title={translate('Absolute')} - data={sceneAbsoluteEpisodeNumber} - /> - } - - { - !!alternateTitles.length && - <DescriptionListItem - titleClassName={styles.title} - descriptionClassName={styles.description} - title={groupedAlternateTitles.length === 1 ? translate('Title') : translate('Titles')} - data={ - <div> - { - groupedAlternateTitles.map(({ title, suffix, comment }) => { - return ( - <div - key={`${title} ${suffix}`} - > - {title} - { - suffix && - <span> ({suffix})</span> - } - { - comment && - <span className={styles.comment}> {comment}</span> - } - </div> - ); - }) - } - </div> - } - /> - } - </DescriptionList> - ); -} - -SceneInfo.propTypes = { - seasonNumber: PropTypes.number, - episodeNumber: PropTypes.number, - sceneSeasonNumber: PropTypes.number, - sceneEpisodeNumber: PropTypes.number, - sceneAbsoluteEpisodeNumber: PropTypes.number, - alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, - seriesType: PropTypes.string -}; - -export default SceneInfo; diff --git a/frontend/src/Episode/SceneInfo.tsx b/frontend/src/Episode/SceneInfo.tsx new file mode 100644 index 000000000..173f5d3f4 --- /dev/null +++ b/frontend/src/Episode/SceneInfo.tsx @@ -0,0 +1,168 @@ +import React, { useMemo } from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import { AlternateTitle } from 'Series/Series'; +import padNumber from 'Utilities/Number/padNumber'; +import translate from 'Utilities/String/translate'; +import styles from './SceneInfo.css'; + +interface SceneInfoProps { + seasonNumber?: number; + episodeNumber?: number; + sceneSeasonNumber?: number; + sceneEpisodeNumber?: number; + sceneAbsoluteEpisodeNumber?: number; + alternateTitles: AlternateTitle[]; + seriesType?: string; +} + +function SceneInfo(props: SceneInfoProps) { + const { + seasonNumber, + episodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + alternateTitles, + seriesType, + } = props; + + const groupedAlternateTitles = useMemo(() => { + const reducedAlternateTitles = alternateTitles.map((alternateTitle) => { + let suffix = ''; + + const altSceneSeasonNumber = + sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber; + const altSceneEpisodeNumber = + sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber; + + const mappingSeasonNumber = + alternateTitle.sceneOrigin === 'tvdb' + ? seasonNumber + : altSceneSeasonNumber; + const altSeasonNumber = + alternateTitle.sceneSeasonNumber !== -1 && + alternateTitle.sceneSeasonNumber !== undefined + ? alternateTitle.sceneSeasonNumber + : mappingSeasonNumber; + const altEpisodeNumber = + alternateTitle.sceneOrigin === 'tvdb' + ? episodeNumber + : altSceneEpisodeNumber; + + if (altEpisodeNumber !== altSceneEpisodeNumber) { + suffix = `S${padNumber(altSeasonNumber as number, 2)}E${padNumber( + altEpisodeNumber as number, + 2 + )}`; + } else if (altSeasonNumber !== altSceneSeasonNumber) { + suffix = `S${padNumber(altSeasonNumber as number, 2)}`; + } + + return { + alternateTitle, + title: alternateTitle.title, + suffix, + comment: alternateTitle.comment, + }; + }); + + return Object.values( + reducedAlternateTitles.reduce( + ( + acc: Record< + string, + { title: string; suffix: string; comment: string } + >, + alternateTitle + ) => { + const key = alternateTitle.suffix + ? `${alternateTitle.title} ${alternateTitle.suffix}` + : alternateTitle.title; + const item = acc[key]; + + if (item) { + item.comment = alternateTitle.comment + ? `${item.comment}/${alternateTitle.comment}` + : item.comment; + } else { + acc[key] = { + title: alternateTitle.title, + suffix: alternateTitle.suffix, + comment: alternateTitle.comment ?? '', + }; + } + + return acc; + }, + {} + ) + ); + }, [ + alternateTitles, + seasonNumber, + episodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + ]); + + return ( + <DescriptionList className={styles.descriptionList}> + {sceneSeasonNumber === undefined ? null : ( + <DescriptionListItem + titleClassName={styles.title} + descriptionClassName={styles.description} + title={translate('Season')} + data={sceneSeasonNumber} + /> + )} + + {sceneEpisodeNumber === undefined ? null : ( + <DescriptionListItem + titleClassName={styles.title} + descriptionClassName={styles.description} + title={translate('Episode')} + data={sceneEpisodeNumber} + /> + )} + + {seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined ? ( + <DescriptionListItem + titleClassName={styles.title} + descriptionClassName={styles.description} + title={translate('Absolute')} + data={sceneAbsoluteEpisodeNumber} + /> + ) : null} + + {alternateTitles.length ? ( + <DescriptionListItem + titleClassName={styles.title} + descriptionClassName={styles.description} + title={ + groupedAlternateTitles.length === 1 + ? translate('Title') + : translate('Titles') + } + data={ + <div> + {groupedAlternateTitles.map(({ title, suffix, comment }) => { + return ( + <div key={`${title} ${suffix}`}> + {title} + {suffix && <span> ({suffix})</span>} + {comment ? ( + <span className={styles.comment}> {comment}</span> + ) : null} + </div> + ); + })} + </div> + } + /> + ) : null} + </DescriptionList> + ); +} + +export default SceneInfo; diff --git a/frontend/src/Episode/Summary/EpisodeAiring.js b/frontend/src/Episode/Summary/EpisodeAiring.tsx similarity index 50% rename from frontend/src/Episode/Summary/EpisodeAiring.js rename to frontend/src/Episode/Summary/EpisodeAiring.tsx index 7a6f84e57..fd45167f8 100644 --- a/frontend/src/Episode/Summary/EpisodeAiring.js +++ b/frontend/src/Episode/Summary/EpisodeAiring.tsx @@ -1,28 +1,29 @@ import moment from 'moment'; -import PropTypes from 'prop-types'; import React from 'react'; +import { useSelector } from 'react-redux'; import Label from 'Components/Label'; import { kinds, sizes } from 'Helpers/Props'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import formatTime from 'Utilities/Date/formatTime'; import isInNextWeek from 'Utilities/Date/isInNextWeek'; import isToday from 'Utilities/Date/isToday'; import isTomorrow from 'Utilities/Date/isTomorrow'; import translate from 'Utilities/String/translate'; -function EpisodeAiring(props) { - const { - airDateUtc, - network, - shortDateFormat, - showRelativeDates, - timeFormat - } = props; +interface EpisodeAiringProps { + airDateUtc?: string; + network: string; +} + +function EpisodeAiring(props: EpisodeAiringProps) { + const { airDateUtc, network } = props; + + const { shortDateFormat, showRelativeDates, timeFormat } = useSelector( + createUISettingsSelector() + ); const networkLabel = ( - <Label - kind={kinds.INFO} - size={sizes.MEDIUM} - > + <Label kind={kinds.INFO} size={sizes.MEDIUM}> {network} </Label> ); @@ -31,7 +32,8 @@ function EpisodeAiring(props) { if (!airDateUtc) { return ( <span> - {translate('AirsTbaOn', { networkLabel: '' })}{networkLabel} + {translate('AirsTbaOn', { networkLabel: '' })} + {networkLabel} </span> ); } @@ -41,7 +43,12 @@ function EpisodeAiring(props) { if (!showRelativeDates) { return ( <span> - {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel} + {translate('AirsDateAtTimeOn', { + date: moment(airDateUtc).format(shortDateFormat), + time, + networkLabel: '', + })} + {networkLabel} </span> ); } @@ -49,7 +56,8 @@ function EpisodeAiring(props) { if (isToday(airDateUtc)) { return ( <span> - {translate('AirsTimeOn', { time, networkLabel: '' })}{networkLabel} + {translate('AirsTimeOn', { time, networkLabel: '' })} + {networkLabel} </span> ); } @@ -57,7 +65,8 @@ function EpisodeAiring(props) { if (isTomorrow(airDateUtc)) { return ( <span> - {translate('AirsTomorrowOn', { time, networkLabel: '' })}{networkLabel} + {translate('AirsTomorrowOn', { time, networkLabel: '' })} + {networkLabel} </span> ); } @@ -65,24 +74,26 @@ function EpisodeAiring(props) { if (isInNextWeek(airDateUtc)) { return ( <span> - {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format('dddd'), time, networkLabel: '' })}{networkLabel} + {translate('AirsDateAtTimeOn', { + date: moment(airDateUtc).format('dddd'), + time, + networkLabel: '', + })} + {networkLabel} </span> ); } return ( <span> - {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel} + {translate('AirsDateAtTimeOn', { + date: moment(airDateUtc).format(shortDateFormat), + time, + networkLabel: '', + })} + {networkLabel} </span> ); } -EpisodeAiring.propTypes = { - airDateUtc: PropTypes.string.isRequired, - network: PropTypes.string.isRequired, - shortDateFormat: PropTypes.string.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - timeFormat: PropTypes.string.isRequired -}; - export default EpisodeAiring; diff --git a/frontend/src/Episode/Summary/EpisodeAiringConnector.js b/frontend/src/Episode/Summary/EpisodeAiringConnector.js deleted file mode 100644 index 508467efb..000000000 --- a/frontend/src/Episode/Summary/EpisodeAiringConnector.js +++ /dev/null @@ -1,20 +0,0 @@ -import _ from 'lodash'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import EpisodeAiring from './EpisodeAiring'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return _.pick(uiSettings, [ - 'shortDateFormat', - 'showRelativeDates', - 'timeFormat' - ]); - } - ); -} - -export default connect(createMapStateToProps)(EpisodeAiring); diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.js b/frontend/src/Episode/Summary/EpisodeFileRow.js deleted file mode 100644 index 31c1c93c1..000000000 --- a/frontend/src/Episode/Summary/EpisodeFileRow.js +++ /dev/null @@ -1,205 +0,0 @@ -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 Popover from 'Components/Tooltip/Popover'; -import EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import formatBytes from 'Utilities/Number/formatBytes'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; -import MediaInfo from './MediaInfo'; -import styles from './EpisodeFileRow.css'; - -class EpisodeFileRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isRemoveEpisodeFileModalOpen: false - }; - } - - // - // Listeners - - onRemoveEpisodeFilePress = () => { - this.setState({ isRemoveEpisodeFileModalOpen: true }); - }; - - onConfirmRemoveEpisodeFile = () => { - this.props.onDeleteEpisodeFile(); - - this.setState({ isRemoveEpisodeFileModalOpen: false }); - }; - - onRemoveEpisodeFileModalClose = () => { - this.setState({ isRemoveEpisodeFileModalOpen: false }); - }; - - // - // Render - - render() { - const { - path, - size, - languages, - quality, - customFormats, - customFormatScore, - qualityCutoffNotMet, - mediaInfo, - columns - } = this.props; - - return ( - <TableRow> - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'path') { - return ( - <TableRowCell key={name}> - {path} - </TableRowCell> - ); - } - - if (name === 'size') { - return ( - <TableRowCell key={name}> - {formatBytes(size)} - </TableRowCell> - ); - } - - if (name === 'languages') { - return ( - <TableRowCell - key={name} - className={styles.languages} - > - <EpisodeLanguages languages={languages} /> - </TableRowCell> - ); - } - - if (name === 'quality') { - return ( - <TableRowCell - key={name} - className={styles.quality} - > - <EpisodeQuality - quality={quality} - isCutoffNotMet={qualityCutoffNotMet} - /> - </TableRowCell> - ); - } - - if (name === 'customFormats') { - return ( - <TableRowCell - key={name} - className={styles.customFormats} - > - <EpisodeFormats - formats={customFormats} - /> - </TableRowCell> - ); - } - - if (name === 'customFormatScore') { - return ( - <TableRowCell - key={name} - className={styles.customFormatScore} - > - {formatCustomFormatScore(customFormatScore, customFormats.length)} - </TableRowCell> - ); - } - - if (name === 'actions') { - return ( - <TableRowCell - key={name} - className={styles.actions} - > - { - mediaInfo ? - <Popover - anchor={ - <Icon - name={icons.MEDIA_INFO} - /> - } - title={translate('MediaInfo')} - body={<MediaInfo {...mediaInfo} />} - position={tooltipPositions.LEFT} - /> : - null - } - - <IconButton - title={translate('DeleteEpisodeFromDisk')} - name={icons.REMOVE} - onPress={this.onRemoveEpisodeFilePress} - /> - </TableRowCell> - ); - } - - return null; - }) - } - - <ConfirmModal - isOpen={this.state.isRemoveEpisodeFileModalOpen} - kind={kinds.DANGER} - title={translate('DeleteEpisodeFile')} - message={translate('DeleteEpisodeFileMessage', { path })} - confirmLabel={translate('Delete')} - onConfirm={this.onConfirmRemoveEpisodeFile} - onCancel={this.onRemoveEpisodeFileModalClose} - /> - </TableRow> - ); - } - -} - -EpisodeFileRow.propTypes = { - path: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - quality: PropTypes.object.isRequired, - qualityCutoffNotMet: PropTypes.bool.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - mediaInfo: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onDeleteEpisodeFile: PropTypes.func.isRequired -}; - -export default EpisodeFileRow; diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.tsx b/frontend/src/Episode/Summary/EpisodeFileRow.tsx new file mode 100644 index 000000000..a6b084f78 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeFileRow.tsx @@ -0,0 +1,149 @@ +import React, { useCallback } 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 Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; +import formatBytes from 'Utilities/Number/formatBytes'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import translate from 'Utilities/String/translate'; +import MediaInfo from './MediaInfo'; +import styles from './EpisodeFileRow.css'; + +interface EpisodeFileRowProps { + path: string; + size: number; + languages: Language[]; + quality: QualityModel; + qualityCutoffNotMet: boolean; + customFormats: CustomFormat[]; + customFormatScore: number; + mediaInfo: object; + columns: Column[]; + onDeleteEpisodeFile(): void; +} + +function EpisodeFileRow(props: EpisodeFileRowProps) { + const { + path, + size, + languages, + quality, + customFormats, + customFormatScore, + qualityCutoffNotMet, + mediaInfo, + columns, + onDeleteEpisodeFile, + } = props; + + const [ + isRemoveEpisodeFileModalOpen, + setRemoveEpisodeFileModalOpen, + setRemoveEpisodeFileModalClosed, + ] = useModalOpenState(false); + + const handleRemoveEpisodeFilePress = useCallback(() => { + onDeleteEpisodeFile(); + + setRemoveEpisodeFileModalClosed(); + }, [onDeleteEpisodeFile, setRemoveEpisodeFileModalClosed]); + + return ( + <TableRow> + {columns.map(({ name, isVisible }) => { + if (!isVisible) { + return null; + } + + if (name === 'path') { + return <TableRowCell key={name}>{path}</TableRowCell>; + } + + if (name === 'size') { + return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>; + } + + if (name === 'languages') { + return ( + <TableRowCell key={name} className={styles.languages}> + <EpisodeLanguages languages={languages} /> + </TableRowCell> + ); + } + + if (name === 'quality') { + return ( + <TableRowCell key={name} className={styles.quality}> + <EpisodeQuality + quality={quality} + isCutoffNotMet={qualityCutoffNotMet} + /> + </TableRowCell> + ); + } + + if (name === 'customFormats') { + return ( + <TableRowCell key={name} className={styles.customFormats}> + <EpisodeFormats formats={customFormats} /> + </TableRowCell> + ); + } + + if (name === 'customFormatScore') { + return ( + <TableRowCell key={name} className={styles.customFormatScore}> + {formatCustomFormatScore(customFormatScore, customFormats.length)} + </TableRowCell> + ); + } + + if (name === 'actions') { + return ( + <TableRowCell key={name} className={styles.actions}> + {mediaInfo ? ( + <Popover + anchor={<Icon name={icons.MEDIA_INFO} />} + title={translate('MediaInfo')} + body={<MediaInfo {...mediaInfo} />} + position={tooltipPositions.LEFT} + /> + ) : null} + + <IconButton + title={translate('DeleteEpisodeFromDisk')} + name={icons.REMOVE} + onPress={setRemoveEpisodeFileModalOpen} + /> + </TableRowCell> + ); + } + + return null; + })} + + <ConfirmModal + isOpen={isRemoveEpisodeFileModalOpen} + kind={kinds.DANGER} + title={translate('DeleteEpisodeFile')} + message={translate('DeleteEpisodeFileMessage', { path })} + confirmLabel={translate('Delete')} + onConfirm={handleRemoveEpisodeFilePress} + onCancel={setRemoveEpisodeFileModalClosed} + /> + </TableRow> + ); +} + +export default EpisodeFileRow; diff --git a/frontend/src/Episode/Summary/EpisodeSummary.js b/frontend/src/Episode/Summary/EpisodeSummary.js deleted file mode 100644 index 497008467..000000000 --- a/frontend/src/Episode/Summary/EpisodeSummary.js +++ /dev/null @@ -1,198 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { icons, kinds, sizes } from 'Helpers/Props'; -import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; -import translate from 'Utilities/String/translate'; -import EpisodeAiringConnector from './EpisodeAiringConnector'; -import EpisodeFileRow from './EpisodeFileRow'; -import styles from './EpisodeSummary.css'; - -const columns = [ - { - name: 'path', - label: () => translate('Path'), - isSortable: false, - isVisible: true - }, - { - name: 'size', - label: () => translate('Size'), - isSortable: false, - isVisible: true - }, - { - name: 'languages', - label: () => translate('Languages'), - isSortable: false, - isVisible: true - }, - { - name: 'quality', - label: () => translate('Quality'), - isSortable: false, - isVisible: true - }, - { - name: 'customFormats', - label: () => translate('Formats'), - isSortable: false, - isVisible: true - }, - { - name: 'customFormatScore', - label: React.createElement(Icon, { - name: icons.SCORE, - title: () => translate('CustomFormatScore') - }), - isSortable: true, - isVisible: true - }, - { - name: 'actions', - label: '', - isSortable: false, - isVisible: true - } -]; - -class EpisodeSummary extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isRemoveEpisodeFileModalOpen: false - }; - } - - // - // Listeners - - onRemoveEpisodeFilePress = () => { - this.setState({ isRemoveEpisodeFileModalOpen: true }); - }; - - onConfirmRemoveEpisodeFile = () => { - this.props.onDeleteEpisodeFile(); - this.setState({ isRemoveEpisodeFileModalOpen: false }); - }; - - onRemoveEpisodeFileModalClose = () => { - this.setState({ isRemoveEpisodeFileModalOpen: false }); - }; - - // - // Render - - render() { - const { - qualityProfileId, - network, - overview, - airDateUtc, - mediaInfo, - path, - size, - languages, - quality, - customFormats, - customFormatScore, - qualityCutoffNotMet, - onDeleteEpisodeFile - } = this.props; - - const hasOverview = !!overview; - - return ( - <div> - <div> - <span className={styles.infoTitle}>{translate('Airs')}</span> - - <EpisodeAiringConnector - airDateUtc={airDateUtc} - network={network} - /> - </div> - - <div> - <span className={styles.infoTitle}>{translate('QualityProfile')}</span> - - <Label - kind={kinds.PRIMARY} - size={sizes.MEDIUM} - > - <QualityProfileNameConnector - qualityProfileId={qualityProfileId} - /> - </Label> - </div> - - <div className={styles.overview}> - { - hasOverview ? - overview : - translate('NoEpisodeOverview') - } - </div> - - { - path ? - <Table columns={columns}> - <TableBody> - <EpisodeFileRow - path={path} - size={size} - languages={languages} - quality={quality} - qualityCutoffNotMet={qualityCutoffNotMet} - customFormats={customFormats} - customFormatScore={customFormatScore} - mediaInfo={mediaInfo} - columns={columns} - onDeleteEpisodeFile={onDeleteEpisodeFile} - /> - </TableBody> - </Table> : - null - } - - <ConfirmModal - isOpen={this.state.isRemoveEpisodeFileModalOpen} - kind={kinds.DANGER} - title={translate('DeleteEpisodeFile')} - message={translate('DeleteEpisodeFileMessage', { path })} - confirmLabel={translate('Delete')} - onConfirm={this.onConfirmRemoveEpisodeFile} - onCancel={this.onRemoveEpisodeFileModalClose} - /> - </div> - ); - } -} - -EpisodeSummary.propTypes = { - episodeFileId: PropTypes.number.isRequired, - qualityProfileId: PropTypes.number.isRequired, - network: PropTypes.string.isRequired, - overview: PropTypes.string, - airDateUtc: PropTypes.string.isRequired, - mediaInfo: PropTypes.object, - path: PropTypes.string, - size: PropTypes.number, - languages: PropTypes.arrayOf(PropTypes.object), - quality: PropTypes.object, - qualityCutoffNotMet: PropTypes.bool, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - onDeleteEpisodeFile: PropTypes.func.isRequired -}; - -export default EpisodeSummary; diff --git a/frontend/src/Episode/Summary/EpisodeSummary.tsx b/frontend/src/Episode/Summary/EpisodeSummary.tsx new file mode 100644 index 000000000..75d2993d5 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeSummary.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import Episode from 'Episode/Episode'; +import useEpisode, { EpisodeEntities } from 'Episode/useEpisode'; +import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Series from 'Series/Series'; +import useSeries from 'Series/useSeries'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import { + deleteEpisodeFile, + fetchEpisodeFile, +} from 'Store/Actions/episodeFileActions'; +import translate from 'Utilities/String/translate'; +import EpisodeAiring from './EpisodeAiring'; +import EpisodeFileRow from './EpisodeFileRow'; +import styles from './EpisodeSummary.css'; + +const COLUMNS: Column[] = [ + { + name: 'path', + label: () => translate('Path'), + isSortable: false, + isVisible: true, + }, + { + name: 'size', + label: () => translate('Size'), + isSortable: false, + isVisible: true, + }, + { + name: 'languages', + label: () => translate('Languages'), + isSortable: false, + isVisible: true, + }, + { + name: 'quality', + label: () => translate('Quality'), + isSortable: false, + isVisible: true, + }, + { + name: 'customFormats', + label: () => translate('Formats'), + isSortable: false, + isVisible: true, + }, + { + name: 'customFormatScore', + label: React.createElement(Icon, { + name: icons.SCORE, + title: () => translate('CustomFormatScore'), + }), + isSortable: true, + isVisible: true, + }, + { + name: 'actions', + label: '', + isSortable: false, + isVisible: true, + }, +]; + +interface EpisodeSummaryProps { + seriesId: number; + episodeId: number; + episodeEntity: EpisodeEntities; + episodeFileId?: number; +} + +function EpisodeSummary(props: EpisodeSummaryProps) { + const { seriesId, episodeId, episodeEntity, episodeFileId } = props; + + const dispatch = useDispatch(); + + const { qualityProfileId, network } = useSeries(seriesId) as Series; + + const { airDateUtc, overview } = useEpisode( + episodeId, + episodeEntity + ) as Episode; + + const { + path, + mediaInfo, + size, + languages, + quality, + qualityCutoffNotMet, + customFormats, + customFormatScore, + } = useEpisodeFile(episodeFileId) || {}; + + const handleDeleteEpisodeFile = useCallback(() => { + dispatch( + deleteEpisodeFile({ + id: episodeFileId, + episodeEntity, + }) + ); + }, [episodeFileId, episodeEntity, dispatch]); + + useEffect(() => { + if (episodeFileId && !path) { + dispatch(fetchEpisodeFile({ id: episodeFileId })); + } + }, [episodeFileId, path, dispatch]); + + const hasOverview = !!overview; + + return ( + <div> + <div> + <span className={styles.infoTitle}>{translate('Airs')}</span> + + <EpisodeAiring airDateUtc={airDateUtc} network={network} /> + </div> + + <div> + <span className={styles.infoTitle}>{translate('QualityProfile')}</span> + + <Label kind={kinds.PRIMARY} size={sizes.MEDIUM}> + <QualityProfileNameConnector qualityProfileId={qualityProfileId} /> + </Label> + </div> + + <div className={styles.overview}> + {hasOverview ? overview : translate('NoEpisodeOverview')} + </div> + + {path ? ( + <Table columns={COLUMNS}> + <TableBody> + <EpisodeFileRow + path={path} + size={size!} + languages={languages!} + quality={quality!} + qualityCutoffNotMet={qualityCutoffNotMet!} + customFormats={customFormats!} + customFormatScore={customFormatScore!} + mediaInfo={mediaInfo!} + columns={COLUMNS} + onDeleteEpisodeFile={handleDeleteEpisodeFile} + /> + </TableBody> + </Table> + ) : null} + </div> + ); +} + +export default EpisodeSummary; diff --git a/frontend/src/Episode/Summary/EpisodeSummaryConnector.js b/frontend/src/Episode/Summary/EpisodeSummaryConnector.js deleted file mode 100644 index 15f940f2c..000000000 --- a/frontend/src/Episode/Summary/EpisodeSummaryConnector.js +++ /dev/null @@ -1,109 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteEpisodeFile, fetchEpisodeFile } from 'Store/Actions/episodeFileActions'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import EpisodeSummary from './EpisodeSummary'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - createEpisodeSelector(), - createEpisodeFileSelector(), - (series, episode, episodeFile = {}) => { - const { - qualityProfileId, - network - } = series; - - const { - airDateUtc, - overview - } = episode; - - const { - mediaInfo, - path, - size, - languages, - quality, - qualityCutoffNotMet, - customFormats, - customFormatScore - } = episodeFile; - - return { - network, - qualityProfileId, - airDateUtc, - overview, - mediaInfo, - path, - size, - languages, - quality, - qualityCutoffNotMet, - customFormats, - customFormatScore - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onDeleteEpisodeFile() { - dispatch(deleteEpisodeFile({ - id: props.episodeFileId, - episodeEntity: props.episodeEntity - })); - }, - - dispatchFetchEpisodeFile() { - dispatch(fetchEpisodeFile({ - id: props.episodeFileId - })); - } - }; -} - -class EpisodeSummaryConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - episodeFileId, - path, - dispatchFetchEpisodeFile - } = this.props; - - if (episodeFileId && !path) { - dispatchFetchEpisodeFile({ id: episodeFileId }); - } - } - - // - // Render - - render() { - const { - dispatchFetchEpisodeFile, - ...otherProps - } = this.props; - - return <EpisodeSummary {...otherProps} />; - } -} - -EpisodeSummaryConnector.propTypes = { - episodeFileId: PropTypes.number, - path: PropTypes.string, - dispatchFetchEpisodeFile: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSummaryConnector); diff --git a/frontend/src/Episode/episodeEntities.js b/frontend/src/Episode/episodeEntities.ts similarity index 91% rename from frontend/src/Episode/episodeEntities.js rename to frontend/src/Episode/episodeEntities.ts index fe21d4ed0..b5759563f 100644 --- a/frontend/src/Episode/episodeEntities.js +++ b/frontend/src/Episode/episodeEntities.ts @@ -9,5 +9,5 @@ export default { EPISODES, INTERACTIVE_IMPORT, WANTED_CUTOFF_UNMET, - WANTED_MISSING -}; + WANTED_MISSING, +} as const; diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts index 7f80ae1b3..01062b2a6 100644 --- a/frontend/src/Episode/useEpisode.ts +++ b/frontend/src/Episode/useEpisode.ts @@ -5,15 +5,15 @@ import AppState from 'App/State/AppState'; export type EpisodeEntities = | 'calendar' | 'episodes' - | 'interactiveImport' - | 'cutoffUnmet' - | 'missing'; + | 'interactiveImport.episodes' + | 'wanted.cutoffUnmet' + | 'wanted.missing'; function createEpisodeSelector(episodeId?: number) { return createSelector( (state: AppState) => state.episodes.items, (episodes) => { - return episodes.find((e) => e.id === episodeId); + return episodes.find(({ id }) => id === episodeId); } ); } @@ -22,7 +22,25 @@ function createCalendarEpisodeSelector(episodeId?: number) { return createSelector( (state: AppState) => state.calendar.items, (episodes) => { - return episodes.find((e) => e.id === episodeId); + return episodes.find(({ id }) => id === episodeId); + } + ); +} + +function createWantedCutoffUnmetEpisodeSelector(episodeId?: number) { + return createSelector( + (state: AppState) => state.wanted.cutoffUnmet.items, + (episodes) => { + return episodes.find(({ id }) => id === episodeId); + } + ); +} + +function createWantedMissingEpisodeSelector(episodeId?: number) { + return createSelector( + (state: AppState) => state.wanted.missing.items, + (episodes) => { + return episodes.find(({ id }) => id === episodeId); } ); } @@ -37,6 +55,12 @@ function useEpisode( case 'calendar': selector = createCalendarEpisodeSelector; break; + case 'wanted.cutoffUnmet': + selector = createWantedCutoffUnmetEpisodeSelector; + break; + case 'wanted.missing': + selector = createWantedMissingEpisodeSelector; + break; default: break; } diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts index da362db82..f5dc11d3c 100644 --- a/frontend/src/EpisodeFile/EpisodeFile.ts +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -17,6 +17,7 @@ export interface EpisodeFile extends ModelBase { languages: Language[]; quality: QualityModel; customFormats: CustomFormat[]; + customFormatScore: number; indexerFlags: number; releaseType: ReleaseType; mediaInfo: MediaInfo; diff --git a/frontend/src/EpisodeFile/useEpisodeFile.ts b/frontend/src/EpisodeFile/useEpisodeFile.ts new file mode 100644 index 000000000..76fc8cc4d --- /dev/null +++ b/frontend/src/EpisodeFile/useEpisodeFile.ts @@ -0,0 +1,18 @@ +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createEpisodeFileSelector(episodeFileId?: number) { + return createSelector( + (state: AppState) => state.episodeFiles.items, + (episodeFiles) => { + return episodeFiles.find(({ id }) => id === episodeFileId); + } + ); +} + +function useEpisodeFile(episodeFileId: number | undefined) { + return useSelector(createEpisodeFileSelector(episodeFileId)); +} + +export default useEpisodeFile; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index d4f234fa3..1ea5be1f7 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -32,6 +32,7 @@ import { reprocessInteractiveImportItems, updateInteractiveImportItem, } from 'Store/Actions/interactiveImportActions'; +import CustomFormat from 'typings/CustomFormat'; import { SelectStateInputProps } from 'typings/props'; import Rejection from 'typings/Rejection'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -66,7 +67,7 @@ interface InteractiveImportRowProps { languages?: Language[]; size: number; releaseType: ReleaseType; - customFormats?: object[]; + customFormats?: CustomFormat[]; customFormatScore?: number; indexerFlags: number; rejections: Rejection[]; @@ -92,7 +93,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { releaseGroup, size, releaseType, - customFormats, + customFormats = [], customFormatScore, indexerFlags, rejections, diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index d9e0b1b04..ba3b9d3df 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -4,6 +4,7 @@ import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import Series from 'Series/Series'; +import CustomFormat from 'typings/CustomFormat'; import Rejection from 'typings/Rejection'; export interface InteractiveImportCommandOptions { @@ -33,7 +34,7 @@ interface InteractiveImport extends ModelBase { seasonNumber: number; episodes: Episode[]; qualityWeight: number; - customFormats: object[]; + customFormats: CustomFormat[]; indexerFlags: number; releaseType: ReleaseType; rejections: Rejection[]; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js index c9f90472b..10cad7224 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js @@ -86,9 +86,10 @@ class InteractiveSearchConnector extends Component { } InteractiveSearchConnector.propTypes = { + type: PropTypes.string.isRequired, searchPayload: PropTypes.object.isRequired, - isPopulated: PropTypes.bool.isRequired, - dispatchFetchReleases: PropTypes.func.isRequired + isPopulated: PropTypes.bool, + dispatchFetchReleases: PropTypes.func }; export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector); diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index 5f24a18e3..85243b6bb 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -9,8 +9,8 @@ import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeNumber from 'Episode/EpisodeNumber'; -import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; -import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; +import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import IndexerFlags from 'Episode/IndexerFlags'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; @@ -147,6 +147,7 @@ class EpisodeRow extends Component { episodeId={id} seriesId={seriesId} episodeTitle={title} + episodeEntity="episodes" finaleType={finaleType} showOpenSeriesButton={false} /> @@ -351,7 +352,7 @@ class EpisodeRow extends Component { key={name} className={styles.status} > - <EpisodeStatusConnector + <EpisodeStatus episodeId={id} episodeFileId={episodeFileId} /> @@ -361,9 +362,10 @@ class EpisodeRow extends Component { if (name === 'actions') { return ( - <EpisodeSearchCellConnector + <EpisodeSearchCell key={name} episodeId={id} + episodeEntity='episodes' seriesId={seriesId} episodeTitle={title} /> diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.js b/frontend/src/Series/Details/SeriesDetailsSeason.js index 9ec7edcc8..3baf06b00 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.js +++ b/frontend/src/Series/Details/SeriesDetailsSeason.js @@ -18,7 +18,7 @@ import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import SeriesHistoryModal from 'Series/History/SeriesHistoryModal'; -import SeasonInteractiveSearchModalConnector from 'Series/Search/SeasonInteractiveSearchModalConnector'; +import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal'; import isAfter from 'Utilities/Date/isAfter'; import isBefore from 'Utilities/Date/isBefore'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -505,7 +505,7 @@ class SeriesDetailsSeason extends Component { onModalClose={this.onHistoryModalClose} /> - <SeasonInteractiveSearchModalConnector + <SeasonInteractiveSearchModal isOpen={isInteractiveSearchModalOpen} seriesId={seriesId} seasonNumber={seasonNumber} diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js b/frontend/src/Series/Search/SeasonInteractiveSearchModal.js deleted file mode 100644 index 861c9113c..000000000 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent'; - -function SeasonInteractiveSearchModal(props) { - const { - isOpen, - seriesId, - seasonNumber, - onModalClose - } = props; - - return ( - <Modal - isOpen={isOpen} - size={sizes.EXTRA_EXTRA_LARGE} - closeOnBackgroundClick={false} - onModalClose={onModalClose} - > - <SeasonInteractiveSearchModalContent - seriesId={seriesId} - seasonNumber={seasonNumber} - onModalClose={onModalClose} - /> - </Modal> - ); -} - -SeasonInteractiveSearchModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SeasonInteractiveSearchModal; diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx b/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx new file mode 100644 index 000000000..babe59469 --- /dev/null +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { + cancelFetchReleases, + clearReleases, +} from 'Store/Actions/releaseActions'; +import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent'; + +interface SeasonInteractiveSearchModalProps { + isOpen: boolean; + seriesId: number; + seasonNumber: number; + onModalClose(): void; +} + +function SeasonInteractiveSearchModal( + props: SeasonInteractiveSearchModalProps +) { + const { isOpen, seriesId, seasonNumber, onModalClose } = props; + + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(cancelFetchReleases()); + dispatch(clearReleases()); + + onModalClose(); + }, [dispatch, onModalClose]); + + useEffect(() => { + return () => { + dispatch(cancelFetchReleases()); + dispatch(clearReleases()); + }; + }, [dispatch]); + + return ( + <Modal + isOpen={isOpen} + size={sizes.EXTRA_EXTRA_LARGE} + closeOnBackgroundClick={false} + onModalClose={handleModalClose} + > + <SeasonInteractiveSearchModalContent + seriesId={seriesId} + seasonNumber={seasonNumber} + onModalClose={handleModalClose} + /> + </Modal> + ); +} + +export default SeasonInteractiveSearchModal; diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModalConnector.js b/frontend/src/Series/Search/SeasonInteractiveSearchModalConnector.js deleted file mode 100644 index c721d869b..000000000 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModalConnector.js +++ /dev/null @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; -import SeasonInteractiveSearchModal from './SeasonInteractiveSearchModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchCancelFetchReleases() { - dispatch(cancelFetchReleases()); - }, - - dispatchClearReleases() { - dispatch(clearReleases()); - }, - - onModalClose() { - dispatch(cancelFetchReleases()); - dispatch(clearReleases()); - props.onModalClose(); - } - }; -} - -class SeasonInteractiveSearchModalConnector extends Component { - - // - // Lifecycle - - componentWillUnmount() { - this.props.dispatchCancelFetchReleases(); - this.props.dispatchClearReleases(); - } - - // - // Render - - render() { - const { - dispatchCancelFetchReleases, - dispatchClearReleases, - ...otherProps - } = this.props; - - return ( - <SeasonInteractiveSearchModal - {...otherProps} - /> - ); - } -} - -SeasonInteractiveSearchModalConnector.propTypes = { - ...SeasonInteractiveSearchModal.propTypes, - dispatchCancelFetchReleases: PropTypes.func.isRequired, - dispatchClearReleases: PropTypes.func.isRequired -}; - -export default connect(null, createMapDispatchToProps)(SeasonInteractiveSearchModalConnector); diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.js b/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx similarity index 59% rename from frontend/src/Series/Search/SeasonInteractiveSearchModalContent.js rename to frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx index c76dec22f..362972c89 100644 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.js +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Button from 'Components/Link/Button'; import ModalBody from 'Components/Modal/ModalBody'; @@ -10,20 +9,25 @@ import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConne import formatSeason from 'Season/formatSeason'; import translate from 'Utilities/String/translate'; -function SeasonInteractiveSearchModalContent(props) { - const { - seriesId, - seasonNumber, - onModalClose - } = props; +interface SeasonInteractiveSearchModalContentProps { + seriesId: number; + seasonNumber: number; + onModalClose(): void; +} + +function SeasonInteractiveSearchModalContent( + props: SeasonInteractiveSearchModalContentProps +) { + const { seriesId, seasonNumber, onModalClose } = props; return ( <ModalContent onModalClose={onModalClose}> <ModalHeader> - {seasonNumber === null ? - translate('InteractiveSearchModalHeader') : - translate('InteractiveSearchModalHeaderSeason', { season: formatSeason(seasonNumber) }) - } + {seasonNumber === null + ? translate('InteractiveSearchModalHeader') + : translate('InteractiveSearchModalHeaderSeason', { + season: formatSeason(seasonNumber) as string, + })} </ModalHeader> <ModalBody scrollDirection={scrollDirections.BOTH}> @@ -31,24 +35,16 @@ function SeasonInteractiveSearchModalContent(props) { type="season" searchPayload={{ seriesId, - seasonNumber + seasonNumber, }} /> </ModalBody> <ModalFooter> - <Button onPress={onModalClose}> - {translate('Close')} - </Button> + <Button onPress={onModalClose}>{translate('Close')}</Button> </ModalFooter> </ModalContent> ); } -SeasonInteractiveSearchModalContent.propTypes = { - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - export default SeasonInteractiveSearchModalContent; diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index be2215f7e..9f9148b27 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -53,6 +53,7 @@ export interface AlternateTitle { sceneSeasonNumber?: number; title: string; sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb'; + comment?: string; } export interface SeriesAddOptions { diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.ts b/frontend/src/Store/Selectors/createQueueItemSelector.ts index 7d033559a..92f8a2a73 100644 --- a/frontend/src/Store/Selectors/createQueueItemSelector.ts +++ b/frontend/src/Store/Selectors/createQueueItemSelector.ts @@ -1,6 +1,19 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; +export function createQueueItemSelectorForHook(episodeId: number) { + return createSelector( + (state: AppState) => state.queue.details.items, + (details) => { + if (!episodeId || !details) { + return null; + } + + return details.find((item) => item.episodeId === episodeId); + } + ); +} + function createQueueItemSelector() { return createSelector( (_: AppState, { episodeId }: { episodeId: number }) => episodeId, diff --git a/frontend/src/Store/Selectors/createSeriesSelector.js b/frontend/src/Store/Selectors/createSeriesSelector.js index 5cdf8becd..6e95b324d 100644 --- a/frontend/src/Store/Selectors/createSeriesSelector.js +++ b/frontend/src/Store/Selectors/createSeriesSelector.js @@ -5,8 +5,7 @@ export function createSeriesSelectorForHook(seriesId) { (state) => state.series.itemMap, (state) => state.series.items, (itemMap, allSeries) => { - - return seriesId ? allSeries[itemMap[seriesId]]: undefined; + return seriesId ? allSeries[itemMap[seriesId]] : undefined; } ); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 76fe0e0dd..a51ead746 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -5,8 +5,8 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; import episodeEntities from 'Episode/episodeEntities'; -import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; -import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; +import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; @@ -125,7 +125,7 @@ function CutoffUnmetRow(props) { key={name} className={styles.status} > - <EpisodeStatusConnector + <EpisodeStatus episodeId={id} episodeFileId={episodeFileId} episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET} @@ -136,7 +136,7 @@ function CutoffUnmetRow(props) { if (name === 'actions') { return ( - <EpisodeSearchCellConnector + <EpisodeSearchCell key={name} episodeId={id} seriesId={series.id} diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index 7064d9a9a..0831a2bc3 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -5,8 +5,8 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; import episodeEntities from 'Episode/episodeEntities'; -import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; -import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; +import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; import SeriesTitleLink from 'Series/SeriesTitleLink'; @@ -115,7 +115,7 @@ function MissingRow(props) { key={name} className={styles.status} > - <EpisodeStatusConnector + <EpisodeStatus episodeId={id} episodeFileId={episodeFileId} episodeEntity={episodeEntities.WANTED_MISSING} @@ -126,7 +126,7 @@ function MissingRow(props) { if (name === 'actions') { return ( - <EpisodeSearchCellConnector + <EpisodeSearchCell key={name} episodeId={id} seriesId={series.id} diff --git a/package.json b/package.json index cc098051c..4d62f7d2a 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-slider": "1.1.4", - "react-tabs": "3.2.2", + "react-tabs": "4.3.0", "react-text-truncate": "0.18.0", "react-use-measure": "2.1.1", "react-virtualized": "9.21.1", diff --git a/yarn.lock b/yarn.lock index 189a159cf..2fd9a7e55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5817,10 +5817,10 @@ react-slider@1.1.4: resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-1.1.4.tgz#08b55f9be3e04cc10ae00cc3aedb6891dffe9bf3" integrity sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ== -react-tabs@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0" - integrity sha512-/o52eGKxFHRa+ssuTEgSM8qORnV4+k7ibW+aNQzKe+5gifeVz8nLxCrsI9xdRhfb0wCLdgIambIpb1qCxaMN+A== +react-tabs@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-4.3.0.tgz#9f4db0fd209ba4ab2c1e78993ff964435f84af62" + integrity sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q== dependencies: clsx "^1.1.0" prop-types "^15.5.0" From 882b54be613279bc7366ace6deb98331700de660 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 26 Aug 2024 00:30:31 +0000 Subject: [PATCH 497/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 106 ++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index bc562f2fe..f357c07b9 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -933,6 +933,26 @@ } }, "/api/v3/customformat": { + "get": { + "tags": [ + "CustomFormat" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + } + }, "post": { "tags": [ "CustomFormat" @@ -968,26 +988,6 @@ } } } - }, - "get": { - "tags": [ - "CustomFormat" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomFormatResource" - } - } - } - } - } - } } }, "/api/v3/customformat/{id}": { @@ -1087,6 +1087,53 @@ } } }, + "/api/v3/customformat/bulk": { + "put": { + "tags": [ + "CustomFormat" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "CustomFormat" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v3/customformat/schema": { "get": { "tags": [ @@ -7986,6 +8033,25 @@ }, "additionalProperties": false }, + "CustomFormatBulkResource": { + "type": "object", + "properties": { + "ids": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "includeCustomFormatWhenRenaming": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, "CustomFormatResource": { "type": "object", "properties": { From cfa2f4d4c6e35d7b9ddd2e1da2e59f7287859516 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 26 Aug 2024 21:40:43 -0700 Subject: [PATCH 498/762] Fixed: Queue header --- frontend/src/Components/Link/Link.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx index d6d731106..80ee66e82 100644 --- a/frontend/src/Components/Link/Link.tsx +++ b/frontend/src/Components/Link/Link.tsx @@ -78,7 +78,11 @@ export default function Link<C extends ElementType = 'button'>({ return ( <Component - type={type || 'button'} + type={ + component === 'button' || component === 'input' + ? type || 'button' + : type + } target={target} className={linkClass} disabled={isDisabled} From f033799d7a257b0554c877b4ae6dcc129ccd7fe1 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:41:10 +0200 Subject: [PATCH 499/762] Convert IconButton to Typescript --- frontend/src/Components/Link/IconButton.js | 59 --------------------- frontend/src/Components/Link/IconButton.tsx | 41 ++++++++++++++ 2 files changed, 41 insertions(+), 59 deletions(-) delete mode 100644 frontend/src/Components/Link/IconButton.js create mode 100644 frontend/src/Components/Link/IconButton.tsx diff --git a/frontend/src/Components/Link/IconButton.js b/frontend/src/Components/Link/IconButton.js deleted file mode 100644 index fffbe13e0..000000000 --- a/frontend/src/Components/Link/IconButton.js +++ /dev/null @@ -1,59 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import translate from 'Utilities/String/translate'; -import Link from './Link'; -import styles from './IconButton.css'; - -function IconButton(props) { - const { - className, - iconClassName, - name, - kind, - size, - isSpinning, - isDisabled, - ...otherProps - } = props; - - return ( - <Link - className={classNames( - className, - isDisabled && styles.isDisabled - )} - aria-label={translate('TableOptionsButton')} - isDisabled={isDisabled} - {...otherProps} - > - <Icon - className={iconClassName} - name={name} - kind={kind} - size={size} - isSpinning={isSpinning} - /> - </Link> - ); -} - -IconButton.propTypes = { - ...Link.propTypes, - className: PropTypes.string.isRequired, - iconClassName: PropTypes.string, - kind: PropTypes.string, - name: PropTypes.object.isRequired, - size: PropTypes.number, - title: PropTypes.string, - isSpinning: PropTypes.bool, - isDisabled: PropTypes.bool -}; - -IconButton.defaultProps = { - className: styles.button, - size: 12 -}; - -export default IconButton; diff --git a/frontend/src/Components/Link/IconButton.tsx b/frontend/src/Components/Link/IconButton.tsx new file mode 100644 index 000000000..b6951c00c --- /dev/null +++ b/frontend/src/Components/Link/IconButton.tsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import React from 'react'; +import Icon, { IconProps } from 'Components/Icon'; +import translate from 'Utilities/String/translate'; +import Link, { LinkProps } from './Link'; +import styles from './IconButton.css'; + +export interface IconButtonProps + extends Omit<LinkProps, 'name' | 'kind'>, + Pick<IconProps, 'name' | 'kind' | 'size' | 'isSpinning'> { + iconClassName?: IconProps['className']; +} + +export default function IconButton({ + className = styles.button, + iconClassName, + name, + kind, + size = 12, + isSpinning, + ...otherProps +}: IconButtonProps) { + return ( + <Link + className={classNames( + className, + otherProps.isDisabled && styles.isDisabled + )} + aria-label={translate('TableOptionsButton')} + {...otherProps} + > + <Icon + className={iconClassName} + name={name} + kind={kind} + size={size} + isSpinning={isSpinning} + /> + </Link> + ); +} From 7ea1301221793ded0f64258d48294d20451422f2 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:41:30 +0200 Subject: [PATCH 500/762] Convert TableRowCell to Typescript --- .../Components/Table/Cells/TableRowCell.js | 37 ------------------- .../Components/Table/Cells/TableRowCell.tsx | 11 ++++++ 2 files changed, 11 insertions(+), 37 deletions(-) delete mode 100644 frontend/src/Components/Table/Cells/TableRowCell.js create mode 100644 frontend/src/Components/Table/Cells/TableRowCell.tsx diff --git a/frontend/src/Components/Table/Cells/TableRowCell.js b/frontend/src/Components/Table/Cells/TableRowCell.js deleted file mode 100644 index f66bbf3aa..000000000 --- a/frontend/src/Components/Table/Cells/TableRowCell.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './TableRowCell.css'; - -class TableRowCell extends Component { - - // - // Render - - render() { - const { - className, - children, - ...otherProps - } = this.props; - - return ( - <td - className={className} - {...otherProps} - > - {children} - </td> - ); - } -} - -TableRowCell.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) -}; - -TableRowCell.defaultProps = { - className: styles.cell -}; - -export default TableRowCell; diff --git a/frontend/src/Components/Table/Cells/TableRowCell.tsx b/frontend/src/Components/Table/Cells/TableRowCell.tsx new file mode 100644 index 000000000..3b4b97c14 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.tsx @@ -0,0 +1,11 @@ +import React, { ComponentPropsWithoutRef } from 'react'; +import styles from './TableRowCell.css'; + +export interface TableRowCellprops extends ComponentPropsWithoutRef<'td'> {} + +export default function TableRowCell({ + className = styles.cell, + ...tdProps +}: TableRowCellprops) { + return <td className={className} {...tdProps} />; +} From 25d9f09a43ded9dd07c6777390fc541ca5f89eeb Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:41:58 +0200 Subject: [PATCH 501/762] Convert SpinnerIcon to TypeScript --- frontend/src/Components/SpinnerIcon.js | 34 ------------------- frontend/src/Components/SpinnerIcon.tsx | 21 ++++++++++++ .../src/Helpers/Props/{icons.js => icons.ts} | 4 +-- 3 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 frontend/src/Components/SpinnerIcon.js create mode 100644 frontend/src/Components/SpinnerIcon.tsx rename frontend/src/Helpers/Props/{icons.js => icons.ts} (99%) diff --git a/frontend/src/Components/SpinnerIcon.js b/frontend/src/Components/SpinnerIcon.js deleted file mode 100644 index 5ae03ee66..000000000 --- a/frontend/src/Components/SpinnerIcon.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { icons } from 'Helpers/Props'; -import Icon from './Icon'; - -function SpinnerIcon(props) { - const { - name, - spinningName, - isSpinning, - ...otherProps - } = props; - - return ( - <Icon - name={isSpinning ? (spinningName || name) : name} - isSpinning={isSpinning} - {...otherProps} - /> - ); -} - -SpinnerIcon.propTypes = { - className: PropTypes.string, - name: PropTypes.object.isRequired, - spinningName: PropTypes.object.isRequired, - isSpinning: PropTypes.bool.isRequired -}; - -SpinnerIcon.defaultProps = { - spinningName: icons.SPINNER -}; - -export default SpinnerIcon; diff --git a/frontend/src/Components/SpinnerIcon.tsx b/frontend/src/Components/SpinnerIcon.tsx new file mode 100644 index 000000000..27ddadc41 --- /dev/null +++ b/frontend/src/Components/SpinnerIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon, { IconProps } from './Icon'; + +export interface SpinnerIconProps extends IconProps { + spinningName?: IconProps['name']; + isSpinning: Required<IconProps['isSpinning']>; +} + +export default function SpinnerIcon({ + name, + spinningName = icons.SPINNER, + ...otherProps +}: SpinnerIconProps) { + return ( + <Icon + name={(otherProps.isSpinning && spinningName) || name} + {...otherProps} + /> + ); +} diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.ts similarity index 99% rename from frontend/src/Helpers/Props/icons.js rename to frontend/src/Helpers/Props/icons.ts index d297257db..3ba5c4db1 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.ts @@ -16,7 +16,7 @@ import { faKeyboard as farKeyboard, faObjectGroup as farObjectGroup, faObjectUngroup as farObjectUngroup, - faSquare as farSquare + faSquare as farSquare, } from '@fortawesome/free-regular-svg-icons'; // // Solid @@ -107,7 +107,7 @@ import { faUser as fasUser, faUserPlus as fasUserPlus, faVial as fasVial, - faWrench as fasWrench + faWrench as fasWrench, } from '@fortawesome/free-solid-svg-icons'; // From 98c4cbdd13dc49ad30e91343897b8bd006002489 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:27:34 +0300 Subject: [PATCH 502/762] Don't persist value for SslCertHash when checking for existence --- src/NzbDrone.Core/Configuration/ConfigFileProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index bed0399f9..bb61499db 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -381,7 +381,7 @@ namespace NzbDrone.Core.Configuration } // If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL - if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace())) + if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace())) { SetValue("EnableSsl", false); } From 66e4b7c819e8b45dc72ebf9425dfef8cd8805e90 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 30 Aug 2024 08:25:20 +0000 Subject: [PATCH 503/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Dream <seth.gecko.rr@gmail.com> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: 极染 <poledye@icloud.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 8 ++++++- src/NzbDrone.Core/Localization/Core/it.json | 10 ++++---- .../Localization/Core/pt_BR.json | 8 ++++++- src/NzbDrone.Core/Localization/Core/ru.json | 23 ++++++++++--------- .../Localization/Core/zh_CN.json | 4 ++-- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 0e836bf58..bb78aa4e6 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2104,5 +2104,11 @@ "DeleteSelected": "Borrar seleccionados", "DeleteSelectedImportListExclusionsMessageText": "¿Estás seguro que quieres borrar las exclusiones de listas de importación seleccionadas?", "LogSizeLimit": "Límite de tamaño de registro", - "LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB." + "LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB.", + "NoCustomFormatsFound": "Ningún formato personalizado encontrado", + "DeleteSelectedCustomFormats": "Borrar formato(s) personalizado(s)", + "DeleteSelectedCustomFormatsMessageText": "¿Estás seguro que quieres borrar los {count} formato(s) personalizado(s) seleccionado(s)?", + "EditSelectedCustomFormats": "Editar formatos personalizados seleccionados", + "ManageCustomFormats": "Gestionar formatos personalizados", + "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) seleccionado(s)" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index bc31e8976..9b1183d4b 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -307,7 +307,7 @@ "TagDetails": "Dettagli Etichetta - {label}", "BranchUpdate": "Branca da usare per aggiornare {appName}", "DefaultNotFoundMessage": "Ti devi essere perso, non c'è nulla da vedere qui.", - "DeleteIndexerMessageText": "Sicuro di voler eliminare l'indicizzatore '{name}'?", + "DeleteIndexerMessageText": "Sei sicuro di voler eliminare l'indicizzatore '{name}'?", "Socks5": "Socks5 (Supporto TOR)", "DeleteEpisodeFileMessage": "Sei sicuro di volere eliminare '{path}'?", "NotificationsKodiSettingsCleanLibraryHelpText": "Pulisci libreria dopo l'aggiornamento", @@ -331,7 +331,7 @@ "RemoveFromDownloadClient": "Rimuovi dal client di download", "RemoveQueueItemConfirmation": "Sei sicuro di voler rimuovere '{sourceTitle}' dalla coda?", "NoIndexersFound": "Nessun indicizzatore trovato", - "DeleteImportListMessageText": "Sei sicuro di volere eliminare la lista '{name}'?", + "DeleteImportListMessageText": "Sei sicuro di voler eliminare la lista '{name}'?", "DeleteDelayProfile": "Elimina Profilo di Ritardo", "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?", "MinutesSixty": "60 Minuti: {sixty}", @@ -355,7 +355,7 @@ "CustomFormatsSpecificationMaximumSize": "Dimensione Massima", "CustomFormatsSpecificationMinimumSize": "Dimensione Minima", "DelayProfile": "Profilo di Ritardo", - "DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{name}'?", + "DeleteBackupMessageText": "Sei sicuro di voler eliminare il backup '{name}'?", "DeleteDelayProfileMessageText": "Sei sicuro di volere eliminare questo profilo di ritardo?", "NotificationsTelegramSettingsSendSilentlyHelpText": "Invia il messaggio silenziosamente. L'utente riceverà una notifica senza suono", "NotificationsPushoverSettingsRetry": "Riprova", @@ -927,5 +927,7 @@ "EditRestriction": "Modifica Restrizione", "EnableSsl": "Abilita SSL", "EpisodeFileDeleted": "File dell'Episodio Eliminato", - "Importing": "Importando" + "Importing": "Importando", + "DownloadClientDelugeSettingsDirectory": "Cartella Download", + "CountCustomFormatsSelected": "{count} formati personalizzati selezionati" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 065b313f0..46e80207a 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2104,5 +2104,11 @@ "DeleteSelected": "Excluir Selecionado", "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja excluir as listas de importação selecionadas das exclusões?", "LogSizeLimit": "Limite de Tamanho do Registro", - "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB." + "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.", + "DeleteSelectedCustomFormats": "Excluir formato(s) personalizado(s)", + "DeleteSelectedCustomFormatsMessageText": "Tem certeza que deseja excluir o(s) {count} formato(s) personalizado(s) selecionado(s)?", + "EditSelectedCustomFormats": "Editar formatos personalizados selecionados", + "ManageCustomFormats": "Gerenciar formatos personalizados", + "NoCustomFormatsFound": "Nenhum formato personalizado encontrado", + "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index b69123122..e714b0f2b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -295,7 +295,7 @@ "ChooseAnotherFolder": "Выбрать другой каталог", "DownloadClients": "Клиенты для скачивания", "DownloadPropersAndRepacks": "Проперы и репаки", - "Edit": "Редактирование", + "Edit": "Изменить", "Duration": "Длительность", "EnableColorImpairedMode": "Включить режим для слабовидящих", "EnableProfileHelpText": "Установите флажок, чтобы включить профиль релиза", @@ -897,11 +897,11 @@ "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления. Вы так же можете установить в Система: Обновления", "Upcoming": "Предстоящие", "UnselectAll": "Снять все выделения", - "UnmonitoredOnly": "Только отслеживаемые", + "UnmonitoredOnly": "Только не отслеживаемые", "UpdateSelected": "Обновление выбрано", "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который обрабатывает остатки после процесса обновления", "UpdateMonitoring": "Мониторинг обновлений", - "UpdateAvailableHealthCheckMessage": "Доступно новое обновление", + "UpdateAvailableHealthCheckMessage": "Доступно новое обновление: {version}", "UsenetBlackhole": "Usenet Черная дыра", "YesterdayAt": "Вчера в {time}", "WithFiles": "С файлами", @@ -1061,7 +1061,7 @@ "ProgressBarProgress": "Индикатор выполнения: {progress}%", "QualityProfile": "Профиль качества", "RecyclingBinCleanupHelpText": "Установите значение 0, чтобы отключить автоматическую очистку", - "RefreshAndScan": "Обновить и сканировать", + "RefreshAndScan": "Обновить", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Локальный загрузочный клиент {downloadClientName} сообщил о файлах в {path}, но это недопустимый путь {osName}. Проверьте настройки загрузочного клиента.", "RemotePathMappingRemotePathHelpText": "Корневой путь к каталогу, к которому имеет доступ загрузочный клиент", "RemoveRootFolder": "Удалить корневой каталог", @@ -1224,7 +1224,7 @@ "MetadataSettingsSeriesMetadata": "Метаданные сериала", "MediaManagementSettings": "Настройки управления медиа", "MinutesFortyFive": "45 минут: {fortyFive}", - "MonitorAllSeasonsDescription": "Автоматически отслеживайте все новые сезоны", + "MonitorAllSeasonsDescription": "Автоматически отслеживать новые сезоны", "Mixed": "Смешанный", "NotificationsCustomScriptSettingsArguments": "Аргументы", "NotificationsEmailSettingsServer": "Сервер", @@ -1485,7 +1485,7 @@ "PostImportCategory": "Категория после импорта", "PreferUsenet": "Предпочитать Usenet", "PreferredProtocol": "Предпочтительный протокол", - "PreviewRename": "Предварительный просмотр переименования", + "PreviewRename": "Предпросмотр\nпереименования", "PreviousAiringDate": "Предыдущий выход в эфир: {date}", "Proper": "Пропер (Proper)", "Protocol": "Протокол", @@ -1561,7 +1561,7 @@ "RssSyncIntervalHelpTextWarning": "Это будет применяться ко всем индексаторам, пожалуйста, следуйте установленным ими правилам", "Save": "Сохранить", "SeasonFinale": "Финал сезона", - "SearchMonitored": "Искать отслеживаемое", + "SearchMonitored": "Искать сериал", "SearchForMonitoredEpisodes": "Поиск отслеживаемых эпизодов", "SearchForCutoffUnmetEpisodes": "Искать все эпизоды не достигшие указанного качества", "SearchFailedError": "Не удалось выполнить поиск, повторите попытку позже.", @@ -1750,7 +1750,7 @@ "NotificationsAppriseSettingsTags": "Информирующие теги", "NotificationsEmailSettingsCcAddress": "Адрес(а) CC", "NotificationsEmailSettingsRecipientAddressHelpText": "Список получателей электронной почты, разделенный запятыми", - "NotificationsEmbySettingsSendNotificationsHelpText": "Заставить MediaBrowser отправлять уведомления настроенным поставщикам", + "NotificationsEmbySettingsSendNotificationsHelpText": "Позврлить Emby отправлять уведомления настроенным поставщикам. Не поддерживается в Jellyfin.", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Загрузочный клиент {downloadClientName} сообщает о наличии файлах по пути {path}, но {appName} не видит этот каталог. Возможно, вам придется настроить права доступа к папке.", "RemotePathMappings": "Сопоставления удаленного пути", "SearchIsNotSupportedWithThisIndexer": "Поиск не поддерживается этим индексатором", @@ -1803,7 +1803,7 @@ "MediaManagementSettingsLoadError": "Не удалось загрузить настройки управления медиа", "NotificationsDiscordSettingsOnImportFields": "Поля импорта", "NotificationsDiscordSettingsUsernameHelpText": "Имя пользователя для публикации по умолчанию — Discord webhook по умолчанию", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Обновить библиотеку при импорте, переименовании или удалении?", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Обновить библиотеку при импорте, переименовании или удалении", "Posters": "Постеры", "PendingChangesMessage": "У вас есть несохраненные изменения. Вы уверены, что хотите покинуть эту страницу?", "Path": "Путь", @@ -1846,7 +1846,7 @@ "NotificationsDiscordSettingsAuthor": "Автор", "PackageVersionInfo": "{packageVersion} от {packageAuthor}", "Peers": "Пиры", - "PreviewRenameSeason": "Предварительный просмотр переименования этого сезона", + "PreviewRenameSeason": "Предпросмотр переименования этого сезона", "Proxy": "Прокси", "RenameEpisodesHelpText": "{appName} будет использовать существующее имя файла, если переименование отключено", "QualityProfiles": "Профили качества", @@ -2087,5 +2087,6 @@ "OverrideGrabNoSeries": "Необходимо выбрать сериал", "NotificationsPushBulletSettingSenderIdHelpText": "Идентификатор устройства для отправки уведомлений. Используйте device_iden в URL-адресе устройства на pushbullet.com (оставьте пустым, чтобы отправлять уведомления от себя)", "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages поддерживает суффикс `:EN+DE`, позволяющий фильтровать языки, включенные в имя файла. Используйте `-DE`, чтобы исключить определенные языки. Добавление `+` (например, `:EN+`) приведет к выводу `[EN]`/`[EN+--]`/`[--]` в зависимости от исключенных языков. Например `{MediaInfo Full:EN+DE}`.", - "MaximumSingleEpisodeAgeHelpText": "Во время поиска по всему сезону будут разрешены только сезонные пакеты, если последний эпизод сезона старше этой настройки. Только стандартные сериалы. Используйте 0 для отключения." + "MaximumSingleEpisodeAgeHelpText": "Во время поиска по всему сезону будут разрешены только сезонные пакеты, если последний эпизод сезона старше этой настройки. Только стандартные сериалы. Используйте 0 для отключения.", + "SeasonsMonitoredAll": "Все" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 8d54b972a..4c9aa4df7 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1723,8 +1723,8 @@ "NotificationsEmailSettingsServer": "服务器", "NotificationsEmailSettingsServerHelpText": "邮件服务器的主机名或 IP", "NotificationsEmbySettingsSendNotifications": "发送通知", - "NotificationsEmbySettingsSendNotificationsHelpText": "由 MediaBrowser 向配置的提供方发送通知", - "NotificationsEmbySettingsUpdateLibraryHelpText": "在导入、重命名、删除时更新库?", + "NotificationsEmbySettingsSendNotificationsHelpText": "让 Emby 向配置的提供者发送通知。不支持 Jellyfin。", + "NotificationsEmbySettingsUpdateLibraryHelpText": "在导入、重命名、删除时更新库", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "在消息中包括剧集海报", "NotificationsGotifySettingIncludeSeriesPoster": "包括剧集海报", "NotificationsGotifySettingsAppTokenHelpText": "Gotify 生成的应用凭据", From 44fab9a96c68a25fb1c022e4543498a285de26f7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 31 Aug 2024 06:24:08 +0300 Subject: [PATCH 504/762] Fixed: Generating absolute episode file paths in webhook events Closes #7149 --- .../MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index b4f62addc..f5419dbf6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -92,6 +92,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.Size = _diskProvider.GetFileSize(localEpisode.Path); episodeFile.Quality = localEpisode.Quality; episodeFile.MediaInfo = localEpisode.MediaInfo; + episodeFile.Series = localEpisode.Series; episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; From 9136ee4ad934c10eef743e646f8b1c05eff2a2ff Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 31 Aug 2024 06:25:32 +0300 Subject: [PATCH 505/762] Fixed: Forbid empty spaces in Release Profile restrictions --- .../src/Components/Form/TextTagInputConnector.js | 13 +++++++++++-- .../Profiles/Release/ReleaseProfileController.cs | 15 +++++++++++++-- .../Profiles/Release/ReleaseProfileResource.cs | 6 +++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/Components/Form/TextTagInputConnector.js b/frontend/src/Components/Form/TextTagInputConnector.js index 17677a51e..be5abb16e 100644 --- a/frontend/src/Components/Form/TextTagInputConnector.js +++ b/frontend/src/Components/Form/TextTagInputConnector.js @@ -49,7 +49,11 @@ class TextTagInputConnector extends Component { const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name); newTags.forEach((newTag) => { - newValue.push(newTag.trim()); + const newTagValue = newTag.trim(); + + if (newTagValue) { + newValue.push(newTagValue); + } }); onChange({ name, value: newValue }); @@ -80,7 +84,12 @@ class TextTagInputConnector extends Component { const newValue = [...valueArray]; newValue.splice(tagToReplace.index, 1); - newValue.push(newTag.name.trim()); + + const newTagValue = newTag.name.trim(); + + if (newTagValue) { + newValue.push(newTagValue); + } onChange({ name, value: newValue }); }; diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs index 3d33c3b25..d3549ac75 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; @@ -23,9 +24,19 @@ namespace Sonarr.Api.V3.Profiles.Release SharedValidator.RuleFor(d => d).Custom((restriction, context) => { - if (restriction.MapIgnored().Empty() && restriction.MapRequired().Empty()) + if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty()) { - context.AddFailure("'Must contain' or 'Must not contain' is required"); + context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' or 'Must not contain' is required"); + } + + if (restriction.MapRequired().Any(t => t.IsNullOrWhiteSpace())) + { + context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' should not contain whitespaces or an empty string"); + } + + if (restriction.MapIgnored().Any(t => t.IsNullOrWhiteSpace())) + { + context.AddFailure(nameof(ReleaseProfile.Ignored), "'Must not contain' should not contain whitespaces or an empty string"); } if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId)) diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs index 8c3e3fb78..bd5169e2d 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs @@ -88,18 +88,18 @@ namespace Sonarr.Api.V3.Profiles.Release { if (array.ValueKind == JsonValueKind.String) { - return array.GetString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + return array.GetString().Split(new[] { ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); } if (array.ValueKind == JsonValueKind.Array) { - return JsonSerializer.Deserialize<List<string>>(array); + return array.Deserialize<List<string>>(); } } if (resource is string str) { - return str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + return str.Split(new[] { ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); } throw new BadRequestException($"Invalid field {title}, should be string or string array"); From 53d8c9ba8dd83da02a58dde36a6fbdfdeabad104 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:29:47 +0300 Subject: [PATCH 506/762] Fixed: Importing files without media info available --- .../UpdateMediaInfoServiceFixture.cs | 26 +++++++++++++++++++ .../MediaInfo/UpdateMediaInfoService.cs | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs index 72af3a7ca..c97fc5adf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -60,6 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo { var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(3) .All() + .With(v => v.Path = null) .With(v => v.RelativePath = "media.mkv") .TheFirst(1) .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.CURRENT_MEDIA_INFO_SCHEMA_REVISION }) @@ -86,6 +87,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo { var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(3) .All() + .With(v => v.Path = null) .With(v => v.RelativePath = "media.mkv") .TheFirst(1) .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION }) @@ -112,6 +114,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo { var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(3) .All() + .With(v => v.Path = null) .With(v => v.RelativePath = "media.mkv") .TheFirst(1) .With(v => v.MediaInfo = new MediaInfoModel()) @@ -161,6 +164,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo { var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(2) .All() + .With(v => v.Path = null) .With(v => v.RelativePath = "media.mkv") .TheFirst(1) .With(v => v.RelativePath = "media2.mkv") @@ -240,6 +244,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo public void should_update_media_info() { var episodeFile = Builder<EpisodeFile>.CreateNew() + .With(v => v.Path = null) .With(v => v.RelativePath = "media.mkv") .With(e => e.MediaInfo = new MediaInfoModel { SchemaRevision = 3 }) .Build(); @@ -288,5 +293,26 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo Mocker.GetMock<IMediaFileService>() .Verify(v => v.Update(episodeFile), Times.Never()); } + + [Test] + public void should_not_update_media_info_if_file_does_not_support_media_info() + { + var path = Path.Combine(_series.Path, "media.iso"); + + var episodeFile = Builder<EpisodeFile>.CreateNew() + .With(v => v.Path = path) + .Build(); + + GivenFileExists(); + GivenFailedScan(path); + + Subject.Update(episodeFile, _series); + + Mocker.GetMock<IVideoFileInfoReader>() + .Verify(v => v.GetMediaInfo(path), Times.Once()); + + Mocker.GetMock<IMediaFileService>() + .Verify(v => v.Update(episodeFile), Times.Never()); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index ac4371c92..cde1db229 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; @@ -68,7 +69,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo public bool UpdateMediaInfo(EpisodeFile episodeFile, Series series) { - var path = Path.Combine(series.Path, episodeFile.RelativePath); + var path = episodeFile.Path.IsNotNullOrWhiteSpace() ? episodeFile.Path : Path.Combine(series.Path, episodeFile.RelativePath); if (!_diskProvider.FileExists(path)) { From e1cbc4a78249881de96160739a50c0a399ea4313 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 28 Jul 2024 22:31:16 -0700 Subject: [PATCH 507/762] Convert Components to TypeScript --- frontend/src/Activity/Queue/QueueStatus.tsx | 2 +- frontend/src/App/State/AppSectionState.ts | 2 +- frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/PathsAppState.ts | 29 +++ frontend/src/App/State/SeriesAppState.ts | 2 +- frontend/src/Components/Alert.js | 34 --- frontend/src/Components/Alert.tsx | 18 ++ frontend/src/Components/Card.js | 60 ----- frontend/src/Components/Card.tsx | 39 +++ .../DescriptionList/DescriptionList.js | 33 --- .../DescriptionList/DescriptionList.tsx | 15 ++ .../DescriptionList/DescriptionListItem.js | 46 ---- .../DescriptionList/DescriptionListItem.tsx | 34 +++ .../DescriptionListItemDescription.js | 27 -- .../DescriptionListItemDescription.tsx | 17 ++ .../DescriptionListItemTitle.js | 27 -- .../DescriptionListItemTitle.tsx | 15 ++ frontend/src/Components/DragPreviewLayer.js | 22 -- frontend/src/Components/DragPreviewLayer.tsx | 21 ++ .../src/Components/Error/ErrorBoundary.js | 62 ----- .../src/Components/Error/ErrorBoundary.tsx | 46 ++++ frontend/src/Components/FieldSet.js | 41 --- frontend/src/Components/FieldSet.tsx | 29 +++ .../FileBrowser/FileBrowserModal.js | 39 --- .../FileBrowser/FileBrowserModal.tsx | 23 ++ .../FileBrowser/FileBrowserModalContent.js | 246 ------------------ .../FileBrowser/FileBrowserModalContent.tsx | 237 +++++++++++++++++ .../FileBrowserModalContentConnector.js | 119 --------- .../Components/FileBrowser/FileBrowserRow.js | 62 ----- .../Components/FileBrowser/FileBrowserRow.tsx | 49 ++++ .../FileBrowser/createPathsSelector.ts | 36 +++ .../{HeartRating.js => HeartRating.tsx} | 29 +-- frontend/src/Components/Icon.tsx | 3 +- frontend/src/Components/Label.tsx | 6 +- frontend/src/Components/Link/Button.tsx | 6 +- .../Components/Loading/LoadingIndicator.js | 50 ---- .../Components/Loading/LoadingIndicator.tsx | 32 +++ .../{LoadingMessage.js => LoadingMessage.tsx} | 22 +- .../src/Components/Markdown/InlineMarkdown.js | 74 ------ .../Components/Markdown/InlineMarkdown.tsx | 75 ++++++ ...Attribution.js => MetadataAttribution.tsx} | 5 +- .../src/Components/MonitorToggleButton.js | 80 ------ .../src/Components/MonitorToggleButton.tsx | 65 +++++ .../Components/{NotFound.js => NotFound.tsx} | 15 +- .../src/Components/Page/PageContentBody.tsx | 3 +- frontend/src/Components/Portal.js | 18 -- frontend/src/Components/Portal.tsx | 20 ++ frontend/src/Components/Router/Switch.js | 44 ---- frontend/src/Components/Router/Switch.tsx | 38 +++ .../Components/Scroller/OverlayScroller.js | 179 ------------- .../Components/Scroller/OverlayScroller.tsx | 127 +++++++++ frontend/src/Components/Scroller/Scroller.tsx | 6 +- frontend/src/Components/SpinnerIcon.tsx | 4 +- frontend/src/Components/Tooltip/Popover.js | 43 --- frontend/src/Components/Tooltip/Popover.tsx | 26 ++ frontend/src/Components/Tooltip/Tooltip.js | 235 ----------------- frontend/src/Components/Tooltip/Tooltip.tsx | 216 +++++++++++++++ .../Episode/EpisodeDetailsModalContent.tsx | 1 - frontend/src/Helpers/Props/ScrollDirection.ts | 8 - frontend/src/Helpers/Props/SortDirection.ts | 6 - frontend/src/Helpers/Props/TooltipPosition.ts | 3 - frontend/src/Helpers/Props/align.ts | 2 + frontend/src/Helpers/Props/kinds.ts | 12 + ...crollDirections.js => scrollDirections.ts} | 2 + frontend/src/Helpers/Props/sizes.ts | 8 + .../{sortDirections.js => sortDirections.ts} | 2 + ...ooltipPositions.js => tooltipPositions.ts} | 9 +- .../Episode/SelectEpisodeModalContent.tsx | 2 +- .../Index/Menus/SeriesIndexSortMenu.tsx | 2 +- .../Index/Posters/SeriesIndexPosters.tsx | 2 +- frontend/src/Series/Index/SeriesIndex.tsx | 4 +- .../Series/Index/Table/SeriesIndexTable.tsx | 8 +- .../Index/Table/SeriesIndexTableHeader.tsx | 2 +- .../ManageCustomFormatsModalContent.tsx | 2 +- .../ManageDownloadClientsModalContent.tsx | 2 +- .../Manage/ManageIndexersModalContent.tsx | 2 +- .../src/System/Status/DiskSpace/DiskSpace.tsx | 3 +- frontend/src/typings/callbacks.ts | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 2 +- src/NzbDrone.Core/Localization/Core/fi.json | 2 +- src/NzbDrone.Core/Localization/Core/fr.json | 2 +- src/NzbDrone.Core/Localization/Core/hu.json | 2 +- .../Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/ru.json | 2 +- .../Localization/Core/zh_CN.json | 2 +- 86 files changed, 1305 insertions(+), 1650 deletions(-) create mode 100644 frontend/src/App/State/PathsAppState.ts delete mode 100644 frontend/src/Components/Alert.js create mode 100644 frontend/src/Components/Alert.tsx delete mode 100644 frontend/src/Components/Card.js create mode 100644 frontend/src/Components/Card.tsx delete mode 100644 frontend/src/Components/DescriptionList/DescriptionList.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionList.tsx delete mode 100644 frontend/src/Components/DescriptionList/DescriptionListItem.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItem.tsx delete mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemDescription.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx delete mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemTitle.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx delete mode 100644 frontend/src/Components/DragPreviewLayer.js create mode 100644 frontend/src/Components/DragPreviewLayer.tsx delete mode 100644 frontend/src/Components/Error/ErrorBoundary.js create mode 100644 frontend/src/Components/Error/ErrorBoundary.tsx delete mode 100644 frontend/src/Components/FieldSet.js create mode 100644 frontend/src/Components/FieldSet.tsx delete mode 100644 frontend/src/Components/FileBrowser/FileBrowserModal.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModal.tsx delete mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContent.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx delete mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js delete mode 100644 frontend/src/Components/FileBrowser/FileBrowserRow.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserRow.tsx create mode 100644 frontend/src/Components/FileBrowser/createPathsSelector.ts rename frontend/src/Components/{HeartRating.js => HeartRating.tsx} (53%) delete mode 100644 frontend/src/Components/Loading/LoadingIndicator.js create mode 100644 frontend/src/Components/Loading/LoadingIndicator.tsx rename frontend/src/Components/Loading/{LoadingMessage.js => LoadingMessage.tsx} (63%) delete mode 100644 frontend/src/Components/Markdown/InlineMarkdown.js create mode 100644 frontend/src/Components/Markdown/InlineMarkdown.tsx rename frontend/src/Components/{MetadataAttribution.js => MetadataAttribution.tsx} (79%) delete mode 100644 frontend/src/Components/MonitorToggleButton.js create mode 100644 frontend/src/Components/MonitorToggleButton.tsx rename frontend/src/Components/{NotFound.js => NotFound.tsx} (72%) delete mode 100644 frontend/src/Components/Portal.js create mode 100644 frontend/src/Components/Portal.tsx delete mode 100644 frontend/src/Components/Router/Switch.js create mode 100644 frontend/src/Components/Router/Switch.tsx delete mode 100644 frontend/src/Components/Scroller/OverlayScroller.js create mode 100644 frontend/src/Components/Scroller/OverlayScroller.tsx delete mode 100644 frontend/src/Components/Tooltip/Popover.js create mode 100644 frontend/src/Components/Tooltip/Popover.tsx delete mode 100644 frontend/src/Components/Tooltip/Tooltip.js create mode 100644 frontend/src/Components/Tooltip/Tooltip.tsx delete mode 100644 frontend/src/Helpers/Props/ScrollDirection.ts delete mode 100644 frontend/src/Helpers/Props/SortDirection.ts delete mode 100644 frontend/src/Helpers/Props/TooltipPosition.ts rename frontend/src/Helpers/Props/{scrollDirections.js => scrollDirections.ts} (71%) rename frontend/src/Helpers/Props/{sortDirections.js => sortDirections.ts} (68%) rename frontend/src/Helpers/Props/{tooltipPositions.js => tooltipPositions.ts} (50%) diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx index 2bd7f6d79..31a28f35c 100644 --- a/frontend/src/Activity/Queue/QueueStatus.tsx +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Icon, { IconProps } from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds } from 'Helpers/Props'; -import TooltipPosition from 'Helpers/Props/TooltipPosition'; +import { TooltipPosition } from 'Helpers/Props/tooltipPositions'; import { QueueTrackedDownloadState, QueueTrackedDownloadStatus, diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 18f0adf50..edf2b2d9d 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,5 +1,5 @@ import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 39520d971..4a6951aa3 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -6,6 +6,7 @@ import EpisodesAppState from './EpisodesAppState'; import HistoryAppState from './HistoryAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; import ParseAppState from './ParseAppState'; +import PathsAppState from './PathsAppState'; import QueueAppState from './QueueAppState'; import RootFolderAppState from './RootFolderAppState'; import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; @@ -69,6 +70,7 @@ interface AppState { history: HistoryAppState; interactiveImport: InteractiveImportAppState; parse: ParseAppState; + paths: PathsAppState; queue: QueueAppState; rootFolders: RootFolderAppState; series: SeriesAppState; diff --git a/frontend/src/App/State/PathsAppState.ts b/frontend/src/App/State/PathsAppState.ts new file mode 100644 index 000000000..068a48dc0 --- /dev/null +++ b/frontend/src/App/State/PathsAppState.ts @@ -0,0 +1,29 @@ +interface BasePath { + name: string; + path: string; + size: number; + lastModified: string; +} + +interface File extends BasePath { + type: 'file'; +} + +interface Folder extends BasePath { + type: 'folder'; +} + +export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent'; +export type Path = File | Folder; + +interface PathsAppState { + currentPath: string; + isFetching: boolean; + isPopulated: boolean; + error: Error; + directories: Folder[]; + files: File[]; + parent: string | null; +} + +export default PathsAppState; diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts index 8d13f0c0b..1f8a3427b 100644 --- a/frontend/src/App/State/SeriesAppState.ts +++ b/frontend/src/App/State/SeriesAppState.ts @@ -3,7 +3,7 @@ import AppSectionState, { AppSectionSaveState, } from 'App/State/AppSectionState'; import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import Series from 'Series/Series'; import { Filter, FilterBuilderProp } from './AppState'; diff --git a/frontend/src/Components/Alert.js b/frontend/src/Components/Alert.js deleted file mode 100644 index 418cbf5e6..000000000 --- a/frontend/src/Components/Alert.js +++ /dev/null @@ -1,34 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { kinds } from 'Helpers/Props'; -import styles from './Alert.css'; - -function Alert(props) { - const { className, kind, children, ...otherProps } = props; - - return ( - <div - className={classNames( - className, - styles[kind] - )} - {...otherProps} - > - {children} - </div> - ); -} - -Alert.propTypes = { - className: PropTypes.string, - kind: PropTypes.oneOf(kinds.all), - children: PropTypes.node.isRequired -}; - -Alert.defaultProps = { - className: styles.alert, - kind: kinds.INFO -}; - -export default Alert; diff --git a/frontend/src/Components/Alert.tsx b/frontend/src/Components/Alert.tsx new file mode 100644 index 000000000..92c89e741 --- /dev/null +++ b/frontend/src/Components/Alert.tsx @@ -0,0 +1,18 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Kind } from 'Helpers/Props/kinds'; +import styles from './Alert.css'; + +interface AlertProps { + className?: string; + kind?: Extract<Kind, keyof typeof styles>; + children: React.ReactNode; +} + +function Alert(props: AlertProps) { + const { className = styles.alert, kind = 'info', children } = props; + + return <div className={classNames(className, styles[kind])}>{children}</div>; +} + +export default Alert; diff --git a/frontend/src/Components/Card.js b/frontend/src/Components/Card.js deleted file mode 100644 index c5a4d164c..000000000 --- a/frontend/src/Components/Card.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; -import styles from './Card.css'; - -class Card extends Component { - - // - // Render - - render() { - const { - className, - overlayClassName, - overlayContent, - children, - onPress - } = this.props; - - if (overlayContent) { - return ( - <div className={className}> - <Link - className={styles.underlay} - onPress={onPress} - /> - - <div className={overlayClassName}> - {children} - </div> - </div> - ); - } - - return ( - <Link - className={className} - onPress={onPress} - > - {children} - </Link> - ); - } -} - -Card.propTypes = { - className: PropTypes.string.isRequired, - overlayClassName: PropTypes.string.isRequired, - overlayContent: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, - onPress: PropTypes.func.isRequired -}; - -Card.defaultProps = { - className: styles.card, - overlayClassName: styles.overlay, - overlayContent: false -}; - -export default Card; diff --git a/frontend/src/Components/Card.tsx b/frontend/src/Components/Card.tsx new file mode 100644 index 000000000..24588c841 --- /dev/null +++ b/frontend/src/Components/Card.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Link, { LinkProps } from 'Components/Link/Link'; +import styles from './Card.css'; + +interface CardProps extends Pick<LinkProps, 'onPress'> { + // TODO: Consider using different properties for classname depending if it's overlaying content or not + className?: string; + overlayClassName?: string; + overlayContent?: boolean; + children: React.ReactNode; +} + +function Card(props: CardProps) { + const { + className = styles.card, + overlayClassName = styles.overlay, + overlayContent = false, + children, + onPress, + } = props; + + if (overlayContent) { + return ( + <div className={className}> + <Link className={styles.underlay} onPress={onPress} /> + + <div className={overlayClassName}>{children}</div> + </div> + ); + } + + return ( + <Link className={className} onPress={onPress}> + {children} + </Link> + ); +} + +export default Card; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.js b/frontend/src/Components/DescriptionList/DescriptionList.js deleted file mode 100644 index be2c87c55..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionList.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './DescriptionList.css'; - -class DescriptionList extends Component { - - // - // Render - - render() { - const { - className, - children - } = this.props; - - return ( - <dl className={className}> - {children} - </dl> - ); - } -} - -DescriptionList.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.node -}; - -DescriptionList.defaultProps = { - className: styles.descriptionList -}; - -export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.tsx b/frontend/src/Components/DescriptionList/DescriptionList.tsx new file mode 100644 index 000000000..6deee77e5 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './DescriptionList.css'; + +interface DescriptionListProps { + className?: string; + children?: React.ReactNode; +} + +function DescriptionList(props: DescriptionListProps) { + const { className = styles.descriptionList, children } = props; + + return <dl className={className}>{children}</dl>; +} + +export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js deleted file mode 100644 index 931557045..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import DescriptionListItemDescription from './DescriptionListItemDescription'; -import DescriptionListItemTitle from './DescriptionListItemTitle'; - -class DescriptionListItem extends Component { - - // - // Render - - render() { - const { - className, - titleClassName, - descriptionClassName, - title, - data - } = this.props; - - return ( - <div className={className}> - <DescriptionListItemTitle - className={titleClassName} - > - {title} - </DescriptionListItemTitle> - - <DescriptionListItemDescription - className={descriptionClassName} - > - {data} - </DescriptionListItemDescription> - </div> - ); - } -} - -DescriptionListItem.propTypes = { - className: PropTypes.string, - titleClassName: PropTypes.string, - descriptionClassName: PropTypes.string, - title: PropTypes.string, - data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) -}; - -export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.tsx b/frontend/src/Components/DescriptionList/DescriptionListItem.tsx new file mode 100644 index 000000000..13a7efdd0 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import DescriptionListItemDescription, { + DescriptionListItemDescriptionProps, +} from './DescriptionListItemDescription'; +import DescriptionListItemTitle, { + DescriptionListItemTitleProps, +} from './DescriptionListItemTitle'; + +interface DescriptionListItemProps { + className?: string; + titleClassName?: DescriptionListItemTitleProps['className']; + descriptionClassName?: DescriptionListItemDescriptionProps['className']; + title?: DescriptionListItemTitleProps['children']; + data?: DescriptionListItemDescriptionProps['children']; +} + +function DescriptionListItem(props: DescriptionListItemProps) { + const { className, titleClassName, descriptionClassName, title, data } = + props; + + return ( + <div className={className}> + <DescriptionListItemTitle className={titleClassName}> + {title} + </DescriptionListItemTitle> + + <DescriptionListItemDescription className={descriptionClassName}> + {data} + </DescriptionListItemDescription> + </div> + ); +} + +export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js deleted file mode 100644 index 4ef3c015e..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DescriptionListItemDescription.css'; - -function DescriptionListItemDescription(props) { - const { - className, - children - } = props; - - return ( - <dd className={className}> - {children} - </dd> - ); -} - -DescriptionListItemDescription.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) -}; - -DescriptionListItemDescription.defaultProps = { - className: styles.description -}; - -export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx new file mode 100644 index 000000000..e08c117dc --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; +import styles from './DescriptionListItemDescription.css'; + +export interface DescriptionListItemDescriptionProps { + className?: string; + children?: ReactNode; +} + +function DescriptionListItemDescription( + props: DescriptionListItemDescriptionProps +) { + const { className = styles.description, children } = props; + + return <dd className={className}>{children}</dd>; +} + +export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js deleted file mode 100644 index e1632c1cf..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DescriptionListItemTitle.css'; - -function DescriptionListItemTitle(props) { - const { - className, - children - } = props; - - return ( - <dt className={className}> - {children} - </dt> - ); -} - -DescriptionListItemTitle.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.string -}; - -DescriptionListItemTitle.defaultProps = { - className: styles.title -}; - -export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx new file mode 100644 index 000000000..59ea6955c --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode } from 'react'; +import styles from './DescriptionListItemTitle.css'; + +export interface DescriptionListItemTitleProps { + className?: string; + children?: ReactNode; +} + +function DescriptionListItemTitle(props: DescriptionListItemTitleProps) { + const { className = styles.title, children } = props; + + return <dt className={className}>{children}</dt>; +} + +export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DragPreviewLayer.js b/frontend/src/Components/DragPreviewLayer.js deleted file mode 100644 index a111df70e..000000000 --- a/frontend/src/Components/DragPreviewLayer.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DragPreviewLayer.css'; - -function DragPreviewLayer({ children, ...otherProps }) { - return ( - <div {...otherProps}> - {children} - </div> - ); -} - -DragPreviewLayer.propTypes = { - children: PropTypes.node, - className: PropTypes.string -}; - -DragPreviewLayer.defaultProps = { - className: styles.dragLayer -}; - -export default DragPreviewLayer; diff --git a/frontend/src/Components/DragPreviewLayer.tsx b/frontend/src/Components/DragPreviewLayer.tsx new file mode 100644 index 000000000..2e578504b --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './DragPreviewLayer.css'; + +interface DragPreviewLayerProps { + className?: string; + children?: React.ReactNode; +} + +function DragPreviewLayer({ + className = styles.dragLayer, + children, + ...otherProps +}: DragPreviewLayerProps) { + return ( + <div className={className} {...otherProps}> + {children} + </div> + ); +} + +export default DragPreviewLayer; diff --git a/frontend/src/Components/Error/ErrorBoundary.js b/frontend/src/Components/Error/ErrorBoundary.js deleted file mode 100644 index 88412ad19..000000000 --- a/frontend/src/Components/Error/ErrorBoundary.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as sentry from '@sentry/browser'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -class ErrorBoundary extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - error: null, - info: null - }; - } - - componentDidCatch(error, info) { - this.setState({ - error, - info - }); - - sentry.captureException(error); - } - - // - // Render - - render() { - const { - children, - errorComponent: ErrorComponent, - ...otherProps - } = this.props; - - const { - error, - info - } = this.state; - - if (error) { - return ( - <ErrorComponent - error={error} - info={info} - {...otherProps} - /> - ); - } - - return children; - } -} - -ErrorBoundary.propTypes = { - children: PropTypes.node.isRequired, - errorComponent: PropTypes.elementType.isRequired -}; - -export default ErrorBoundary; diff --git a/frontend/src/Components/Error/ErrorBoundary.tsx b/frontend/src/Components/Error/ErrorBoundary.tsx new file mode 100644 index 000000000..6b27f7a09 --- /dev/null +++ b/frontend/src/Components/Error/ErrorBoundary.tsx @@ -0,0 +1,46 @@ +import * as sentry from '@sentry/browser'; +import React, { Component, ErrorInfo } from 'react'; + +interface ErrorBoundaryProps { + children: React.ReactNode; + errorComponent: React.ElementType; +} + +interface ErrorBoundaryState { + error: Error | null; + info: ErrorInfo | null; +} + +// Class component until componentDidCatch is supported in functional components +class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + error: null, + info: null, + }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + this.setState({ + error, + info, + }); + + sentry.captureException(error); + } + + render() { + const { children, errorComponent: ErrorComponent } = this.props; + const { error, info } = this.state; + + if (error) { + return <ErrorComponent error={error} info={info} />; + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/Components/FieldSet.js b/frontend/src/Components/FieldSet.js deleted file mode 100644 index 8243fd00c..000000000 --- a/frontend/src/Components/FieldSet.js +++ /dev/null @@ -1,41 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { sizes } from 'Helpers/Props'; -import styles from './FieldSet.css'; - -class FieldSet extends Component { - - // - // Render - - render() { - const { - size, - legend, - children - } = this.props; - - return ( - <fieldset className={styles.fieldSet}> - <legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}> - {legend} - </legend> - {children} - </fieldset> - ); - } - -} - -FieldSet.propTypes = { - size: PropTypes.oneOf(sizes.all).isRequired, - legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - children: PropTypes.node -}; - -FieldSet.defaultProps = { - size: sizes.MEDIUM -}; - -export default FieldSet; diff --git a/frontend/src/Components/FieldSet.tsx b/frontend/src/Components/FieldSet.tsx new file mode 100644 index 000000000..c2ff03a7f --- /dev/null +++ b/frontend/src/Components/FieldSet.tsx @@ -0,0 +1,29 @@ +import classNames from 'classnames'; +import React, { ComponentProps } from 'react'; +import { sizes } from 'Helpers/Props'; +import { Size } from 'Helpers/Props/sizes'; +import styles from './FieldSet.css'; + +interface FieldSetProps { + size?: Size; + legend?: ComponentProps<'legend'>['children']; + children?: React.ReactNode; +} + +function FieldSet({ size = sizes.MEDIUM, legend, children }: FieldSetProps) { + return ( + <fieldset className={styles.fieldSet}> + <legend + className={classNames( + styles.legend, + size === sizes.SMALL && styles.small + )} + > + {legend} + </legend> + {children} + </fieldset> + ); +} + +export default FieldSet; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.js b/frontend/src/Components/FileBrowser/FileBrowserModal.js deleted file mode 100644 index 6b58dbb8c..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModal.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import FileBrowserModalContentConnector from './FileBrowserModalContentConnector'; -import styles from './FileBrowserModal.css'; - -class FileBrowserModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - <Modal - className={styles.modal} - isOpen={isOpen} - onModalClose={onModalClose} - > - <FileBrowserModalContentConnector - {...otherProps} - onModalClose={onModalClose} - /> - </Modal> - ); - } -} - -FileBrowserModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.tsx b/frontend/src/Components/FileBrowser/FileBrowserModal.tsx new file mode 100644 index 000000000..0925890de --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import FileBrowserModalContent, { + FileBrowserModalContentProps, +} from './FileBrowserModalContent'; +import styles from './FileBrowserModal.css'; + +interface FileBrowserModalProps extends FileBrowserModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +function FileBrowserModal(props: FileBrowserModalProps) { + const { isOpen, onModalClose, ...otherProps } = props; + + return ( + <Modal className={styles.modal} isOpen={isOpen} onModalClose={onModalClose}> + <FileBrowserModalContent {...otherProps} onModalClose={onModalClose} /> + </Modal> + ); +} + +export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js deleted file mode 100644 index 61ebfaaa4..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ /dev/null @@ -1,246 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import PathInput from 'Components/Form/PathInput'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -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 Scroller from 'Components/Scroller/Scroller'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { kinds, scrollDirections } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import FileBrowserRow from './FileBrowserRow'; -import styles from './FileBrowserModalContent.css'; - -const columns = [ - { - name: 'type', - label: () => translate('Type'), - isVisible: true - }, - { - name: 'name', - label: () => translate('Name'), - isVisible: true - } -]; - -class FileBrowserModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scrollerRef = React.createRef(); - - this.state = { - isFileBrowserModalOpen: false, - currentPath: props.value - }; - } - - componentDidUpdate(prevProps, prevState) { - const { - currentPath - } = this.props; - - if ( - currentPath !== this.state.currentPath && - currentPath !== prevState.currentPath - ) { - this.setState({ currentPath }); - this._scrollerRef.current.scrollTop = 0; - } - } - - // - // Listeners - - onPathInputChange = ({ value }) => { - this.setState({ currentPath: value }); - }; - - onRowPress = (path) => { - this.props.onFetchPaths(path); - }; - - onOkPress = () => { - this.props.onChange({ - name: this.props.name, - value: this.state.currentPath - }); - - this.props.onClearPaths(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - parent, - directories, - files, - isWindowsService, - onModalClose, - ...otherProps - } = this.props; - - const emptyParent = parent === ''; - - return ( - <ModalContent - onModalClose={onModalClose} - > - <ModalHeader> - {translate('FileBrowser')} - </ModalHeader> - - <ModalBody - className={styles.modalBody} - scrollDirection={scrollDirections.NONE} - > - { - isWindowsService && - <Alert - className={styles.mappedDrivesWarning} - kind={kinds.WARNING} - > - <InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} /> - </Alert> - } - - <PathInput - className={styles.pathInput} - placeholder={translate('FileBrowserPlaceholderText')} - hasFileBrowser={false} - {...otherProps} - value={this.state.currentPath} - onChange={this.onPathInputChange} - /> - - <Scroller - ref={this._scrollerRef} - className={styles.scroller} - scrollDirection={scrollDirections.BOTH} - > - { - !!error && - <div>{translate('ErrorLoadingContents')}</div> - } - - { - isPopulated && !error && - <Table - horizontalScroll={false} - columns={columns} - > - <TableBody> - { - emptyParent && - <FileBrowserRow - type="computer" - name={translate('MyComputer')} - path={parent} - onPress={this.onRowPress} - /> - } - - { - !emptyParent && parent && - <FileBrowserRow - type="parent" - name="..." - path={parent} - onPress={this.onRowPress} - /> - } - - { - directories.map((directory) => { - return ( - <FileBrowserRow - key={directory.path} - type={directory.type} - name={directory.name} - path={directory.path} - onPress={this.onRowPress} - /> - ); - }) - } - - { - files.map((file) => { - return ( - <FileBrowserRow - key={file.path} - type={file.type} - name={file.name} - path={file.path} - onPress={this.onRowPress} - /> - ); - }) - } - </TableBody> - </Table> - } - </Scroller> - </ModalBody> - - <ModalFooter> - { - isFetching && - <LoadingIndicator - className={styles.loading} - size={20} - /> - } - - <Button - onPress={onModalClose} - > - {translate('Cancel')} - </Button> - - <Button - onPress={this.onOkPress} - > - {translate('Ok')} - </Button> - </ModalFooter> - </ModalContent> - ); - } -} - -FileBrowserModalContent.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - parent: PropTypes.string, - currentPath: PropTypes.string.isRequired, - directories: PropTypes.arrayOf(PropTypes.object).isRequired, - files: PropTypes.arrayOf(PropTypes.object).isRequired, - isWindowsService: PropTypes.bool.isRequired, - onFetchPaths: PropTypes.func.isRequired, - onClearPaths: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx new file mode 100644 index 000000000..53589551f --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx @@ -0,0 +1,237 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import PathInput from 'Components/Form/PathInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +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 Scroller from 'Components/Scroller/Scroller'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { kinds, scrollDirections } from 'Helpers/Props'; +import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import createPathsSelector from './createPathsSelector'; +import FileBrowserRow from './FileBrowserRow'; +import styles from './FileBrowserModalContent.css'; + +const columns: Column[] = [ + { + name: 'type', + label: () => translate('Type'), + isVisible: true, + }, + { + name: 'name', + label: () => translate('Name'), + isVisible: true, + }, +]; + +const handleClearPaths = () => {}; + +export interface FileBrowserModalContentProps { + name: string; + value: string; + includeFiles?: boolean; + onChange: (args: InputChanged<string>) => unknown; + onModalClose: () => void; +} + +function FileBrowserModalContent(props: FileBrowserModalContentProps) { + const { name, value, includeFiles = true, onChange, onModalClose } = props; + + const dispatch = useDispatch(); + + const { isWindows, mode } = useSelector(createSystemStatusSelector()); + const { isFetching, isPopulated, error, parent, directories, files, paths } = + useSelector(createPathsSelector()); + + const [currentPath, setCurrentPath] = useState(value); + const scrollerRef = useRef(null); + const previousValue = usePrevious(value); + + const emptyParent = parent === ''; + const isWindowsService = isWindows && mode === 'service'; + + const handlePathInputChange = useCallback( + ({ value }: InputChanged<string>) => { + setCurrentPath(value); + }, + [] + ); + + const handleRowPress = useCallback( + (path: string) => { + setCurrentPath(path); + + dispatch( + fetchPaths({ + path, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + }, + [includeFiles, dispatch, setCurrentPath] + ); + + const handleOkPress = useCallback(() => { + onChange({ + name, + value: currentPath, + }); + + dispatch(clearPaths()); + onModalClose(); + }, [name, currentPath, dispatch, onChange, onModalClose]); + + const handleFetchPaths = useCallback( + (path: string) => { + dispatch( + fetchPaths({ + path, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + }, + [includeFiles, dispatch] + ); + + useEffect(() => { + if (value !== previousValue && value !== currentPath) { + setCurrentPath(value); + } + }, [value, previousValue, currentPath, setCurrentPath]); + + useEffect( + () => { + dispatch( + fetchPaths({ + path: currentPath, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + + return () => { + dispatch(clearPaths()); + }; + }, + // This should only run once when the component mounts, + // so we don't need to include the other dependencies. + // eslint-disable-next-line react-hooks/exhaustive-deps + [dispatch] + ); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('FileBrowser')}</ModalHeader> + + <ModalBody + className={styles.modalBody} + scrollDirection={scrollDirections.NONE} + > + {isWindowsService ? ( + <Alert className={styles.mappedDrivesWarning} kind={kinds.WARNING}> + <InlineMarkdown + data={translate('MappedNetworkDrivesWindowsService', { + url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server', + })} + /> + </Alert> + ) : null} + + <PathInput + className={styles.pathInput} + placeholder={translate('FileBrowserPlaceholderText')} + hasFileBrowser={false} + includeFiles={includeFiles} + paths={paths} + name={name} + value={currentPath} + onChange={handlePathInputChange} + onFetchPaths={handleFetchPaths} + onClearPaths={handleClearPaths} + /> + + <Scroller + ref={scrollerRef} + className={styles.scroller} + scrollDirection="both" + > + {error ? <div>{translate('ErrorLoadingContents')}</div> : null} + + {isPopulated && !error ? ( + <Table horizontalScroll={false} columns={columns}> + <TableBody> + {emptyParent ? ( + <FileBrowserRow + type="computer" + name={translate('MyComputer')} + path={parent} + onPress={handleRowPress} + /> + ) : null} + + {!emptyParent && parent ? ( + <FileBrowserRow + type="parent" + name="..." + path={parent} + onPress={handleRowPress} + /> + ) : null} + + {directories.map((directory) => { + return ( + <FileBrowserRow + key={directory.path} + type={directory.type} + name={directory.name} + path={directory.path} + onPress={handleRowPress} + /> + ); + })} + + {files.map((file) => { + return ( + <FileBrowserRow + key={file.path} + type={file.type} + name={file.name} + path={file.path} + onPress={handleRowPress} + /> + ); + })} + </TableBody> + </Table> + ) : null} + </Scroller> + </ModalBody> + + <ModalFooter> + {isFetching ? ( + <LoadingIndicator className={styles.loading} size={20} /> + ) : null} + + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <Button onPress={handleOkPress}>{translate('Ok')}</Button> + </ModalFooter> + </ModalContent> + ); +} + +export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js deleted file mode 100644 index 1a3a41ef0..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js +++ /dev/null @@ -1,119 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import FileBrowserModalContent from './FileBrowserModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.paths, - createSystemStatusSelector(), - (paths, systemStatus) => { - const { - isFetching, - isPopulated, - error, - parent, - currentPath, - directories, - files - } = paths; - - const filteredPaths = _.filter([...directories, ...files], ({ path }) => { - return path.toLowerCase().startsWith(currentPath.toLowerCase()); - }); - - return { - isFetching, - isPopulated, - error, - parent, - currentPath, - directories, - files, - paths: filteredPaths, - isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service' - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchPaths: fetchPaths, - dispatchClearPaths: clearPaths -}; - -class FileBrowserModalContentConnector extends Component { - - // Lifecycle - - componentDidMount() { - const { - value, - includeFiles, - dispatchFetchPaths - } = this.props; - - dispatchFetchPaths({ - path: value, - allowFoldersWithoutTrailingSlashes: true, - includeFiles - }); - } - - // - // Listeners - - onFetchPaths = (path) => { - const { - includeFiles, - dispatchFetchPaths - } = this.props; - - dispatchFetchPaths({ - path, - allowFoldersWithoutTrailingSlashes: true, - includeFiles - }); - }; - - onClearPaths = () => { - // this.props.dispatchClearPaths(); - }; - - onModalClose = () => { - this.props.dispatchClearPaths(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - <FileBrowserModalContent - onFetchPaths={this.onFetchPaths} - onClearPaths={this.onClearPaths} - {...this.props} - onModalClose={this.onModalClose} - /> - ); - } -} - -FileBrowserModalContentConnector.propTypes = { - value: PropTypes.string, - includeFiles: PropTypes.bool.isRequired, - dispatchFetchPaths: PropTypes.func.isRequired, - dispatchClearPaths: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -FileBrowserModalContentConnector.defaultProps = { - includeFiles: false -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector); diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.js b/frontend/src/Components/FileBrowser/FileBrowserRow.js deleted file mode 100644 index 06bb3029d..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserRow.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowButton from 'Components/Table/TableRowButton'; -import { icons } from 'Helpers/Props'; -import styles from './FileBrowserRow.css'; - -function getIconName(type) { - switch (type) { - case 'computer': - return icons.COMPUTER; - case 'drive': - return icons.DRIVE; - case 'file': - return icons.FILE; - case 'parent': - return icons.PARENT; - default: - return icons.FOLDER; - } -} - -class FileBrowserRow extends Component { - - // - // Listeners - - onPress = () => { - this.props.onPress(this.props.path); - }; - - // - // Render - - render() { - const { - type, - name - } = this.props; - - return ( - <TableRowButton onPress={this.onPress}> - <TableRowCell className={styles.type}> - <Icon name={getIconName(type)} /> - </TableRowCell> - - <TableRowCell>{name}</TableRowCell> - </TableRowButton> - ); - } - -} - -FileBrowserRow.propTypes = { - type: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; - -export default FileBrowserRow; diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.tsx b/frontend/src/Components/FileBrowser/FileBrowserRow.tsx new file mode 100644 index 000000000..fe47f1664 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react'; +import { PathType } from 'App/State/PathsAppState'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import styles from './FileBrowserRow.css'; + +function getIconName(type: PathType) { + switch (type) { + case 'computer': + return icons.COMPUTER; + case 'drive': + return icons.DRIVE; + case 'file': + return icons.FILE; + case 'parent': + return icons.PARENT; + default: + return icons.FOLDER; + } +} + +interface FileBrowserRowProps { + type: PathType; + name: string; + path: string; + onPress: (path: string) => void; +} + +function FileBrowserRow(props: FileBrowserRowProps) { + const { type, name, path, onPress } = props; + + const handlePress = useCallback(() => { + onPress(path); + }, [path, onPress]); + + return ( + <TableRowButton onPress={handlePress}> + <TableRowCell className={styles.type}> + <Icon name={getIconName(type)} /> + </TableRowCell> + + <TableRowCell>{name}</TableRowCell> + </TableRowButton> + ); +} + +export default FileBrowserRow; diff --git a/frontend/src/Components/FileBrowser/createPathsSelector.ts b/frontend/src/Components/FileBrowser/createPathsSelector.ts new file mode 100644 index 000000000..5da830bd5 --- /dev/null +++ b/frontend/src/Components/FileBrowser/createPathsSelector.ts @@ -0,0 +1,36 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createPathsSelector() { + return createSelector( + (state: AppState) => state.paths, + (paths) => { + const { + isFetching, + isPopulated, + error, + parent, + currentPath, + directories, + files, + } = paths; + + const filteredPaths = [...directories, ...files].filter(({ path }) => { + return path.toLowerCase().startsWith(currentPath.toLowerCase()); + }); + + return { + isFetching, + isPopulated, + error, + parent, + currentPath, + directories, + files, + paths: filteredPaths, + }; + } + ); +} + +export default createPathsSelector; diff --git a/frontend/src/Components/HeartRating.js b/frontend/src/Components/HeartRating.tsx similarity index 53% rename from frontend/src/Components/HeartRating.js rename to frontend/src/Components/HeartRating.tsx index 01744b143..774cb4239 100644 --- a/frontend/src/Components/HeartRating.js +++ b/frontend/src/Components/HeartRating.tsx @@ -1,22 +1,22 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import Icon from 'Components/Icon'; +import Icon, { IconProps } from 'Components/Icon'; import Tooltip from 'Components/Tooltip/Tooltip'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './HeartRating.css'; -function HeartRating({ rating, votes, iconSize }) { +interface HeartRatingProps { + rating: number; + votes?: number; + iconSize?: IconProps['size']; +} + +function HeartRating({ rating, votes = 0, iconSize = 14 }: HeartRatingProps) { return ( <Tooltip anchor={ <span className={styles.rating}> - <Icon - className={styles.heart} - name={icons.HEART} - size={iconSize} - /> - + <Icon className={styles.heart} name={icons.HEART} size={iconSize} /> {rating * 10}% </span> } @@ -27,15 +27,4 @@ function HeartRating({ rating, votes, iconSize }) { ); } -HeartRating.propTypes = { - rating: PropTypes.number.isRequired, - votes: PropTypes.number.isRequired, - iconSize: PropTypes.number.isRequired -}; - -HeartRating.defaultProps = { - votes: 0, - iconSize: 14 -}; - export default HeartRating; diff --git a/frontend/src/Components/Icon.tsx b/frontend/src/Components/Icon.tsx index 86ff57a30..ea5279840 100644 --- a/frontend/src/Components/Icon.tsx +++ b/frontend/src/Components/Icon.tsx @@ -5,6 +5,7 @@ import { import classNames from 'classnames'; import React, { ComponentProps } from 'react'; import { kinds } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; import styles from './Icon.css'; export interface IconProps @@ -14,7 +15,7 @@ export interface IconProps > { containerClassName?: ComponentProps<'span'>['className']; name: FontAwesomeIconProps['icon']; - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; + kind?: Extract<Kind, keyof typeof styles>; size?: number; isSpinning?: FontAwesomeIconProps['spin']; title?: string | (() => string); diff --git a/frontend/src/Components/Label.tsx b/frontend/src/Components/Label.tsx index 411cefddf..9ab360f42 100644 --- a/frontend/src/Components/Label.tsx +++ b/frontend/src/Components/Label.tsx @@ -1,11 +1,13 @@ import classNames from 'classnames'; import React, { ComponentProps, ReactNode } from 'react'; import { kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; import styles from './Label.css'; export interface LabelProps extends ComponentProps<'span'> { - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; - size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + kind?: Extract<Kind, keyof typeof styles>; + size?: Extract<Size, keyof typeof styles>; outline?: boolean; children: ReactNode; } diff --git a/frontend/src/Components/Link/Button.tsx b/frontend/src/Components/Link/Button.tsx index c512b3a90..cf2293f59 100644 --- a/frontend/src/Components/Link/Button.tsx +++ b/frontend/src/Components/Link/Button.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames'; import React from 'react'; import { align, kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; import Link, { LinkProps } from './Link'; import styles from './Button.css'; @@ -9,8 +11,8 @@ export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> { (typeof align.all)[number], keyof typeof styles >; - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; - size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + kind?: Extract<Kind, keyof typeof styles>; + size?: Extract<Size, keyof typeof styles>; children: Required<LinkProps['children']>; } diff --git a/frontend/src/Components/Loading/LoadingIndicator.js b/frontend/src/Components/Loading/LoadingIndicator.js deleted file mode 100644 index 60f692a45..000000000 --- a/frontend/src/Components/Loading/LoadingIndicator.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './LoadingIndicator.css'; - -function LoadingIndicator({ className, rippleClassName, size }) { - const sizeInPx = `${size}px`; - const width = sizeInPx; - const height = sizeInPx; - - return ( - <div - className={className} - style={{ height }} - > - <div - className={styles.rippleContainer} - style={{ width, height }} - > - <div - className={rippleClassName} - style={{ width, height }} - /> - - <div - className={rippleClassName} - style={{ width, height }} - /> - - <div - className={rippleClassName} - style={{ width, height }} - /> - </div> - </div> - ); -} - -LoadingIndicator.propTypes = { - className: PropTypes.string, - rippleClassName: PropTypes.string, - size: PropTypes.number -}; - -LoadingIndicator.defaultProps = { - className: styles.loading, - rippleClassName: styles.ripple, - size: 50 -}; - -export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingIndicator.tsx b/frontend/src/Components/Loading/LoadingIndicator.tsx new file mode 100644 index 000000000..00ad803fa --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styles from './LoadingIndicator.css'; + +interface LoadingIndicatorProps { + className?: string; + rippleClassName?: string; + size?: number; +} + +function LoadingIndicator({ + className = styles.loading, + rippleClassName = styles.ripple, + size = 50, +}: LoadingIndicatorProps) { + const sizeInPx = `${size}px`; + const width = sizeInPx; + const height = sizeInPx; + + return ( + <div className={className} style={{ height }}> + <div className={styles.rippleContainer} style={{ width, height }}> + <div className={rippleClassName} style={{ width, height }} /> + + <div className={rippleClassName} style={{ width, height }} /> + + <div className={rippleClassName} style={{ width, height }} /> + </div> + </div> + ); +} + +export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingMessage.js b/frontend/src/Components/Loading/LoadingMessage.tsx similarity index 63% rename from frontend/src/Components/Loading/LoadingMessage.js rename to frontend/src/Components/Loading/LoadingMessage.tsx index 99ab06469..bc2b48af5 100644 --- a/frontend/src/Components/Loading/LoadingMessage.js +++ b/frontend/src/Components/Loading/LoadingMessage.tsx @@ -8,21 +8,21 @@ const messages = [ 'Bleep Bloop.', 'Locating the required gigapixels to render...', 'Spinning up the hamster wheel...', - 'At least you\'re not on hold', + "At least you're not on hold", 'Hum something loud while others stare', 'Loading humorous message... Please Wait', - 'I could\'ve been faster in Python', - 'Don\'t forget to rewind your episodes', + "I could've been faster in Python", + "Don't forget to rewind your episodes", 'Congratulations! You are the 1000th visitor.', - 'HELP! I\'m being held hostage and forced to write these stupid lines!', + "HELP! I'm being held hostage and forced to write these stupid lines!", 'RE-calibrating the internet...', - 'I\'ll be here all week', - 'Don\'t forget to tip your waitress', + "I'll be here all week", + "Don't forget to tip your waitress", 'Apply directly to the forehead', - 'Loading Battlestation' + 'Loading Battlestation', ]; -let message = null; +let message: string | null = null; function LoadingMessage() { if (!message) { @@ -30,11 +30,7 @@ function LoadingMessage() { message = messages[index]; } - return ( - <div className={styles.loadingMessage}> - {message} - </div> - ); + return <div className={styles.loadingMessage}>{message}</div>; } export default LoadingMessage; diff --git a/frontend/src/Components/Markdown/InlineMarkdown.js b/frontend/src/Components/Markdown/InlineMarkdown.js deleted file mode 100644 index 993bb241e..000000000 --- a/frontend/src/Components/Markdown/InlineMarkdown.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; - -class InlineMarkdown extends Component { - - // - // Render - - render() { - const { - className, - data, - blockClassName - } = this.props; - - // For now only replace links or code blocks (not both) - const markdownBlocks = []; - if (data) { - const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g); - - let endIndex = 0; - let match = null; - - while ((match = linkRegex.exec(data)) !== null) { - if (match.index > endIndex) { - markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); - } - - markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>); - endIndex = match.index + match[0].length; - } - - if (endIndex !== data.length && markdownBlocks.length > 0) { - markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); - } - - const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g); - - endIndex = 0; - match = null; - let matchedCode = false; - - while ((match = codeRegex.exec(data)) !== null) { - matchedCode = true; - - if (match.index > endIndex) { - markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); - } - - markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>); - endIndex = match.index + match[0].length; - } - - if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) { - markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); - } - - if (markdownBlocks.length === 0) { - markdownBlocks.push(data); - } - } - - return <span className={className}>{markdownBlocks}</span>; - } -} - -InlineMarkdown.propTypes = { - className: PropTypes.string, - data: PropTypes.string, - blockClassName: PropTypes.string -}; - -export default InlineMarkdown; diff --git a/frontend/src/Components/Markdown/InlineMarkdown.tsx b/frontend/src/Components/Markdown/InlineMarkdown.tsx new file mode 100644 index 000000000..80e99336a --- /dev/null +++ b/frontend/src/Components/Markdown/InlineMarkdown.tsx @@ -0,0 +1,75 @@ +import React, { ReactElement } from 'react'; +import Link from 'Components/Link/Link'; + +interface InlineMarkdownProps { + className?: string; + data?: string; + blockClassName?: string; +} + +function InlineMarkdown(props: InlineMarkdownProps) { + const { className, data, blockClassName } = props; + + // For now only replace links or code blocks (not both) + const markdownBlocks: (ReactElement | string)[] = []; + + if (data) { + const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g); + + let endIndex = 0; + let match = null; + + while ((match = linkRegex.exec(data)) !== null) { + if (match.index > endIndex) { + markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); + } + + markdownBlocks.push( + <Link key={match.index} to={match[2]}> + {match[1]} + </Link> + ); + endIndex = match.index + match[0].length; + } + + if (endIndex !== data.length && markdownBlocks.length > 0) { + markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); + } + + const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g); + + endIndex = 0; + match = null; + let matchedCode = false; + + while ((match = codeRegex.exec(data)) !== null) { + matchedCode = true; + + if (match.index > endIndex) { + markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); + } + + markdownBlocks.push( + <code + key={`code-${match.index}`} + className={blockClassName ?? undefined} + > + {match[0].substring(1, match[0].length - 1)} + </code> + ); + endIndex = match.index + match[0].length; + } + + if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) { + markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); + } + + if (markdownBlocks.length === 0) { + markdownBlocks.push(data); + } + } + + return <span className={className}>{markdownBlocks}</span>; +} + +export default InlineMarkdown; diff --git a/frontend/src/Components/MetadataAttribution.js b/frontend/src/Components/MetadataAttribution.tsx similarity index 79% rename from frontend/src/Components/MetadataAttribution.js rename to frontend/src/Components/MetadataAttribution.tsx index b32eea696..dc0d0a2c9 100644 --- a/frontend/src/Components/MetadataAttribution.js +++ b/frontend/src/Components/MetadataAttribution.tsx @@ -6,10 +6,7 @@ import styles from './MetadataAttribution.css'; export default function MetadataAttribution() { return ( <div className={styles.container}> - <Link - className={styles.attribution} - to="/settings/metadatasource" - > + <Link className={styles.attribution} to="/settings/metadatasource"> {translate('MetadataProvidedBy', { provider: 'TheTVDB' })} </Link> </div> diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js deleted file mode 100644 index cb92e43ba..000000000 --- a/frontend/src/Components/MonitorToggleButton.js +++ /dev/null @@ -1,80 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './MonitorToggleButton.css'; - -function getTooltip(monitored, isDisabled) { - if (isDisabled) { - return translate('ToggleMonitoredSeriesUnmonitored '); - } - - if (monitored) { - return translate('ToggleMonitoredToUnmonitored'); - } - - return translate('ToggleUnmonitoredToMonitored'); -} - -class MonitorToggleButton extends Component { - - // - // Listeners - - onPress = (event) => { - const shiftKey = event.nativeEvent.shiftKey; - - this.props.onPress(!this.props.monitored, { shiftKey }); - }; - - // - // Render - - render() { - const { - className, - monitored, - isDisabled, - isSaving, - size, - ...otherProps - } = this.props; - - const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; - - return ( - <SpinnerIconButton - className={classNames( - className, - isDisabled && styles.isDisabled - )} - name={iconName} - size={size} - title={getTooltip(monitored, isDisabled)} - isDisabled={isDisabled} - isSpinning={isSaving} - {...otherProps} - onPress={this.onPress} - /> - ); - } -} - -MonitorToggleButton.propTypes = { - className: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - size: PropTypes.number, - isDisabled: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - onPress: PropTypes.func.isRequired -}; - -MonitorToggleButton.defaultProps = { - className: styles.toggleButton, - isDisabled: false, - isSaving: false -}; - -export default MonitorToggleButton; diff --git a/frontend/src/Components/MonitorToggleButton.tsx b/frontend/src/Components/MonitorToggleButton.tsx new file mode 100644 index 000000000..1c1fcbbeb --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.tsx @@ -0,0 +1,65 @@ +import classNames from 'classnames'; +import React, { SyntheticEvent, useCallback, useMemo } from 'react'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './MonitorToggleButton.css'; + +interface MonitorToggleButtonProps { + className?: string; + monitored: boolean; + size?: number; + isDisabled?: boolean; + isSaving?: boolean; + onPress: (value: boolean, options: { shiftKey: boolean }) => unknown; +} + +function MonitorToggleButton(props: MonitorToggleButtonProps) { + const { + className = styles.toggleButton, + monitored, + isDisabled = false, + isSaving = false, + size, + onPress, + ...otherProps + } = props; + + const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; + + const title = useMemo(() => { + if (isDisabled) { + return translate('ToggleMonitoredSeriesUnmonitored'); + } + + if (monitored) { + return translate('ToggleMonitoredToUnmonitored'); + } + + return translate('ToggleUnmonitoredToMonitored'); + }, [monitored, isDisabled]); + + const handlePress = useCallback( + (event: SyntheticEvent<HTMLLinkElement, MouseEvent>) => { + const shiftKey = event.nativeEvent.shiftKey; + + onPress(!monitored, { shiftKey }); + }, + [monitored, onPress] + ); + + return ( + <SpinnerIconButton + className={classNames(className, isDisabled && styles.isDisabled)} + name={iconName} + size={size} + title={title} + isDisabled={isDisabled} + isSpinning={isSaving} + {...otherProps} + onPress={handlePress} + /> + ); +} + +export default MonitorToggleButton; diff --git a/frontend/src/Components/NotFound.js b/frontend/src/Components/NotFound.tsx similarity index 72% rename from frontend/src/Components/NotFound.js rename to frontend/src/Components/NotFound.tsx index da4221200..30f4aebd4 100644 --- a/frontend/src/Components/NotFound.js +++ b/frontend/src/Components/NotFound.tsx @@ -1,18 +1,19 @@ -import PropTypes from 'prop-types'; import React from 'react'; import PageContent from 'Components/Page/PageContent'; import translate from 'Utilities/String/translate'; import styles from './NotFound.css'; -function NotFound(props) { +interface NotFoundProps { + message?: string; +} + +function NotFound(props: NotFoundProps) { const { message = translate('DefaultNotFoundMessage') } = props; return ( <PageContent title="MIA"> <div className={styles.container}> - <div className={styles.message}> - {message} - </div> + <div className={styles.message}>{message}</div> <img className={styles.image} @@ -23,8 +24,4 @@ function NotFound(props) { ); } -NotFound.propTypes = { - message: PropTypes.string -}; - export default NotFound; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index ce9b0e7e4..9c3ffcd0a 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -1,6 +1,5 @@ import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; import { isLocked } from 'Utilities/scrollLock'; import styles from './PageContentBody.css'; @@ -36,7 +35,7 @@ const PageContentBody = forwardRef( ref={ref} {...otherProps} className={className} - scrollDirection={ScrollDirection.Vertical} + scrollDirection="vertical" onScroll={onScrollWrapper} > <div className={innerClassName}>{children}</div> diff --git a/frontend/src/Components/Portal.js b/frontend/src/Components/Portal.js deleted file mode 100644 index 2e5237093..000000000 --- a/frontend/src/Components/Portal.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; - -function Portal(props) { - const { children, target } = props; - return ReactDOM.createPortal(children, target); -} - -Portal.propTypes = { - children: PropTypes.node.isRequired, - target: PropTypes.object.isRequired -}; - -Portal.defaultProps = { - target: document.getElementById('portal-root') -}; - -export default Portal; diff --git a/frontend/src/Components/Portal.tsx b/frontend/src/Components/Portal.tsx new file mode 100644 index 000000000..1cc1c7da6 --- /dev/null +++ b/frontend/src/Components/Portal.tsx @@ -0,0 +1,20 @@ +import ReactDOM from 'react-dom'; + +interface PortalProps { + children: Parameters<typeof ReactDOM.createPortal>[0]; + target?: Parameters<typeof ReactDOM.createPortal>[1]; +} + +const defaultTarget = document.getElementById('portal-root'); + +function Portal(props: PortalProps) { + const { children, target = defaultTarget } = props; + + if (!target) { + return null; + } + + return ReactDOM.createPortal(children, target); +} + +export default Portal; diff --git a/frontend/src/Components/Router/Switch.js b/frontend/src/Components/Router/Switch.js deleted file mode 100644 index 6479d5291..000000000 --- a/frontend/src/Components/Router/Switch.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Switch as RouterSwitch } from 'react-router-dom'; -import { map } from 'Helpers/elementChildren'; -import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; - -class Switch extends Component { - - // - // Render - - render() { - const { - children - } = this.props; - - return ( - <RouterSwitch> - { - map(children, (child) => { - const { - path: childPath, - addUrlBase = true - } = child.props; - - if (!childPath) { - return child; - } - - const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; - - return React.cloneElement(child, { path }); - }) - } - </RouterSwitch> - ); - } -} - -Switch.propTypes = { - children: PropTypes.node.isRequired -}; - -export default Switch; diff --git a/frontend/src/Components/Router/Switch.tsx b/frontend/src/Components/Router/Switch.tsx new file mode 100644 index 000000000..032471681 --- /dev/null +++ b/frontend/src/Components/Router/Switch.tsx @@ -0,0 +1,38 @@ +import React, { Children, ReactElement, ReactNode } from 'react'; +import { Switch as RouterSwitch } from 'react-router-dom'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; + +interface ExtendedRoute { + path: string; + addUrlBase?: boolean; +} + +interface SwitchProps { + children: ReactNode; +} + +function Switch({ children }: SwitchProps) { + return ( + <RouterSwitch> + {Children.map(children, (child) => { + if (!React.isValidElement<ExtendedRoute>(child)) { + return child; + } + + const elementChild: ReactElement<ExtendedRoute> = child; + + const { path: childPath, addUrlBase = true } = elementChild.props; + + if (!childPath) { + return child; + } + + const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; + + return React.cloneElement(child, { path }); + })} + </RouterSwitch> + ); +} + +export default Switch; diff --git a/frontend/src/Components/Scroller/OverlayScroller.js b/frontend/src/Components/Scroller/OverlayScroller.js deleted file mode 100644 index e590c42b2..000000000 --- a/frontend/src/Components/Scroller/OverlayScroller.js +++ /dev/null @@ -1,179 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Scrollbars } from 'react-custom-scrollbars-2'; -import { scrollDirections } from 'Helpers/Props'; -import styles from './OverlayScroller.css'; - -const SCROLLBAR_SIZE = 10; - -class OverlayScroller extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scroller = null; - this._isScrolling = false; - } - - componentDidUpdate(prevProps) { - const { - scrollTop - } = this.props; - - if ( - !this._isScrolling && - scrollTop != null && - scrollTop !== prevProps.scrollTop - ) { - this._scroller.scrollTop(scrollTop); - } - } - - // - // Control - - _setScrollRef = (ref) => { - this._scroller = ref; - - if (ref) { - this.props.registerScroller(ref.view); - } - }; - - _renderThumb = (props) => { - return ( - <div - className={this.props.trackClassName} - {...props} - /> - ); - }; - - _renderTrackHorizontal = ({ style, props }) => { - const finalStyle = { - ...style, - right: 2, - bottom: 2, - left: 2, - borderRadius: 3, - height: SCROLLBAR_SIZE - }; - - return ( - <div - className={styles.track} - style={finalStyle} - {...props} - /> - ); - }; - - _renderTrackVertical = ({ style, props }) => { - const finalStyle = { - ...style, - right: 2, - bottom: 2, - top: 2, - borderRadius: 3, - width: SCROLLBAR_SIZE - }; - - return ( - <div - className={styles.track} - style={finalStyle} - {...props} - /> - ); - }; - - _renderView = (props) => { - return ( - <div - className={this.props.className} - {...props} - /> - ); - }; - - // - // Listers - - onScrollStart = () => { - this._isScrolling = true; - }; - - onScrollStop = () => { - this._isScrolling = false; - }; - - onScroll = (event) => { - const { - scrollTop, - scrollLeft - } = event.currentTarget; - - this._isScrolling = true; - const onScroll = this.props.onScroll; - - if (onScroll) { - onScroll({ scrollTop, scrollLeft }); - } - }; - - // - // Render - - render() { - const { - autoHide, - autoScroll, - children - } = this.props; - - return ( - <Scrollbars - ref={this._setScrollRef} - autoHide={autoHide} - hideTracksWhenNotNeeded={autoScroll} - renderTrackHorizontal={this._renderTrackHorizontal} - renderTrackVertical={this._renderTrackVertical} - renderThumbHorizontal={this._renderThumb} - renderThumbVertical={this._renderThumb} - renderView={this._renderView} - onScrollStart={this.onScrollStart} - onScrollStop={this.onScrollStop} - onScroll={this.onScroll} - > - {children} - </Scrollbars> - ); - } - -} - -OverlayScroller.propTypes = { - className: PropTypes.string, - trackClassName: PropTypes.string, - scrollTop: PropTypes.number, - scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, - autoHide: PropTypes.bool.isRequired, - autoScroll: PropTypes.bool.isRequired, - children: PropTypes.node, - onScroll: PropTypes.func, - registerScroller: PropTypes.func -}; - -OverlayScroller.defaultProps = { - className: styles.scroller, - trackClassName: styles.thumb, - scrollDirection: scrollDirections.VERTICAL, - autoHide: false, - autoScroll: true, - registerScroller: () => { /* no-op */ } -}; - -export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/OverlayScroller.tsx b/frontend/src/Components/Scroller/OverlayScroller.tsx new file mode 100644 index 000000000..b242642e8 --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.tsx @@ -0,0 +1,127 @@ +import React, { ComponentPropsWithoutRef, useCallback, useRef } from 'react'; +import { Scrollbars } from 'react-custom-scrollbars-2'; +import { ScrollDirection } from 'Helpers/Props/scrollDirections'; +import { OnScroll } from './Scroller'; +import styles from './OverlayScroller.css'; + +const SCROLLBAR_SIZE = 10; + +interface OverlayScrollerProps { + className?: string; + trackClassName?: string; + scrollTop?: number; + scrollDirection: ScrollDirection; + autoHide: boolean; + autoScroll: boolean; + children?: React.ReactNode; + onScroll?: (payload: OnScroll) => void; +} + +interface ScrollbarTrackProps { + style: React.CSSProperties; + props: ComponentPropsWithoutRef<'div'>; +} + +function OverlayScroller(props: OverlayScrollerProps) { + const { + autoHide = false, + autoScroll = true, + className = styles.scroller, + trackClassName = styles.thumb, + children, + onScroll, + } = props; + const scrollBarRef = useRef<Scrollbars>(null); + const isScrolling = useRef(false); + + const handleScrollStart = useCallback(() => { + isScrolling.current = true; + }, []); + const handleScrollStop = useCallback(() => { + isScrolling.current = false; + }, []); + + const handleScroll = useCallback(() => { + if (!scrollBarRef.current) { + return; + } + + const { scrollTop, scrollLeft } = scrollBarRef.current.getValues(); + isScrolling.current = true; + + if (onScroll) { + onScroll({ scrollTop, scrollLeft }); + } + }, [onScroll]); + + const renderThumb = useCallback( + (props: ComponentPropsWithoutRef<'div'>) => { + return <div className={trackClassName} {...props} />; + }, + [trackClassName] + ); + + const renderTrackHorizontal = useCallback( + ({ style, props: trackProps }: ScrollbarTrackProps) => { + const finalStyle = { + ...style, + right: 2, + bottom: 2, + left: 2, + borderRadius: 3, + height: SCROLLBAR_SIZE, + }; + + return ( + <div className={styles.track} style={finalStyle} {...trackProps} /> + ); + }, + [] + ); + + const renderTrackVertical = useCallback( + ({ style, props: trackProps }: ScrollbarTrackProps) => { + const finalStyle = { + ...style, + right: 2, + bottom: 2, + top: 2, + borderRadius: 3, + width: SCROLLBAR_SIZE, + }; + + return ( + <div className={styles.track} style={finalStyle} {...trackProps} /> + ); + }, + [] + ); + + const renderView = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any) => { + return <div className={className} {...props} />; + }, + [className] + ); + + return ( + <Scrollbars + ref={scrollBarRef} + autoHide={autoHide} + hideTracksWhenNotNeeded={autoScroll} + renderTrackHorizontal={renderTrackHorizontal} + renderTrackVertical={renderTrackVertical} + renderThumbHorizontal={renderThumb} + renderThumbVertical={renderThumb} + renderView={renderView} + onScrollStart={handleScrollStart} + onScrollStop={handleScrollStop} + onScroll={handleScroll} + > + {children} + </Scrollbars> + ); +} + +export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 37b16eebd..95f85c119 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useRef, } from 'react'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; +import { ScrollDirection } from 'Helpers/Props/scrollDirections'; import styles from './Scroller.css'; export interface OnScroll { @@ -33,7 +33,7 @@ const Scroller = forwardRef( className, autoFocus = false, autoScroll = true, - scrollDirection = ScrollDirection.Vertical, + scrollDirection = 'vertical', children, scrollTop, initialScrollTop, @@ -59,7 +59,7 @@ const Scroller = forwardRef( currentRef.current.scrollTop = scrollTop; } - if (autoFocus && scrollDirection !== ScrollDirection.None) { + if (autoFocus && scrollDirection !== 'none') { currentRef.current.focus({ preventScroll: true }); } }, [autoFocus, currentRef, scrollDirection, scrollTop]); diff --git a/frontend/src/Components/SpinnerIcon.tsx b/frontend/src/Components/SpinnerIcon.tsx index 27ddadc41..d9124d692 100644 --- a/frontend/src/Components/SpinnerIcon.tsx +++ b/frontend/src/Components/SpinnerIcon.tsx @@ -10,11 +10,13 @@ export interface SpinnerIconProps extends IconProps { export default function SpinnerIcon({ name, spinningName = icons.SPINNER, + isSpinning, ...otherProps }: SpinnerIconProps) { return ( <Icon - name={(otherProps.isSpinning && spinningName) || name} + name={(isSpinning && spinningName) || name} + isSpinning={isSpinning} {...otherProps} /> ); diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js deleted file mode 100644 index 1fe92fcbf..000000000 --- a/frontend/src/Components/Tooltip/Popover.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { tooltipPositions } from 'Helpers/Props'; -import Tooltip from './Tooltip'; -import styles from './Popover.css'; - -function Popover(props) { - const { - title, - body, - ...otherProps - } = props; - - return ( - <Tooltip - {...otherProps} - bodyClassName={styles.tooltipBody} - tooltip={ - <div> - <div className={styles.title}> - {title} - </div> - - <div className={styles.body}> - {body} - </div> - </div> - } - /> - ); -} - -Popover.propTypes = { - className: PropTypes.string, - bodyClassName: PropTypes.string, - anchor: PropTypes.node.isRequired, - title: PropTypes.string.isRequired, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - position: PropTypes.oneOf(tooltipPositions.all), - canFlip: PropTypes.bool -}; - -export default Popover; diff --git a/frontend/src/Components/Tooltip/Popover.tsx b/frontend/src/Components/Tooltip/Popover.tsx new file mode 100644 index 000000000..4c6781343 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Tooltip, { TooltipProps } from './Tooltip'; +import styles from './Popover.css'; + +interface PopoverProps extends Omit<TooltipProps, 'tooltip' | 'bodyClassName'> { + title: string; + body: React.ReactNode; +} + +function Popover({ title, body, ...otherProps }: PopoverProps) { + return ( + <Tooltip + {...otherProps} + bodyClassName={styles.tooltipBody} + tooltip={ + <div> + <div className={styles.title}>{title}</div> + + <div className={styles.body}>{body}</div> + </div> + } + /> + ); +} + +export default Popover; diff --git a/frontend/src/Components/Tooltip/Tooltip.js b/frontend/src/Components/Tooltip/Tooltip.js deleted file mode 100644 index 1499e7451..000000000 --- a/frontend/src/Components/Tooltip/Tooltip.js +++ /dev/null @@ -1,235 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Manager, Popper, Reference } from 'react-popper'; -import Portal from 'Components/Portal'; -import { kinds, tooltipPositions } from 'Helpers/Props'; -import dimensions from 'Styles/Variables/dimensions'; -import { isMobile as isMobileUtil } from 'Utilities/browser'; -import styles from './Tooltip.css'; - -let maxWidth = null; - -function getMaxWidth() { - const windowWidth = window.innerWidth; - - if (windowWidth >= parseInt(dimensions.breakpointLarge)) { - maxWidth = 800; - } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { - maxWidth = 650; - } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { - maxWidth = 500; - } else { - maxWidth = 450; - } - - return maxWidth; -} - -class Tooltip extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scheduleUpdate = null; - this._closeTimeout = null; - this._maxWidth = maxWidth || getMaxWidth(); - - this.state = { - isOpen: false - }; - } - - componentDidUpdate() { - if (this._scheduleUpdate && this.state.isOpen) { - this._scheduleUpdate(); - } - } - - componentWillUnmount() { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - } - - // - // Control - - computeMaxSize = (data) => { - const { - top, - right, - bottom, - left - } = data.offsets.reference; - - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - - if ((/^top/).test(data.placement)) { - data.styles.maxHeight = top - 20; - } else if ((/^bottom/).test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom - 20; - } else if ((/^right/).test(data.placement)) { - data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20); - data.styles.maxHeight = top - 20; - } else { - data.styles.maxWidth = Math.min(this._maxWidth, left - 20); - data.styles.maxHeight = top - 20; - } - - return data; - }; - - // - // Listeners - - onMeasure = ({ width }) => { - this.setState({ width }); - }; - - onClick = () => { - if (isMobileUtil()) { - this.setState({ isOpen: !this.state.isOpen }); - } - }; - - onMouseEnter = () => { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - - this.setState({ isOpen: true }); - }; - - onMouseLeave = () => { - this._closeTimeout = setTimeout(() => { - this.setState({ isOpen: false }); - }, 100); - }; - - // - // Render - - render() { - const { - className, - bodyClassName, - anchor, - tooltip, - kind, - position, - canFlip - } = this.props; - - return ( - <Manager> - <Reference> - {({ ref }) => ( - <span - ref={ref} - className={className} - onClick={this.onClick} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} - > - {anchor} - </span> - )} - </Reference> - - <Portal> - <Popper - placement={position} - // Disable events to improve performance when many tooltips - // are shown (Quality Definitions for example). - eventsEnabled={false} - modifiers={{ - computeMaxHeight: { - order: 851, - enabled: true, - fn: this.computeMaxSize - }, - preventOverflow: { - // Fixes positioning for tooltips in the queue - // and likely others. - escapeWithReference: false - }, - flip: { - enabled: canFlip - } - }} - > - {({ ref, style, placement, arrowProps, scheduleUpdate }) => { - this._scheduleUpdate = scheduleUpdate; - - const popperPlacement = placement ? placement.split('-')[0] : position; - const vertical = popperPlacement === 'top' || popperPlacement === 'bottom'; - - return ( - <div - ref={ref} - className={classNames( - styles.tooltipContainer, - vertical ? styles.verticalContainer : styles.horizontalContainer - )} - style={style} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} - > - <div - className={this.state.isOpen ? classNames( - styles.arrow, - styles[kind], - styles[popperPlacement] - ) : styles.arrowDisabled} - ref={arrowProps.ref} - style={arrowProps.style} - /> - { - this.state.isOpen ? - <div - className={classNames( - styles.tooltip, - styles[kind] - )} - > - <div - className={bodyClassName} - > - {tooltip} - </div> - </div> : - null - } - </div> - ); - }} - </Popper> - </Portal> - </Manager> - ); - } -} - -Tooltip.propTypes = { - className: PropTypes.string, - bodyClassName: PropTypes.string.isRequired, - anchor: PropTypes.node.isRequired, - tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), - position: PropTypes.oneOf(tooltipPositions.all), - canFlip: PropTypes.bool.isRequired -}; - -Tooltip.defaultProps = { - bodyClassName: styles.body, - kind: kinds.DEFAULT, - position: tooltipPositions.TOP, - canFlip: false -}; - -export default Tooltip; diff --git a/frontend/src/Components/Tooltip/Tooltip.tsx b/frontend/src/Components/Tooltip/Tooltip.tsx new file mode 100644 index 000000000..35cce5738 --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.tsx @@ -0,0 +1,216 @@ +import classNames from 'classnames'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Manager, Popper, Reference } from 'react-popper'; +import Portal from 'Components/Portal'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import dimensions from 'Styles/Variables/dimensions'; +import { isMobile as isMobileUtil } from 'Utilities/browser'; +import styles from './Tooltip.css'; + +export interface TooltipProps { + className?: string; + bodyClassName?: string; + anchor: React.ReactNode; + tooltip: string | React.ReactNode; + kind?: Extract<Kind, keyof typeof styles>; + position?: (typeof tooltipPositions.all)[number]; + canFlip?: boolean; +} +function Tooltip(props: TooltipProps) { + const { + className, + bodyClassName = styles.body, + anchor, + tooltip, + kind = kinds.DEFAULT, + position = tooltipPositions.TOP, + canFlip = false, + } = props; + + const closeTimeout = useRef(0); + const updater = useRef<(() => void) | null>(null); + const [isOpen, setIsOpen] = useState(false); + + const handleClick = useCallback(() => { + if (!isMobileUtil()) { + return; + } + + setIsOpen((isOpen) => { + return !isOpen; + }); + }, [setIsOpen]); + + const handleMouseEnter = useCallback(() => { + // Mobile will fire mouse enter and click events rapidly, + // this causes the tooltip not to open on the first press. + // Ignore the mouse enter event on mobile. + if (isMobileUtil()) { + return; + } + + if (closeTimeout.current) { + window.clearTimeout(closeTimeout.current); + } + + setIsOpen(true); + }, [setIsOpen]); + + const handleMouseLeave = useCallback(() => { + // Still listen for mouse leave on mobile to allow clicks outside to close the tooltip. + + setTimeout(() => { + setIsOpen(false); + }, 100); + }, [setIsOpen]); + + const maxWidth = useMemo(() => { + const windowWidth = window.innerWidth; + + if (windowWidth >= parseInt(dimensions.breakpointLarge)) { + return 800; + } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { + return 650; + } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { + return 500; + } + + return 450; + }, []); + + const computeMaxSize = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data: any) => { + const { top, right, bottom, left } = data.offsets.reference; + + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + if (/^top/.test(data.placement)) { + data.styles.maxHeight = top - 20; + } else if (/^bottom/.test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom - 20; + } else if (/^right/.test(data.placement)) { + data.styles.maxWidth = Math.min(maxWidth, windowWidth - right - 20); + data.styles.maxHeight = top - 20; + } else { + data.styles.maxWidth = Math.min(maxWidth, left - 20); + data.styles.maxHeight = top - 20; + } + + return data; + }, + [maxWidth] + ); + + useEffect(() => { + const currentTimeout = closeTimeout.current; + + if (updater.current && isOpen) { + updater.current(); + } + + return () => { + if (currentTimeout) { + window.clearTimeout(currentTimeout); + } + }; + }); + + return ( + <Manager> + <Reference> + {({ ref }) => ( + <span + ref={ref} + className={className} + onClick={handleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + {anchor} + </span> + )} + </Reference> + + <Portal> + <Popper + // @ts-expect-error - PopperJS types are not in sync with our position types. + placement={position} + // Disable events to improve performance when many tooltips + // are shown (Quality Definitions for example). + eventsEnabled={false} + modifiers={{ + computeMaxHeight: { + order: 851, + enabled: true, + fn: computeMaxSize, + }, + preventOverflow: { + // Fixes positioning for tooltips in the queue + // and likely others. + escapeWithReference: false, + }, + flip: { + enabled: canFlip, + }, + }} + > + {({ ref, style, placement, arrowProps, scheduleUpdate }) => { + updater.current = scheduleUpdate; + + const popperPlacement = placement + ? placement.split('-')[0] + : position; + const vertical = + popperPlacement === 'top' || popperPlacement === 'bottom'; + + return ( + <div + ref={ref} + className={classNames( + styles.tooltipContainer, + vertical + ? styles.verticalContainer + : styles.horizontalContainer + )} + style={style} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div + ref={arrowProps.ref} + className={ + isOpen + ? classNames( + styles.arrow, + styles[kind], + // @ts-expect-error - is a string that may not exist in styles + styles[popperPlacement] + ) + : styles.arrowDisabled + } + style={arrowProps.style} + /> + {isOpen ? ( + <div className={classNames(styles.tooltip, styles[kind])}> + <div className={bodyClassName}>{tooltip}</div> + </div> + ) : null} + </div> + ); + }} + </Popper> + </Portal> + </Manager> + ); +} + +export default Tooltip; diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.tsx b/frontend/src/Episode/EpisodeDetailsModalContent.tsx index d049ab9f7..05a08f16f 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContent.tsx +++ b/frontend/src/Episode/EpisodeDetailsModalContent.tsx @@ -111,7 +111,6 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) { <ModalContent onModalClose={onModalClose}> <ModalHeader> <MonitorToggleButton - id={episodeId} monitored={monitored} size={18} isDisabled={!seriesMonitored} diff --git a/frontend/src/Helpers/Props/ScrollDirection.ts b/frontend/src/Helpers/Props/ScrollDirection.ts deleted file mode 100644 index 0da932d22..000000000 --- a/frontend/src/Helpers/Props/ScrollDirection.ts +++ /dev/null @@ -1,8 +0,0 @@ -enum ScrollDirection { - Horizontal = 'horizontal', - Vertical = 'vertical', - None = 'none', - Both = 'both', -} - -export default ScrollDirection; diff --git a/frontend/src/Helpers/Props/SortDirection.ts b/frontend/src/Helpers/Props/SortDirection.ts deleted file mode 100644 index ac027fadc..000000000 --- a/frontend/src/Helpers/Props/SortDirection.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum SortDirection { - Ascending = 'ascending', - Descending = 'descending', -} - -export default SortDirection; diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts deleted file mode 100644 index 885c73470..000000000 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; - -export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.ts b/frontend/src/Helpers/Props/align.ts index f381959c6..06b19ca11 100644 --- a/frontend/src/Helpers/Props/align.ts +++ b/frontend/src/Helpers/Props/align.ts @@ -3,3 +3,5 @@ export const CENTER = 'center'; export const RIGHT = 'right'; export const all = [LEFT, CENTER, RIGHT]; + +export type Align = 'left' | 'center' | 'right'; diff --git a/frontend/src/Helpers/Props/kinds.ts b/frontend/src/Helpers/Props/kinds.ts index 5d4d53057..77930ab0b 100644 --- a/frontend/src/Helpers/Props/kinds.ts +++ b/frontend/src/Helpers/Props/kinds.ts @@ -21,3 +21,15 @@ export const all = [ SUCCESS, WARNING, ] as const; + +export type Kind = + | 'danger' + | 'default' + | 'disabled' + | 'info' + | 'inverse' + | 'pink' + | 'primary' + | 'purple' + | 'success' + | 'warning'; diff --git a/frontend/src/Helpers/Props/scrollDirections.js b/frontend/src/Helpers/Props/scrollDirections.ts similarity index 71% rename from frontend/src/Helpers/Props/scrollDirections.js rename to frontend/src/Helpers/Props/scrollDirections.ts index 1ae61143b..e82fdfae6 100644 --- a/frontend/src/Helpers/Props/scrollDirections.js +++ b/frontend/src/Helpers/Props/scrollDirections.ts @@ -4,3 +4,5 @@ export const HORIZONTAL = 'horizontal'; export const VERTICAL = 'vertical'; export const all = [NONE, HORIZONTAL, VERTICAL, BOTH]; + +export type ScrollDirection = 'none' | 'both' | 'horizontal' | 'vertical'; diff --git a/frontend/src/Helpers/Props/sizes.ts b/frontend/src/Helpers/Props/sizes.ts index 809f0397a..526f49f8c 100644 --- a/frontend/src/Helpers/Props/sizes.ts +++ b/frontend/src/Helpers/Props/sizes.ts @@ -13,3 +13,11 @@ export const all = [ EXTRA_LARGE, EXTRA_EXTRA_LARGE, ] as const; + +export type Size = + | 'extraSmall' + | 'small' + | 'medium' + | 'large' + | 'extraLarge' + | 'extraExtraLarge'; diff --git a/frontend/src/Helpers/Props/sortDirections.js b/frontend/src/Helpers/Props/sortDirections.ts similarity index 68% rename from frontend/src/Helpers/Props/sortDirections.js rename to frontend/src/Helpers/Props/sortDirections.ts index ff3b17bb6..f082cfa59 100644 --- a/frontend/src/Helpers/Props/sortDirections.js +++ b/frontend/src/Helpers/Props/sortDirections.ts @@ -2,3 +2,5 @@ export const ASCENDING = 'ascending'; export const DESCENDING = 'descending'; export const all = [ASCENDING, DESCENDING]; + +export type SortDirection = 'ascending' | 'descending'; diff --git a/frontend/src/Helpers/Props/tooltipPositions.js b/frontend/src/Helpers/Props/tooltipPositions.ts similarity index 50% rename from frontend/src/Helpers/Props/tooltipPositions.js rename to frontend/src/Helpers/Props/tooltipPositions.ts index bca3c4ed4..9dcd543a3 100644 --- a/frontend/src/Helpers/Props/tooltipPositions.js +++ b/frontend/src/Helpers/Props/tooltipPositions.ts @@ -3,9 +3,6 @@ export const RIGHT = 'right'; export const BOTTOM = 'bottom'; export const LEFT = 'left'; -export const all = [ - TOP, - RIGHT, - BOTTOM, - LEFT -]; +export const all = [TOP, RIGHT, BOTTOM, LEFT]; + +export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index c29e7ac56..c87b98380 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -15,7 +15,7 @@ import TableBody from 'Components/Table/TableBody'; import Episode from 'Episode/Episode'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds, scrollDirections } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { clearEpisodes, fetchEpisodes, diff --git a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx index 461ae5dcf..d3698e501 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx @@ -3,7 +3,7 @@ import MenuContent from 'Components/Menu/MenuContent'; import SortMenu from 'Components/Menu/SortMenu'; import SortMenuItem from 'Components/Menu/SortMenuItem'; import { align } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import translate from 'Utilities/String/translate'; interface SeriesIndexSortMenuProps { diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index b4f859f84..636cd2064 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -5,7 +5,7 @@ import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import useMeasure from 'Helpers/Hooks/useMeasure'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import SeriesIndexPoster from 'Series/Index/Posters/SeriesIndexPoster'; import Series from 'Series/Series'; import dimensions from 'Styles/Variables/dimensions'; diff --git a/frontend/src/Series/Index/SeriesIndex.tsx b/frontend/src/Series/Index/SeriesIndex.tsx index a38351c76..1f9a3e549 100644 --- a/frontend/src/Series/Index/SeriesIndex.tsx +++ b/frontend/src/Series/Index/SeriesIndex.tsx @@ -22,7 +22,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import withScrollPosition from 'Components/withScrollPosition'; import { align, icons, kinds } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { DESCENDING } from 'Helpers/Props/sortDirections'; import ParseToolbarButton from 'Parse/ParseToolbarButton'; import NoSeries from 'Series/NoSeries'; import { executeCommand } from 'Store/Actions/commandActions'; @@ -201,7 +201,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { const order = Object.keys(characters).sort(); // Reverse if sorting descending - if (sortDirection === SortDirection.Descending) { + if (sortDirection === DESCENDING) { order.reverse(); } diff --git a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx index e6b4ca010..1c6064e5c 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx @@ -7,8 +7,7 @@ import AppState from 'App/State/AppState'; import Scroller from 'Components/Scroller/Scroller'; import Column from 'Components/Table/Column'; import useMeasure from 'Helpers/Hooks/useMeasure'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import Series from 'Series/Series'; import dimensions from 'Styles/Variables/dimensions'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; @@ -172,10 +171,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { return ( <div ref={measureRef}> - <Scroller - className={styles.tableScroller} - scrollDirection={ScrollDirection.Horizontal} - > + <Scroller className={styles.tableScroller} scrollDirection="horizontal"> <SeriesIndexTableHeader showBanners={showBanners} columns={columns} diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx index 7dc61400f..b459cbc3e 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx @@ -9,7 +9,7 @@ import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; import { icons } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { setSeriesSort, setSeriesTableOption, diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx index eab8a4d67..bd16c74e9 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx @@ -14,7 +14,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { bulkDeleteCustomFormats, bulkEditCustomFormats, diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 656f91ef6..1d078b5b2 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -14,7 +14,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { bulkDeleteDownloadClients, bulkEditDownloadClients, diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 7b0b3c0b2..a79143c99 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -14,7 +14,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { bulkDeleteIndexers, bulkEditIndexers, diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx index 2174e5b1e..ebbebe29e 100644 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx @@ -11,6 +11,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import TableRow from 'Components/Table/TableRow'; import { kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; import { fetchDiskSpace } from 'Store/Actions/systemActions'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; @@ -67,7 +68,7 @@ function DiskSpace() { const { freeSpace, totalSpace } = item; const diskUsage = 100 - (freeSpace / totalSpace) * 100; - let diskUsageKind: (typeof kinds.all)[number] = kinds.PRIMARY; + let diskUsageKind: Kind = 'primary'; if (diskUsage > 90) { diskUsageKind = kinds.DANGER; diff --git a/frontend/src/typings/callbacks.ts b/frontend/src/typings/callbacks.ts index 0114efeb0..2f8f6a36d 100644 --- a/frontend/src/typings/callbacks.ts +++ b/frontend/src/typings/callbacks.ts @@ -1,4 +1,4 @@ -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; export type SortCallback = ( sortKey: string, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 1e83d3453..88c8d1c84 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1982,7 +1982,7 @@ "Titles": "Titles", "Today": "Today", "TodayAt": "Today at {time}", - "ToggleMonitoredSeriesUnmonitored ": "Cannot toggle monitored state when series is unmonitored", + "ToggleMonitoredSeriesUnmonitored": "Cannot toggle monitored state when series is unmonitored", "ToggleMonitoredToUnmonitored": "Monitored, click to unmonitor", "ToggleUnmonitoredToMonitored": "Unmonitored, click to monitor", "Tomorrow": "Tomorrow", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index bb78aa4e6..96e88625a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1595,7 +1595,7 @@ "TestParsing": "Probar análisis", "ThemeHelpText": "Cambia 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", + "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", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 4d4e22902..2be390cfa 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -328,7 +328,7 @@ "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tomorrow": "Huomenna", "TestParsing": "Testaa jäsennystä", - "ToggleMonitoredSeriesUnmonitored ": "Valvontatilaa ei ole mahdollista muuttaa, jos sarjaa ei valvota.", + "ToggleMonitoredSeriesUnmonitored": "Valvontatilaa ei ole mahdollista muuttaa, jos sarjaa ei valvota.", "Trace": "Jäljitys", "TotalRecords": "Rivien kokonaismäärä: {totalRecords}", "TotalSpace": "Kokonaistila", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 8cc2c0e61..ac290a7f4 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -779,7 +779,7 @@ "Time": "Heure", "TimeFormat": "Format de l'heure", "TimeLeft": "Temps restant", - "ToggleMonitoredSeriesUnmonitored ": "Impossible de basculer entre l'état surveillé lorsque la série n'est pas surveillée", + "ToggleMonitoredSeriesUnmonitored": "Impossible de basculer entre l'état surveillé lorsque la série n'est pas surveillée", "ToggleMonitoredToUnmonitored": "Surveillé, cliquez pour annuler la surveillance", "TotalFileSize": "Taille totale des fichiers", "TotalRecords": "Enregistrements totaux : {totalRecords}", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index e1e241b77..23a5f0207 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -992,7 +992,7 @@ "SourcePath": "Forrás útvonala", "TableOptionsButton": "Táblázat opciók gomb", "TheTvdb": "TheTVDB", - "ToggleMonitoredSeriesUnmonitored ": "Nem lehet átkapcsolni a figyelt állapotot, ha a sorozat nem figyelhető", + "ToggleMonitoredSeriesUnmonitored": "Nem lehet átkapcsolni a figyelt állapotot, ha a sorozat nem figyelhető", "Ui": "Felület", "UiSettingsLoadError": "Nem sikerült betölteni a felhasználói felület beállításait", "SelectDropdown": "Válassz...", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 46e80207a..85ea050dd 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1288,7 +1288,7 @@ "TablePageSizeMaximum": "O tamanho da página não pode exceder {maximumValue}", "Tba": "A ser anunciado", "Titles": "Título", - "ToggleMonitoredSeriesUnmonitored ": "Não é possível alternar o estado monitorado quando a série não é monitorada", + "ToggleMonitoredSeriesUnmonitored": "Não é possível alternar o estado monitorado quando a série não é monitorada", "ToggleMonitoredToUnmonitored": "Monitorado, clique para cancelar o monitoramento", "ToggleUnmonitoredToMonitored": "Não monitorado, clique para monitorar", "TotalRecords": "Total de registros: {totalRecords}", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index e714b0f2b..6a3971c60 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1668,7 +1668,7 @@ "StopSelecting": "Прекратить выбор", "Status": "Статус", "SupportedCustomConditions": "{appName} поддерживает настраиваемые условия в соответствии со свойствами релиза, указанными ниже.", - "ToggleMonitoredSeriesUnmonitored ": "Невозможно переключить отслеживаемое состояние, если сериал не отслеживается", + "ToggleMonitoredSeriesUnmonitored": "Невозможно переключить отслеживаемое состояние, если сериал не отслеживается", "Failed": "Неудачно", "FilterSeriesPlaceholder": "Фильтр сериалов", "OrganizeSelectedSeriesModalAlert": "Совет: Чтобы просмотреть переименование, выберите «Отмена», затем выберите любой заголовок эпизода и используйте этот значок:", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 4c9aa4df7..4871e00c7 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1165,7 +1165,7 @@ "Test": "测试", "ThemeHelpText": "改变应用界面主题,选择“自动”主题会通过操作系统主题来自适应白天黑夜模式。(受Theme.Park启发)", "TimeFormat": "时间格式", - "ToggleMonitoredSeriesUnmonitored ": "当系列不受监控时,无法切换监控状态", + "ToggleMonitoredSeriesUnmonitored": "当系列不受监控时,无法切换监控状态", "ToggleMonitoredToUnmonitored": "已监视,单击可取消监视", "Total": "全部的", "TorrentsDisabled": "Torrents关闭", From c80bd81bb96b99db15ada43fa467047527da7043 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 2 Sep 2024 15:25:19 +0000 Subject: [PATCH 508/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Nota Inutilis <hugo@notainutilis.fr> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fr.json | 6 +++--- src/NzbDrone.Core/Localization/Core/tr.json | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index ac290a7f4..c91afde38 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -366,7 +366,7 @@ "NoHistory": "Aucun historique", "MonitoredStatus": "Surveillé/Statut", "MultiEpisodeStyle": "Style multi-épisodes", - "NoChanges": "Aucuns changements", + "NoChanges": "Aucun changement", "NoSeasons": "Pas de saisons", "PosterSize": "Taille des affiches", "PosterOptions": "Paramètres des affiches", @@ -1560,8 +1560,8 @@ "MonitorNewItems": "Surveiller les nouveaux éléments", "UsenetBlackholeNzbFolder": "Dossier Nzb", "IndexerSettingsApiPath": "Chemin d'accès à l'API", - "IndexerSettingsSeedTime": "Temps d'envoie", - "IndexerSettingsSeedRatio": "Ratio d'envoie", + "IndexerSettingsSeedTime": "Temps d'envoi", + "IndexerSettingsSeedRatio": "Ratio d'envoi", "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", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index b1f3d15a3..bb900c16b 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -853,5 +853,7 @@ "DeleteSelected": "Seçileni Sil", "LogSizeLimit": "Log Boyutu Sınırı", "LogSizeLimitHelpText": "Arşivlemeden önce MB cinsinden maksimum log dosya boyutu. Varsayılan 1 MB'tır.", - "ProgressBarProgress": "İlerleme Çubuğu %{progress} seviyesinde" + "ProgressBarProgress": "İlerleme Çubuğu %{progress} seviyesinde", + "CountVotes": "{votes} oy", + "UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}" } From 546e9fd1d06759af08b94f2a758148cf4d105ae0 Mon Sep 17 00:00:00 2001 From: ManiMatter <124743318+ManiMatter@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:24:55 +0200 Subject: [PATCH 509/762] New: Last Searched column on Wanted screens --- frontend/src/Episode/Episode.ts | 1 + frontend/src/Store/Actions/wantedActions.js | 12 ++++++++++++ .../src/Wanted/CutoffUnmet/CutoffUnmetConnector.js | 6 ++++-- frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 12 ++++++++++++ frontend/src/Wanted/Missing/MissingConnector.js | 6 ++++-- frontend/src/Wanted/Missing/MissingRow.js | 12 ++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + src/Sonarr.Api.V3/Episodes/EpisodeResource.cs | 3 ++- 8 files changed, 48 insertions(+), 5 deletions(-) diff --git a/frontend/src/Episode/Episode.ts b/frontend/src/Episode/Episode.ts index 87ae86657..c154e0278 100644 --- a/frontend/src/Episode/Episode.ts +++ b/frontend/src/Episode/Episode.ts @@ -9,6 +9,7 @@ interface Episode extends ModelBase { episodeNumber: number; airDate: string; airDateUtc?: string; + lastSearchTime?: string; runtime: number; absoluteEpisodeNumber?: number; sceneSeasonNumber?: number; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index bb39416aa..dac4d0c8d 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -50,6 +50,12 @@ export const defaultState = { isSortable: true, isVisible: true }, + { + name: 'episodes.lastSearchTime', + label: () => translate('LastSearched'), + isSortable: true, + isVisible: false + }, { name: 'status', label: () => translate('Status'), @@ -122,6 +128,12 @@ export const defaultState = { isSortable: true, isVisible: true }, + { + name: 'episodes.lastSearchTime', + label: () => translate('LastSearched'), + isSortable: true, + isVisible: false + }, { name: 'languages', label: () => translate('Languages'), diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index 002365f3a..6b52df496 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -126,14 +126,16 @@ class CutoffUnmetConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.EPISODE_SEARCH, - episodeIds: selected + episodeIds: selected, + commandFinished: this.repopulate }); }; onSearchAllCutoffUnmetPress = (monitored) => { this.props.executeCommand({ name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH, - monitored + monitored, + commandFinished: this.repopulate }); }; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index a51ead746..05fed682c 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -26,6 +26,7 @@ function CutoffUnmetRow(props) { sceneAbsoluteEpisodeNumber, unverifiedSceneNumbering, airDateUtc, + lastSearchTime, title, isSelected, columns, @@ -106,6 +107,16 @@ function CutoffUnmetRow(props) { ); } + if (name === 'episodes.lastSearchTime') { + return ( + <RelativeDateCell + key={name} + date={lastSearchTime} + includeSeconds={true} + /> + ); + } + if (name === 'languages') { return ( <TableRowCell @@ -166,6 +177,7 @@ CutoffUnmetRow.propTypes = { sceneAbsoluteEpisodeNumber: PropTypes.number, unverifiedSceneNumbering: PropTypes.bool.isRequired, airDateUtc: PropTypes.string.isRequired, + lastSearchTime: PropTypes.string, title: PropTypes.string.isRequired, isSelected: PropTypes.bool, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js index b20baf358..d6035ab11 100644 --- a/frontend/src/Wanted/Missing/MissingConnector.js +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -117,14 +117,16 @@ class MissingConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.EPISODE_SEARCH, - episodeIds: selected + episodeIds: selected, + commandFinished: this.repopulate }); }; onSearchAllMissingPress = (monitored) => { this.props.executeCommand({ name: commandNames.MISSING_EPISODE_SEARCH, - monitored + monitored, + commandFinished: this.repopulate }); }; diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index 0831a2bc3..b5d02db21 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -25,6 +25,7 @@ function MissingRow(props) { sceneAbsoluteEpisodeNumber, unverifiedSceneNumbering, airDateUtc, + lastSearchTime, title, isSelected, columns, @@ -109,6 +110,16 @@ function MissingRow(props) { ); } + if (name === 'episodes.lastSearchTime') { + return ( + <RelativeDateCell + key={name} + date={lastSearchTime} + includeSeconds={true} + /> + ); + } + if (name === 'status') { return ( <TableRowCell @@ -156,6 +167,7 @@ MissingRow.propTypes = { sceneAbsoluteEpisodeNumber: PropTypes.number, unverifiedSceneNumbering: PropTypes.bool.isRequired, airDateUtc: PropTypes.string.isRequired, + lastSearchTime: PropTypes.string, title: PropTypes.string.isRequired, isSelected: PropTypes.bool, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 88c8d1c84..3e77ffdc1 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1069,6 +1069,7 @@ "Large": "Large", "LastDuration": "Last Duration", "LastExecution": "Last Execution", + "LastSearched": "Last Searched", "LastUsed": "Last Used", "LastWriteTime": "Last Write Time", "LatestSeason": "Latest Season", diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs index b073a0670..86b3aa377 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs @@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.Episodes public string Title { get; set; } public string AirDate { get; set; } public DateTime? AirDateUtc { get; set; } + public DateTime? LastSearchTime { get; set; } public int Runtime { get; set; } public string FinaleType { get; set; } public string Overview { get; set; } @@ -35,7 +36,6 @@ namespace Sonarr.Api.V3.Episodes public DateTime? EndTime { get; set; } public DateTime? GrabDate { get; set; } public SeriesResource Series { get; set; } - public List<MediaCover> Images { get; set; } // Hiding this so people don't think its usable (only used to set the initial state) @@ -68,6 +68,7 @@ namespace Sonarr.Api.V3.Episodes Runtime = model.Runtime, FinaleType = model.FinaleType, Overview = model.Overview, + LastSearchTime = model.LastSearchTime, // EpisodeFile From 0a0e03dca045bab36dec8516cfba430e1d1ba536 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:49:26 +0300 Subject: [PATCH 510/762] Convert Interactive Search to TypeScript --- frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/ReleasesAppState.ts | 10 + frontend/src/Components/Table/Column.ts | 2 + .../Episode/Search/EpisodeSearchConnector.js | 4 +- .../InteractiveSearch/InteractiveSearch.js | 234 ----------------- .../InteractiveSearch/InteractiveSearch.tsx | 247 ++++++++++++++++++ .../InteractiveSearchConnector.js | 95 ------- .../InteractiveSearchFilterModal.tsx | 65 +++++ .../InteractiveSearchFilterModalConnector.js | 32 --- .../InteractiveSearchRow.tsx | 52 +--- .../InteractiveSearchType.ts | 3 + .../OverrideMatch/OverrideMatchModal.tsx | 2 +- .../OverrideMatchModalContent.tsx | 2 +- .../InteractiveSearch/{Peers.js => Peers.tsx} | 27 +- .../src/InteractiveSearch/ReleaseEpisode.ts | 10 - .../SeasonInteractiveSearchModalContent.tsx | 4 +- frontend/src/typings/Release.ts | 53 ++++ 17 files changed, 409 insertions(+), 435 deletions(-) create mode 100644 frontend/src/App/State/ReleasesAppState.ts delete mode 100644 frontend/src/InteractiveSearch/InteractiveSearch.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearch.tsx delete mode 100644 frontend/src/InteractiveSearch/InteractiveSearchConnector.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx delete mode 100644 frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchType.ts rename frontend/src/InteractiveSearch/{Peers.js => Peers.tsx} (63%) delete mode 100644 frontend/src/InteractiveSearch/ReleaseEpisode.ts create mode 100644 frontend/src/typings/Release.ts diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 4a6951aa3..33638f91f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -8,6 +8,7 @@ import InteractiveImportAppState from './InteractiveImportAppState'; import ParseAppState from './ParseAppState'; import PathsAppState from './PathsAppState'; import QueueAppState from './QueueAppState'; +import ReleasesAppState from './ReleasesAppState'; import RootFolderAppState from './RootFolderAppState'; import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; import SettingsAppState from './SettingsAppState'; @@ -72,6 +73,7 @@ interface AppState { parse: ParseAppState; paths: PathsAppState; queue: QueueAppState; + releases: ReleasesAppState; rootFolders: RootFolderAppState; series: SeriesAppState; seriesIndex: SeriesIndexAppState; diff --git a/frontend/src/App/State/ReleasesAppState.ts b/frontend/src/App/State/ReleasesAppState.ts new file mode 100644 index 000000000..350f6eac8 --- /dev/null +++ b/frontend/src/App/State/ReleasesAppState.ts @@ -0,0 +1,10 @@ +import AppSectionState, { + AppSectionFilterState, +} from 'App/State/AppSectionState'; +import Release from 'typings/Release'; + +interface ReleasesAppState + extends AppSectionState<Release>, + AppSectionFilterState<Release> {} + +export default ReleasesAppState; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 24674c3fc..22d22e963 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { SortDirection } from 'Helpers/Props/sortDirections'; type PropertyFunction<T> = () => T; @@ -9,6 +10,7 @@ interface Column { className?: string; columnLabel?: string; isSortable?: boolean; + fixedSortDirection?: SortDirection; isVisible: boolean; isModifiable?: boolean; } diff --git a/frontend/src/Episode/Search/EpisodeSearchConnector.js b/frontend/src/Episode/Search/EpisodeSearchConnector.js index 36e8e667f..9b41dd9c4 100644 --- a/frontend/src/Episode/Search/EpisodeSearchConnector.js +++ b/frontend/src/Episode/Search/EpisodeSearchConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import * as commandNames from 'Commands/commandNames'; -import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; +import InteractiveSearch from 'InteractiveSearch/InteractiveSearch'; import { executeCommand } from 'Store/Actions/commandActions'; import EpisodeSearch from './EpisodeSearch'; @@ -65,7 +65,7 @@ class EpisodeSearchConnector extends Component { if (this.state.isInteractiveSearchOpen) { return ( - <InteractiveSearchConnector + <InteractiveSearch type="episode" searchPayload={{ episodeId }} /> diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js deleted file mode 100644 index bea804902..000000000 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ /dev/null @@ -1,234 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageMenuButton from 'Components/Menu/PageMenuButton'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { align, icons, kinds, sortDirections } from 'Helpers/Props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; -import InteractiveSearchRow from './InteractiveSearchRow'; -import styles from './InteractiveSearch.css'; - -const columns = [ - { - name: 'protocol', - label: () => translate('Source'), - isSortable: true, - isVisible: true - }, - { - name: 'age', - label: () => translate('Age'), - isSortable: true, - isVisible: true - }, - { - name: 'title', - label: () => translate('Title'), - isSortable: true, - isVisible: true - }, - { - name: 'indexer', - label: () => translate('Indexer'), - isSortable: true, - isVisible: true - }, - { - name: 'size', - label: () => translate('Size'), - isSortable: true, - isVisible: true - }, - { - name: 'peers', - label: () => translate('Peers'), - isSortable: true, - isVisible: true - }, - { - name: 'languageWeight', - label: () => translate('Languages'), - isSortable: true, - isVisible: true - }, - { - name: 'qualityWeight', - label: () => translate('Quality'), - isSortable: true, - isVisible: true - }, - { - name: 'customFormatScore', - label: React.createElement(Icon, { - name: icons.SCORE, - title: () => translate('CustomFormatScore') - }), - isSortable: true, - isVisible: true - }, - { - name: 'indexerFlags', - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isSortable: true, - isVisible: true - }, - { - name: 'rejections', - label: React.createElement(Icon, { - name: icons.DANGER, - title: () => translate('Rejections') - }), - isSortable: true, - fixedSortDirection: sortDirections.ASCENDING, - isVisible: true - }, - { - name: 'releaseWeight', - label: React.createElement(Icon, { name: icons.DOWNLOAD }), - isSortable: true, - fixedSortDirection: sortDirections.ASCENDING, - isVisible: true - } -]; - -function InteractiveSearch(props) { - const { - searchPayload, - isFetching, - isPopulated, - error, - totalReleasesCount, - items, - selectedFilterKey, - filters, - customFilters, - sortKey, - sortDirection, - type, - longDateFormat, - timeFormat, - onSortPress, - onFilterSelect, - onGrabPress - } = props; - - const errorMessage = getErrorMessage(error); - - return ( - <div> - <div className={styles.filterMenuContainer}> - <FilterMenu - alignMenu={align.RIGHT} - selectedFilterKey={selectedFilterKey} - filters={filters} - customFilters={customFilters} - buttonComponent={PageMenuButton} - filterModalConnectorComponent={InteractiveSearchFilterModalConnector} - filterModalConnectorComponentProps={{ type }} - onFilterSelect={onFilterSelect} - /> - </div> - - { - isFetching ? <LoadingIndicator /> : null - } - - { - !isFetching && error ? - <div> - { - errorMessage ? - <Fragment> - {translate('InteractiveSearchResultsSeriesFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })} - </Fragment> : - translate('EpisodeSearchResultsLoadError') - } - </div> : - null - } - - { - !isFetching && isPopulated && !totalReleasesCount ? - <Alert kind={kinds.INFO}> - {translate('NoResultsFound')} - </Alert> : - null - } - - { - !!totalReleasesCount && isPopulated && !items.length ? - <Alert kind={kinds.WARNING}> - {translate('AllResultsAreHiddenByTheAppliedFilter')} - </Alert> : - null - } - - { - isPopulated && !!items.length ? - <Table - columns={columns} - sortKey={sortKey} - sortDirection={sortDirection} - onSortPress={onSortPress} - > - <TableBody> - { - items.map((item) => { - return ( - <InteractiveSearchRow - key={`${item.indexerId}-${item.guid}`} - {...item} - searchPayload={searchPayload} - longDateFormat={longDateFormat} - timeFormat={timeFormat} - onGrabPress={onGrabPress} - /> - ); - }) - } - </TableBody> - </Table> : - null - } - - { - totalReleasesCount !== items.length && !!items.length ? - <div className={styles.filteredMessage}> - {translate('SomeResultsAreHiddenByTheAppliedFilter')} - </div> : - null - } - </div> - ); -} - -InteractiveSearch.propTypes = { - searchPayload: PropTypes.object.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - totalReleasesCount: PropTypes.number.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, - sortDirection: PropTypes.string, - type: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onSortPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired, - onGrabPress: PropTypes.func.isRequired -}; - -export default InteractiveSearch; diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx new file mode 100644 index 000000000..92fc06dbc --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -0,0 +1,247 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; +import ReleasesAppState from 'App/State/ReleasesAppState'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageMenuButton from 'Components/Menu/PageMenuButton'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { align, icons, kinds, sortDirections } from 'Helpers/Props'; +import { SortDirection } from 'Helpers/Props/sortDirections'; +import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType'; +import { + fetchReleases, + grabRelease, + setEpisodeReleasesFilter, + setReleasesSort, + setSeasonReleasesFilter, +} from 'Store/Actions/releaseActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import InteractiveSearchFilterModal from './InteractiveSearchFilterModal'; +import InteractiveSearchRow from './InteractiveSearchRow'; +import styles from './InteractiveSearch.css'; + +const columns: Column[] = [ + { + name: 'protocol', + label: () => translate('Source'), + isSortable: true, + isVisible: true, + }, + { + name: 'age', + label: () => translate('Age'), + isSortable: true, + isVisible: true, + }, + { + name: 'title', + label: () => translate('Title'), + isSortable: true, + isVisible: true, + }, + { + name: 'indexer', + label: () => translate('Indexer'), + isSortable: true, + isVisible: true, + }, + { + name: 'size', + label: () => translate('Size'), + isSortable: true, + isVisible: true, + }, + { + name: 'peers', + label: () => translate('Peers'), + isSortable: true, + isVisible: true, + }, + { + name: 'languageWeight', + label: () => translate('Languages'), + isSortable: true, + isVisible: true, + }, + { + name: 'qualityWeight', + label: () => translate('Quality'), + isSortable: true, + isVisible: true, + }, + { + name: 'customFormatScore', + label: React.createElement(Icon, { + name: icons.SCORE, + title: () => translate('CustomFormatScore'), + }), + isSortable: true, + isVisible: true, + }, + { + name: 'indexerFlags', + label: React.createElement(Icon, { + name: icons.FLAG, + title: () => translate('IndexerFlags'), + }), + isSortable: true, + isVisible: true, + }, + { + name: 'rejections', + label: React.createElement(Icon, { + name: icons.DANGER, + title: () => translate('Rejections'), + }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true, + }, + { + name: 'releaseWeight', + label: React.createElement(Icon, { name: icons.DOWNLOAD }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true, + }, +]; + +interface InteractiveSearchProps { + type: InteractiveSearchType; + searchPayload: object; +} + +function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) { + const { + isFetching, + isPopulated, + error, + items, + totalItems, + selectedFilterKey, + filters, + customFilters, + sortKey, + sortDirection, + }: ReleasesAppState & ClientSideCollectionAppState = useSelector( + createClientSideCollectionSelector('releases', `releases.${type}`) + ); + + const dispatch = useDispatch(); + + const handleFilterSelect = useCallback( + (selectedFilterKey: string) => { + const action = + type === 'episode' ? setEpisodeReleasesFilter : setSeasonReleasesFilter; + + dispatch(action({ selectedFilterKey })); + }, + [type, dispatch] + ); + + const handleSortPress = useCallback( + (sortKey: string, sortDirection: SortDirection) => { + dispatch(setReleasesSort({ sortKey, sortDirection })); + }, + [dispatch] + ); + + const handleGrabPress = useCallback( + (payload: object) => { + dispatch(grabRelease(payload)); + }, + [dispatch] + ); + + useEffect(() => { + // If search results are not yet isPopulated fetch them, + // otherwise re-show the existing props. + + if (!isPopulated) { + dispatch(fetchReleases(searchPayload)); + } + }, [isPopulated, searchPayload, dispatch]); + + const errorMessage = getErrorMessage(error); + + return ( + <div> + <div className={styles.filterMenuContainer}> + <FilterMenu + alignMenu={align.RIGHT} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + buttonComponent={PageMenuButton} + filterModalConnectorComponent={InteractiveSearchFilterModal} + filterModalConnectorComponentProps={{ type }} + onFilterSelect={handleFilterSelect} + /> + </div> + + {isFetching ? <LoadingIndicator /> : null} + + {!isFetching && error ? ( + <div> + {errorMessage ? ( + <> + {translate('InteractiveSearchResultsSeriesFailedErrorMessage', { + message: + errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1), + })} + </> + ) : ( + translate('EpisodeSearchResultsLoadError') + )} + </div> + ) : null} + + {!isFetching && isPopulated && !totalItems ? ( + <Alert kind={kinds.INFO}>{translate('NoResultsFound')}</Alert> + ) : null} + + {!!totalItems && isPopulated && !items.length ? ( + <Alert kind={kinds.WARNING}> + {translate('AllResultsAreHiddenByTheAppliedFilter')} + </Alert> + ) : null} + + {isPopulated && !!items.length ? ( + <Table + columns={columns} + sortKey={sortKey} + sortDirection={sortDirection} + onSortPress={handleSortPress} + > + <TableBody> + {items.map((item) => { + return ( + <InteractiveSearchRow + key={`${item.indexerId}-${item.guid}`} + {...item} + searchPayload={searchPayload} + onGrabPress={handleGrabPress} + /> + ); + })} + </TableBody> + </Table> + ) : null} + + {totalItems !== items.length && !!items.length ? ( + <div className={styles.filteredMessage}> + {translate('SomeResultsAreHiddenByTheAppliedFilter')} + </div> + ) : null} + </div> + ); +} + +export default InteractiveSearch; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js deleted file mode 100644 index 10cad7224..000000000 --- a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as releaseActions from 'Store/Actions/releaseActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import InteractiveSearch from './InteractiveSearch'; - -function createMapStateToProps(appState, { type }) { - return createSelector( - (state) => state.releases.items.length, - createClientSideCollectionSelector('releases', `releases.${type}`), - createUISettingsSelector(), - (totalReleasesCount, releases, uiSettings) => { - return { - totalReleasesCount, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat, - ...releases - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchFetchReleases(payload) { - dispatch(releaseActions.fetchReleases(payload)); - }, - - onSortPress(sortKey, sortDirection) { - dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection })); - }, - - onFilterSelect(selectedFilterKey) { - const action = props.type === 'episode' ? - releaseActions.setEpisodeReleasesFilter : - releaseActions.setSeasonReleasesFilter; - - dispatch(action({ selectedFilterKey })); - }, - - onGrabPress(payload) { - dispatch(releaseActions.grabRelease(payload)); - } - }; -} - -class InteractiveSearchConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - searchPayload, - isPopulated, - dispatchFetchReleases - } = this.props; - - // If search results are not yet isPopulated fetch them, - // otherwise re-show the existing props. - - if (!isPopulated) { - dispatchFetchReleases(searchPayload); - } - } - - // - // Render - - render() { - const { - dispatchFetchReleases, - ...otherProps - } = this.props; - - return ( - - <InteractiveSearch - {...otherProps} - /> - ); - } -} - -InteractiveSearchConnector.propTypes = { - type: PropTypes.string.isRequired, - searchPayload: PropTypes.object.isRequired, - isPopulated: PropTypes.bool, - dispatchFetchReleases: PropTypes.func -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx b/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx new file mode 100644 index 000000000..d24615554 --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterModal from 'Components/Filter/FilterModal'; +import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType'; +import { + setEpisodeReleasesFilter, + setSeasonReleasesFilter, +} from 'Store/Actions/releaseActions'; + +function createReleasesSelector() { + return createSelector( + (state: AppState) => state.releases.items, + (releases) => { + return releases; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.releases.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface InteractiveSearchFilterModalProps { + isOpen: boolean; + type: InteractiveSearchType; +} + +export default function InteractiveSearchFilterModal({ + type, + ...otherProps +}: InteractiveSearchFilterModalProps) { + const sectionItems = useSelector(createReleasesSelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'releases'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + const action = + type === 'episode' ? setEpisodeReleasesFilter : setSeasonReleasesFilter; + + dispatch(action(payload)); + }, + [type, dispatch] + ); + + return ( + <FilterModal + // TODO: Don't spread all the props + {...otherProps} + sectionItems={sectionItems} + filterBuilderProps={filterBuilderProps} + customFilterType={customFilterType} + dispatchSetFilter={dispatchSetFilter} + /> + ); +} diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js deleted file mode 100644 index c0ac11a46..000000000 --- a/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js +++ /dev/null @@ -1,32 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import FilterModal from 'Components/Filter/FilterModal'; -import { setEpisodeReleasesFilter, setSeasonReleasesFilter } from 'Store/Actions/releaseActions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.releases.items, - (state) => state.releases.filterBuilderProps, - (sectionItems, filterBuilderProps) => { - return { - sectionItems, - filterBuilderProps, - customFilterType: 'releases' - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchSetFilter(payload) { - const action = props.type === 'episode' ? - setEpisodeReleasesFilter: - setSeasonReleasesFilter; - - dispatch(action(payload)); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index d860b7fb9..0baf66f57 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; @@ -8,15 +9,13 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; -import type DownloadProtocol from 'DownloadClient/DownloadProtocol'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import IndexerFlags from 'Episode/IndexerFlags'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import Language from 'Language/Language'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import Release from 'typings/Release'; import formatDateTime from 'Utilities/Date/formatDateTime'; import formatAge from 'Utilities/Number/formatAge'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -24,7 +23,6 @@ import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; import OverrideMatchModal from './OverrideMatch/OverrideMatchModal'; import Peers from './Peers'; -import ReleaseEpisode from './ReleaseEpisode'; import ReleaseSceneIndicator from './ReleaseSceneIndicator'; import styles from './InteractiveSearchRow.css'; @@ -72,43 +70,7 @@ function getDownloadTooltip( return translate('AddToDownloadQueue'); } -interface InteractiveSearchRowProps { - guid: string; - protocol: DownloadProtocol; - age: number; - ageHours: number; - ageMinutes: number; - publishDate: string; - title: string; - infoUrl: string; - indexerId: number; - indexer: string; - size: number; - seeders?: number; - leechers?: number; - quality: QualityModel; - languages: Language[]; - customFormats: CustomFormat[]; - customFormatScore: number; - sceneMapping?: object; - seasonNumber?: number; - episodeNumbers?: number[]; - absoluteEpisodeNumbers?: number[]; - mappedSeriesId?: number; - mappedSeasonNumber?: number; - mappedEpisodeNumbers?: number[]; - mappedAbsoluteEpisodeNumbers?: number[]; - mappedEpisodeInfo: ReleaseEpisode[]; - indexerFlags: number; - rejections: string[]; - episodeRequested: boolean; - downloadAllowed: boolean; - isDaily: boolean; - isGrabbing: boolean; - isGrabbed: boolean; - grabError?: string; - longDateFormat: string; - timeFormat: string; +interface InteractiveSearchRowProps extends Release { searchPayload: object; onGrabPress(...args: unknown[]): void; } @@ -148,13 +110,15 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { isDaily, isGrabbing = false, isGrabbed = false, - longDateFormat, - timeFormat, grabError, searchPayload, onGrabPress, } = props; + const { longDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false); const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchType.ts b/frontend/src/InteractiveSearch/InteractiveSearchType.ts new file mode 100644 index 000000000..2ae6733a6 --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchType.ts @@ -0,0 +1,3 @@ +type InteractiveSearchType = 'episode' | 'season'; + +export default InteractiveSearchType; diff --git a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx index 1a2e6514b..e15b5c66d 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx @@ -2,9 +2,9 @@ import React from 'react'; import Modal from 'Components/Modal/Modal'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { sizes } from 'Helpers/Props'; -import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; +import { ReleaseEpisode } from 'typings/Release'; import OverrideMatchModalContent from './OverrideMatchModalContent'; interface OverrideMatchModalProps { diff --git a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx index 9d6ab9253..8e41a93de 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx @@ -18,7 +18,6 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal' import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; -import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import Series from 'Series/Series'; @@ -26,6 +25,7 @@ import { grabRelease } from 'Store/Actions/releaseActions'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector'; +import { ReleaseEpisode } from 'typings/Release'; import translate from 'Utilities/String/translate'; import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal'; import OverrideMatchData from './OverrideMatchData'; diff --git a/frontend/src/InteractiveSearch/Peers.js b/frontend/src/InteractiveSearch/Peers.tsx similarity index 63% rename from frontend/src/InteractiveSearch/Peers.js rename to frontend/src/InteractiveSearch/Peers.tsx index a55e75c09..b23391210 100644 --- a/frontend/src/InteractiveSearch/Peers.js +++ b/frontend/src/InteractiveSearch/Peers.tsx @@ -1,9 +1,8 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Label from 'Components/Label'; import { kinds } from 'Helpers/Props'; -function getKind(seeders) { +function getKind(seeders: number = 0) { if (seeders > 50) { return kinds.PRIMARY; } @@ -19,7 +18,7 @@ function getKind(seeders) { return kinds.DANGER; } -function getPeersTooltipPart(peers, peersUnit) { +function getPeersTooltipPart(peersUnit: string, peers?: number) { if (peers == null) { return `Unknown ${peersUnit}s`; } @@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) { return `${peers} ${peersUnit}s`; } -function Peers(props) { - const { - seeders, - leechers - } = props; +interface PeersProps { + seeders?: number; + leechers?: number; +} + +function Peers(props: PeersProps) { + const { seeders, leechers } = props; const kind = getKind(seeders); return ( <Label kind={kind} - title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`} + title={`${getPeersTooltipPart('seeder', seeders)}, ${getPeersTooltipPart( + 'leecher', + leechers + )}`} > {seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers} </Label> ); } -Peers.propTypes = { - seeders: PropTypes.number, - leechers: PropTypes.number -}; - export default Peers; diff --git a/frontend/src/InteractiveSearch/ReleaseEpisode.ts b/frontend/src/InteractiveSearch/ReleaseEpisode.ts deleted file mode 100644 index 91ab5b7b5..000000000 --- a/frontend/src/InteractiveSearch/ReleaseEpisode.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface ReleaseEpisode { - id: number; - episodeFileId: number; - seasonNumber: number; - episodeNumber: number; - absoluteEpisodeNumber?: number; - title: string; -} - -export default ReleaseEpisode; diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx b/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx index 362972c89..f3644e13b 100644 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModalContent.tsx @@ -5,7 +5,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { scrollDirections } from 'Helpers/Props'; -import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; +import InteractiveSearch from 'InteractiveSearch/InteractiveSearch'; import formatSeason from 'Season/formatSeason'; import translate from 'Utilities/String/translate'; @@ -31,7 +31,7 @@ function SeasonInteractiveSearchModalContent( </ModalHeader> <ModalBody scrollDirection={scrollDirections.BOTH}> - <InteractiveSearchConnector + <InteractiveSearch type="season" searchPayload={{ seriesId, diff --git a/frontend/src/typings/Release.ts b/frontend/src/typings/Release.ts new file mode 100644 index 000000000..4179a0a71 --- /dev/null +++ b/frontend/src/typings/Release.ts @@ -0,0 +1,53 @@ +import type DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; + +export interface ReleaseEpisode { + id: number; + episodeFileId: number; + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + title: string; +} + +interface Release { + guid: string; + protocol: DownloadProtocol; + age: number; + ageHours: number; + ageMinutes: number; + publishDate: string; + title: string; + infoUrl: string; + indexerId: number; + indexer: string; + size: number; + seeders?: number; + leechers?: number; + quality: QualityModel; + languages: Language[]; + customFormats: CustomFormat[]; + customFormatScore: number; + sceneMapping?: object; + seasonNumber?: number; + episodeNumbers?: number[]; + absoluteEpisodeNumbers?: number[]; + mappedSeriesId?: number; + mappedSeasonNumber?: number; + mappedEpisodeNumbers?: number[]; + mappedAbsoluteEpisodeNumbers?: number[]; + mappedEpisodeInfo: ReleaseEpisode[]; + indexerFlags: number; + rejections: string[]; + episodeRequested: boolean; + downloadAllowed: boolean; + isDaily: boolean; + + isGrabbing?: boolean; + isGrabbed?: boolean; + grabError?: string; +} + +export default Release; From 278c7891a3add639b4ff5bc1f4f5e8912dabc897 Mon Sep 17 00:00:00 2001 From: amdavie <amdavie@gmail.com> Date: Mon, 2 Sep 2024 14:25:53 -0600 Subject: [PATCH 511/762] New: Scene and Nuked IndexerFlags for Newznab indexers Closes #6932 --- .../Indexers/Newznab/newznab_indexerflags.xml | 128 ++++++++++++++++++ .../NewznabTests/NewznabFixture.cs | 25 ++++ .../Indexers/Newznab/NewznabRssParser.cs | 18 +++ .../Parser/Model/IndexerFlags.cs | 48 +++++++ src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 12 -- 5 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Files/Indexers/Newznab/newznab_indexerflags.xml create mode 100644 src/NzbDrone.Core/Parser/Model/IndexerFlags.cs diff --git a/src/NzbDrone.Core.Test/Files/Indexers/Newznab/newznab_indexerflags.xml b/src/NzbDrone.Core.Test/Files/Indexers/Newznab/newznab_indexerflags.xml new file mode 100644 index 000000000..0d70b3707 --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/Indexers/Newznab/newznab_indexerflags.xml @@ -0,0 +1,128 @@ +<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:newznab="http://www.newznab.com/DTD/2010/feeds/attributes/"> + <channel> + <title>somenewznabindexer.com + somenewznabindexer.com Feed + https://somenewznabindexer.com/ + en-gb + contact@somenewznabindexer.com + + + + title + no custom attributes + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + title + prematch=1 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + haspretime=1 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + prematch=0 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + haspretime=0 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + nuked=1 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + nuked=0 attribute + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + title + prematch=1 and nuked=1 attributes + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + + title + haspretime=0 and nuked=0 attributes + link + comments + Sat, 31 Aug 2024 12:28:40 +0300 + category + description + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index d400f6f5f..3530c137f 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using DryIoc.ImTools; using FluentAssertions; using Moq; using NUnit.Framework; @@ -154,5 +155,29 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests releases[1].Languages.Should().BeEquivalentTo(new[] { Language.English, Language.Spanish }); releases[2].Languages.Should().BeEquivalentTo(new[] { Language.French }); } + + [TestCase("no custom attributes")] + [TestCase("prematch=1 attribute", IndexerFlags.Scene)] + [TestCase("haspretime=1 attribute", IndexerFlags.Scene)] + [TestCase("prematch=0 attribute")] + [TestCase("haspretime=0 attribute")] + [TestCase("nuked=1 attribute", IndexerFlags.Nuked)] + [TestCase("nuked=0 attribute")] + [TestCase("prematch=1 and nuked=1 attributes", IndexerFlags.Scene, IndexerFlags.Nuked)] + [TestCase("haspretime=0 and nuked=0 attributes")] + public async Task should_parse_indexer_flags(string releaseGuid, params IndexerFlags[] indexerFlags) + { + var feed = ReadAllText(@"Files/Indexers/Newznab/newznab_indexerflags.xml"); + + Mocker.GetMock() + .Setup(o => o.ExecuteAsync(It.Is(v => v.Method == HttpMethod.Get))) + .Returns(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), feed))); + + var releases = await Subject.FetchRecent(); + + var release = releases.Should().ContainSingle(r => r.Guid == releaseGuid).Subject; + + indexerFlags.ForEach(f => release.IndexerFlags.Should().HaveFlag(f)); + } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index 408d22b36..f43f0b5d4 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -91,6 +91,7 @@ namespace NzbDrone.Core.Indexers.Newznab releaseInfo.TvdbId = GetTvdbId(item); releaseInfo.TvRageId = GetTvRageId(item); releaseInfo.ImdbId = GetImdbId(item); + releaseInfo.IndexerFlags = GetFlags(item); return releaseInfo; } @@ -195,6 +196,23 @@ namespace NzbDrone.Core.Indexers.Newznab return null; } + protected IndexerFlags GetFlags(XElement item) + { + IndexerFlags flags = 0; + + if (TryGetNewznabAttribute(item, "prematch") == "1" || TryGetNewznabAttribute(item, "haspretime") == "1") + { + flags |= IndexerFlags.Scene; + } + + if (TryGetNewznabAttribute(item, "nuked") == "1") + { + flags |= IndexerFlags.Nuked; + } + + return flags; + } + protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "") { var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); diff --git a/src/NzbDrone.Core/Parser/Model/IndexerFlags.cs b/src/NzbDrone.Core/Parser/Model/IndexerFlags.cs new file mode 100644 index 000000000..dbc45df5c --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/IndexerFlags.cs @@ -0,0 +1,48 @@ +using System; + +namespace NzbDrone.Core.Parser.Model +{ + [Flags] + public enum IndexerFlags + { + /// + /// Torrent download amount does not count + /// + Freeleech = 1, + + /// + /// Torrent download amount only counts 50% + /// + Halfleech = 2, + + /// + /// Torrent upload amount is doubled + /// + DoubleUpload = 4, + + /// + /// Uploader is an internal release group + /// + Internal = 8, + + /// + /// The release comes from a scene group + /// + Scene = 16, + + /// + /// Torrent download amount only counts 75% + /// + Freeleech75 = 32, + + /// + /// Torrent download amount only counts 25% + /// + Freeleech25 = 64, + + /// + /// The release is nuked + /// + Nuked = 128 + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 1266ac10c..19c3ad335 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -111,16 +111,4 @@ namespace NzbDrone.Core.Parser.Model } } } - - [Flags] - public enum IndexerFlags - { - Freeleech = 1, // General - Halfleech = 2, // General, only 1/2 of download counted - DoubleUpload = 4, // General - Internal = 8, // General, uploader is an internal release group - Scene = 16, // General, the torrent comes from a "scene" group - Freeleech75 = 32, // Signifies a torrent counts towards 75 percent of your download quota. - Freeleech25 = 64, // Signifies a torrent counts towards 25 percent of your download quota. - } } From 1584311914eed697fdd0f143951f4adfe3403351 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 13:26:35 -0700 Subject: [PATCH 512/762] New: Except language option for Language Custom Formats Closes #7120 --- .../MultiLanguageFixture.cs | 30 +++++++++++++++++++ .../SingleLanguageFixture.cs | 10 +++++++ .../Specifications/LanguageSpecification.cs | 13 ++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 4 files changed, 55 insertions(+) diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs index afc775398..74fb5b30b 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/MultiLanguageFixture.cs @@ -42,6 +42,26 @@ namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification Subject.IsSatisfiedBy(_input).Should().BeTrue(); } + [Test] + public void should_match_language_if_other_languages_are_present() + { + Subject.Value = Language.French.Id; + Subject.ExceptLanguage = true; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + + [Test] + public void should_match_language_if_not_original_language_is_present() + { + Subject.Value = Language.Original.Id; + Subject.ExceptLanguage = true; + Subject.Negate = false; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } + [Test] public void should_not_match_different_language() { @@ -68,5 +88,15 @@ namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification Subject.IsSatisfiedBy(_input).Should().BeTrue(); } + + [Test] + public void should_not_match_negate_language_if_other_languages_are_present() + { + Subject.Value = Language.Spanish.Id; + Subject.ExceptLanguage = true; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs index faee8afe7..af5fc64ad 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/Specifications/LanguageSpecification/SingleLanguageFixture.cs @@ -67,5 +67,15 @@ namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification Subject.IsSatisfiedBy(_input).Should().BeTrue(); } + + [Test] + public void should_match_negated_except_language_if_language_is_only_present_language() + { + Subject.Value = Language.French.Id; + Subject.ExceptLanguage = true; + Subject.Negate = true; + + Subject.IsSatisfiedBy(_input).Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs index d841b7053..9632af893 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs @@ -30,6 +30,9 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] public int Value { get; set; } + [FieldDefinition(1, Label = "CustomFormatsSpecificationExceptLanguage", HelpText = "CustomFormatsSpecificationExceptLanguageHelpText", Type = FieldType.Checkbox)] + public bool ExceptLanguage { get; set; } + public override bool IsSatisfiedBy(CustomFormatInput input) { if (Negate) @@ -46,6 +49,11 @@ namespace NzbDrone.Core.CustomFormats ? input.Series.OriginalLanguage : (Language)Value; + if (ExceptLanguage) + { + return input.Languages?.Any(l => l != comparedLanguage) ?? false; + } + return input.Languages?.Contains(comparedLanguage) ?? false; } @@ -55,6 +63,11 @@ namespace NzbDrone.Core.CustomFormats ? input.Series.OriginalLanguage : (Language)Value; + if (ExceptLanguage) + { + return !input.Languages?.Any(l => l != comparedLanguage) ?? false; + } + return !input.Languages?.Contains(comparedLanguage) ?? false; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 3e77ffdc1..aba736a67 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -290,6 +290,8 @@ "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.", "CustomFormatsSpecificationFlag": "Flag", "CustomFormatsSpecificationLanguage": "Language", + "CustomFormatsSpecificationExceptLanguage": "Except Language", + "CustomFormatsSpecificationExceptLanguageHelpText": "Matches if any language other than the selected language is present", "CustomFormatsSpecificationMaximumSize": "Maximum Size", "CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size", "CustomFormatsSpecificationMinimumSize": "Minimum Size", From 7f0696c57497ada845a40f7b5f80ace31ec1527f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 31 Aug 2024 15:00:35 -0700 Subject: [PATCH 513/762] Fixed: Failing to import any file for series if one has bad encoding Closes #7157 --- .../EpisodeImport/ImportDecisionMaker.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index d192c72f2..590b0f5d7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -119,17 +119,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { ImportDecision decision = null; - var fileEpisodeInfo = Parser.Parser.ParsePath(localEpisode.Path); - - localEpisode.FileEpisodeInfo = fileEpisodeInfo; - localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); - localEpisode.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? - localEpisode.FolderEpisodeInfo?.ReleaseType ?? - localEpisode.FileEpisodeInfo?.ReleaseType ?? - ReleaseType.Unknown; - try { + var fileEpisodeInfo = Parser.Parser.ParsePath(localEpisode.Path); + + localEpisode.FileEpisodeInfo = fileEpisodeInfo; + localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); + localEpisode.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo?.ReleaseType ?? + ReleaseType.Unknown; + _aggregationService.Augment(localEpisode, downloadClientItem); if (localEpisode.Episodes.Empty()) From 66cead6b48df86a8c0f785c8c1666f0e5701ec94 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 1 Sep 2024 04:25:21 +0300 Subject: [PATCH 514/762] Cleanup History Details and a typo --- frontend/src/Activity/History/Details/HistoryDetails.tsx | 2 -- .../src/Activity/History/Details/HistoryDetailsModal.tsx | 6 ------ frontend/src/Activity/History/HistoryRow.tsx | 9 +-------- frontend/src/Components/Table/Cells/TableRowCell.tsx | 4 ++-- 4 files changed, 3 insertions(+), 18 deletions(-) diff --git a/frontend/src/Activity/History/Details/HistoryDetails.tsx b/frontend/src/Activity/History/Details/HistoryDetails.tsx index d4c8f9f4f..b5116b3d9 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.tsx +++ b/frontend/src/Activity/History/Details/HistoryDetails.tsx @@ -27,8 +27,6 @@ interface HistoryDetailsProps { sourceTitle: string; data: HistoryData; downloadId?: string; - shortDateFormat: string; - timeFormat: string; } function HistoryDetails(props: HistoryDetailsProps) { diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx index a33e0b1ba..8134a9736 100644 --- a/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx @@ -38,8 +38,6 @@ interface HistoryDetailsModalProps { data: HistoryData; downloadId?: string; isMarkingAsFailed: boolean; - shortDateFormat: string; - timeFormat: string; onMarkAsFailedPress: () => void; onModalClose: () => void; } @@ -52,8 +50,6 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) { data, downloadId, isMarkingAsFailed = false, - shortDateFormat, - timeFormat, onMarkAsFailedPress, onModalClose, } = props; @@ -69,8 +65,6 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) { sourceTitle={sourceTitle} data={data} downloadId={downloadId} - shortDateFormat={shortDateFormat} - timeFormat={timeFormat} /> diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx index ce4b00647..42af2833b 100644 --- a/frontend/src/Activity/History/HistoryRow.tsx +++ b/frontend/src/Activity/History/HistoryRow.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -20,7 +20,6 @@ import { QualityModel } from 'Quality/Quality'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import useSeries from 'Series/useSeries'; import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import CustomFormat from 'typings/CustomFormat'; import { HistoryData, HistoryEventType } from 'typings/History'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; @@ -72,10 +71,6 @@ function HistoryRow(props: HistoryRowProps) { const series = useSeries(seriesId); const episode = useEpisode(episodeId, 'episodes'); - const { shortDateFormat, timeFormat } = useSelector( - createUISettingsSelector() - ); - const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); const handleDetailsPress = useCallback(() => { @@ -260,8 +255,6 @@ function HistoryRow(props: HistoryRowProps) { data={data} downloadId={downloadId} isMarkingAsFailed={isMarkingAsFailed} - shortDateFormat={shortDateFormat} - timeFormat={timeFormat} onMarkAsFailedPress={handleMarkAsFailedPress} onModalClose={handleDetailsModalClose} /> diff --git a/frontend/src/Components/Table/Cells/TableRowCell.tsx b/frontend/src/Components/Table/Cells/TableRowCell.tsx index 3b4b97c14..00b6acb1d 100644 --- a/frontend/src/Components/Table/Cells/TableRowCell.tsx +++ b/frontend/src/Components/Table/Cells/TableRowCell.tsx @@ -1,11 +1,11 @@ import React, { ComponentPropsWithoutRef } from 'react'; import styles from './TableRowCell.css'; -export interface TableRowCellprops extends ComponentPropsWithoutRef<'td'> {} +export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {} export default function TableRowCell({ className = styles.cell, ...tdProps -}: TableRowCellprops) { +}: TableRowCellProps) { return ; } From 6f51e72d0073444b441bee5508322cc9e52e98e4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 13:27:21 -0700 Subject: [PATCH 515/762] Fixed: Respect Quality cutoff if Custom Format cutoff isn't met Closes #7132 --- .../CutoffSpecificationFixture.cs | 269 ------------------ .../RssSync/DelaySpecificationFixture.cs | 3 +- .../UpgradeDiskSpecificationFixture.cs | 210 ++++++++++++++ .../UpgradeSpecificationFixture.cs | 74 +++-- .../Specifications/CutoffSpecification.cs | 58 ---- .../Specifications/QueueSpecification.cs | 27 +- .../RssSync/HistorySpecification.cs | 30 +- .../Specifications/UpgradableSpecification.cs | 51 ++-- .../UpgradeDiskSpecification.cs | 46 ++- .../DecisionEngine/UpgradeableRejectReason.cs | 12 + 10 files changed, 388 insertions(+), 392 deletions(-) delete mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs delete mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs deleted file mode 100644 index 784b285ed..000000000 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Languages; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles.Qualities; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.CustomFormats; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.DecisionEngineTests -{ - [TestFixture] - public class CutoffSpecificationFixture : CoreTest - { - private CustomFormat _customFormat; - private RemoteEpisode _remoteMovie; - - [SetUp] - public void Setup() - { - Mocker.SetConstant(Mocker.Resolve()); - - _remoteMovie = new RemoteEpisode() - { - Series = Builder.CreateNew().Build(), - Episodes = new List { Builder.CreateNew().Build() }, - ParsedEpisodeInfo = Builder.CreateNew().With(x => x.Quality = null).Build() - }; - - GivenOldCustomFormats(new List()); - } - - private void GivenProfile(QualityProfile profile) - { - CustomFormatsTestHelpers.GivenCustomFormats(); - profile.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(); - profile.MinFormatScore = 0; - _remoteMovie.Series.QualityProfile = profile; - - Console.WriteLine(profile.ToJson()); - } - - private void GivenFileQuality(QualityModel quality, Language language) - { - _remoteMovie.Episodes.First().EpisodeFile = Builder.CreateNew().With(x => x.Quality = quality).With(x => x.Languages = new List { language }).Build(); - } - - private void GivenNewQuality(QualityModel quality) - { - _remoteMovie.ParsedEpisodeInfo.Quality = quality; - } - - private void GivenOldCustomFormats(List formats) - { - Mocker.GetMock() - .Setup(x => x.ParseCustomFormat(It.IsAny())) - .Returns(formats); - } - - private void GivenNewCustomFormats(List formats) - { - _remoteMovie.CustomFormats = formats; - } - - private void GivenCustomFormatHigher() - { - _customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 }; - - CustomFormatsTestHelpers.GivenCustomFormats(_customFormat); - } - - [Test] - public void should_return_true_if_current_episode_is_less_than_cutoff() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.Bluray1080p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.DVD, new Revision(version: 2)), Language.English); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_false_if_current_episode_is_equal_to_cutoff() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.English); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_false_if_current_episode_is_greater_than_cutoff() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), Language.English); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_true_when_new_episode_is_proper_but_existing_is_not() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 1)), Language.English); - GivenNewQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_false_if_cutoff_is_met_and_quality_is_higher() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.English); - GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_false_if_quality_cutoff_is_met_and_quality_is_higher_but_language_is_met() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.Spanish); - GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_higher() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.French); - GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_true_if_cutoff_is_not_met_and_new_quality_is_higher_and_language_is_higher() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.SDTV, new Revision(version: 2)), Language.French); - GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_true_if_cutoff_is_not_met_and_language_is_higher() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.SDTV, new Revision(version: 2)), Language.French); - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_false_if_custom_formats_is_met_and_quality_and_format_higher() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV720p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - MinFormatScore = 0, - FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.HDTV720p), Language.English); - GivenNewQuality(new QualityModel(Quality.Bluray1080p)); - - GivenCustomFormatHigher(); - - GivenOldCustomFormats(new List()); - GivenNewCustomFormats(new List { _customFormat }); - - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - - [Test] - public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.HDTV1080p.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = true - }); - - GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.English); - GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2))); - - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_cutoff_is_set_to_highest_quality() - { - GivenProfile(new QualityProfile - { - Cutoff = Quality.RAWHD.Id, - Items = Qualities.QualityFixture.GetDefaultQualities(), - UpgradeAllowed = false - }); - - GivenFileQuality(new QualityModel(Quality.WEBDL1080p), Language.English); - GivenNewQuality(new QualityModel(Quality.Bluray1080p)); - - Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 4a8ba8a6d..493785e70 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -6,6 +6,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.Download.Pending; @@ -86,7 +87,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { Mocker.GetMock() .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny>())) - .Returns(true); + .Returns(UpgradeableRejectReason.None); } [Test] diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index a58f873ad..66a38f076 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -4,10 +4,12 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Serializer; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; @@ -74,6 +76,42 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Returns(new List()); } + private void GivenProfile(QualityProfile profile) + { + CustomFormatsTestHelpers.GivenCustomFormats(); + profile.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(); + profile.MinFormatScore = 0; + _parseResultMulti.Series.QualityProfile = profile; + _parseResultSingle.Series.QualityProfile = profile; + + Console.WriteLine(profile.ToJson()); + } + + private void GivenFileQuality(QualityModel quality) + { + _firstFile.Quality = quality; + _secondFile.Quality = quality; + } + + private void GivenNewQuality(QualityModel quality) + { + _parseResultMulti.ParsedEpisodeInfo.Quality = quality; + _parseResultSingle.ParsedEpisodeInfo.Quality = quality; + } + + private void GivenOldCustomFormats(List formats) + { + Mocker.GetMock() + .Setup(x => x.ParseCustomFormat(It.IsAny())) + .Returns(formats); + } + + private void GivenNewCustomFormats(List formats) + { + _parseResultMulti.CustomFormats = formats; + _parseResultSingle.CustomFormats = formats; + } + private void WithFirstFileUpgradable() { _firstFile.Quality = new QualityModel(Quality.SDTV); @@ -155,5 +193,177 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p); _upgradeDisk.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } + + [Test] + public void should_return_false_if_current_episode_is_equal_to_cutoff() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_false_if_current_episode_is_greater_than_cutoff() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_when_new_episode_is_proper_but_existing_is_not() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 1))); + GivenNewQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); + GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_false_if_quality_cutoff_is_met_and_quality_is_higher_but_language_is_met() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); + GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_higher() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p, new Revision(version: 2))); + GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_new_quality_is_higher_and_language_is_higher() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.SDTV, new Revision(version: 2))); + GivenNewQuality(new QualityModel(Quality.Bluray1080p, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_language_is_higher() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.SDTV, new Revision(version: 2))); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_custom_formats_is_met_and_quality_and_format_higher() + { + var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 }; + + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + MinFormatScore = 0, + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.HDTV720p)); + GivenNewQuality(new QualityModel(Quality.Bluray1080p)); + + GivenOldCustomFormats(new List()); + GivenNewCustomFormats(new List { customFormat }); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.HDTV1080p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true + }); + + GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1))); + GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2))); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_cutoff_is_set_to_highest_quality() + { + GivenProfile(new QualityProfile + { + Cutoff = Quality.RAWHD.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = false + }); + + GivenFileQuality(new QualityModel(Quality.WEBDL1080p)); + GivenNewQuality(new QualityModel(Quality.Bluray1080p)); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index 4d8b4f955..9bf70fa13 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -3,8 +3,8 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -17,23 +17,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { public static object[] IsUpgradeTestCases = { - new object[] { Quality.SDTV, 1, Quality.SDTV, 2, Quality.SDTV, true }, - new object[] { Quality.WEBDL720p, 1, Quality.WEBDL720p, 2, Quality.WEBDL720p, true }, - new object[] { Quality.SDTV, 1, Quality.SDTV, 1, Quality.SDTV, false }, - new object[] { Quality.WEBDL720p, 1, Quality.HDTV720p, 2, Quality.Bluray720p, false }, - new object[] { Quality.WEBDL720p, 1, Quality.HDTV720p, 2, Quality.WEBDL720p, false }, - new object[] { Quality.WEBDL720p, 1, Quality.WEBDL720p, 1, Quality.WEBDL720p, false }, - new object[] { Quality.WEBDL1080p, 1, Quality.WEBDL1080p, 1, Quality.WEBDL1080p, false } - }; - - public static object[] IsUpgradeTestCasesLanguages = - { - new object[] { Quality.SDTV, 1, Language.English, Quality.SDTV, 2, Language.English, Quality.SDTV, Language.Spanish, true }, - new object[] { Quality.SDTV, 1, Language.English, Quality.SDTV, 1, Language.Spanish, Quality.SDTV, Language.Spanish, true }, - new object[] { Quality.WEBDL720p, 1, Language.French, Quality.WEBDL720p, 2, Language.English, Quality.WEBDL720p, Language.Spanish, true }, - new object[] { Quality.SDTV, 1, Language.English, Quality.SDTV, 1, Language.English, Quality.SDTV, Language.English, false }, - new object[] { Quality.WEBDL720p, 1, Language.English, Quality.HDTV720p, 2, Language.Spanish, Quality.Bluray720p, Language.Spanish, false }, - new object[] { Quality.WEBDL720p, 1, Language.Spanish, Quality.HDTV720p, 2, Language.French, Quality.WEBDL720p, Language.Spanish, false } + new object[] { Quality.SDTV, 1, Quality.SDTV, 2, Quality.SDTV, UpgradeableRejectReason.None }, + new object[] { Quality.WEBDL720p, 1, Quality.WEBDL720p, 2, Quality.WEBDL720p, UpgradeableRejectReason.None }, + new object[] { Quality.SDTV, 1, Quality.SDTV, 1, Quality.SDTV, UpgradeableRejectReason.CustomFormatScore }, + new object[] { Quality.WEBDL720p, 1, Quality.HDTV720p, 2, Quality.Bluray720p, UpgradeableRejectReason.BetterQuality }, + new object[] { Quality.WEBDL720p, 1, Quality.HDTV720p, 2, Quality.WEBDL720p, UpgradeableRejectReason.BetterQuality }, + new object[] { Quality.WEBDL720p, 1, Quality.WEBDL720p, 1, Quality.WEBDL720p, UpgradeableRejectReason.CustomFormatScore }, + new object[] { Quality.WEBDL1080p, 1, Quality.WEBDL1080p, 1, Quality.WEBDL1080p, UpgradeableRejectReason.CustomFormatScore } }; private void GivenAutoDownloadPropers(ProperDownloadTypes type) @@ -45,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] [TestCaseSource(nameof(IsUpgradeTestCases))] - public void IsUpgradeTest(Quality current, int currentVersion, Quality newQuality, int newVersion, Quality cutoff, bool expected) + public void IsUpgradeTest(Quality current, int currentVersion, Quality newQuality, int newVersion, Quality cutoff, UpgradeableRejectReason expected) { GivenAutoDownloadPropers(ProperDownloadTypes.PreferAndUpgrade); @@ -80,7 +70,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List(), new QualityModel(Quality.DVD, new Revision(version: 2)), new List()) - .Should().BeTrue(); + .Should().Be(UpgradeableRejectReason.None); } [Test] @@ -99,7 +89,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List(), new QualityModel(Quality.DVD, new Revision(version: 2)), new List()) - .Should().BeFalse(); + .Should().Be(UpgradeableRejectReason.CustomFormatScore); } [Test] @@ -107,7 +97,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var profile = new QualityProfile { - Items = Qualities.QualityFixture.GetDefaultQualities(), + Items = Qualities.QualityFixture.GetDefaultQualities() }; Subject.IsUpgradable( @@ -116,7 +106,45 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List(), new QualityModel(Quality.HDTV720p, new Revision(version: 1)), new List()) - .Should().BeFalse(); + .Should().Be(UpgradeableRejectReason.CustomFormatScore); + } + + [Test] + public void should_return_true_if_release_has_higher_quality_and_cutoff_is_not_already_met() + { + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + Cutoff = Quality.HDTV1080p.Id + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.HDTV720p, new Revision(version: 1)), + new List(), + new QualityModel(Quality.HDTV1080p, new Revision(version: 1)), + new List()) + .Should().Be(UpgradeableRejectReason.None); + } + + [Test] + public void should_return_false_if_release_has_higher_quality_and_cutoff_is_already_met() + { + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + Cutoff = Quality.HDTV720p.Id + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.HDTV720p, new Revision(version: 1)), + new List(), + new QualityModel(Quality.HDTV1080p, new Revision(version: 1)), + new List()) + .Should().Be(UpgradeableRejectReason.QualityCutoff); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs deleted file mode 100644 index 07006315d..000000000 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using NLog; -using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.DecisionEngine.Specifications -{ - public class CutoffSpecification : IDecisionEngineSpecification - { - private readonly UpgradableSpecification _upgradableSpecification; - private readonly ICustomFormatCalculationService _formatService; - private readonly Logger _logger; - - public CutoffSpecification(UpgradableSpecification upgradableSpecification, ICustomFormatCalculationService formatService, Logger logger) - { - _upgradableSpecification = upgradableSpecification; - _formatService = formatService; - _logger = logger; - } - - public SpecificationPriority Priority => SpecificationPriority.Default; - public RejectionType Type => RejectionType.Permanent; - - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) - { - var qualityProfile = subject.Series.QualityProfile.Value; - - foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) - { - if (file == null) - { - _logger.Debug("File is no longer available, skipping this file."); - continue; - } - - _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); - - var customFormats = _formatService.ParseCustomFormat(file); - - if (!_upgradableSpecification.CutoffNotMet(qualityProfile, - file.Quality, - _formatService.ParseCustomFormat(file), - subject.ParsedEpisodeInfo.Quality)) - { - _logger.Debug("Cutoff already met, rejecting."); - - var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); - var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; - - return Decision.Reject("Existing file meets cutoff: {0}", qualityCutoff); - } - } - - return Decision.Accept(); - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index c26fede45..63c4b51e9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -70,13 +70,28 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Checking if release is higher quality than queued release. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); - if (!_upgradableSpecification.IsUpgradable(qualityProfile, - remoteEpisode.ParsedEpisodeInfo.Quality, - queuedItemCustomFormats, - subject.ParsedEpisodeInfo.Quality, - subject.CustomFormats)) + var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile, + remoteEpisode.ParsedEpisodeInfo.Quality, + queuedItemCustomFormats, + subject.ParsedEpisodeInfo.Quality, + subject.CustomFormats); + + switch (upgradeableRejectReason) { - return Decision.Reject("Release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + case UpgradeableRejectReason.BetterQuality: + return Decision.Reject("Release in queue on disk is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + + case UpgradeableRejectReason.BetterRevision: + return Decision.Reject("Release in queue on disk is of equal or higher revision: {0}", remoteEpisode.ParsedEpisodeInfo.Quality.Revision); + + case UpgradeableRejectReason.QualityCutoff: + return Decision.Reject("Release in queue on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + + case UpgradeableRejectReason.CustomFormatCutoff: + return Decision.Reject("Release in queue on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); + + case UpgradeableRejectReason.CustomFormatScore: + return Decision.Reject("Release in queue on disk has an equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(queuedItemCustomFormats)); } _logger.Debug("Checking if profiles allow upgrading. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index c168f6f60..b9c9429f0 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -42,8 +42,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } var cdhEnabled = _configService.EnableCompletedDownloadHandling; + var qualityProfile = subject.Series.QualityProfile.Value; _logger.Debug("Performing history status check on report"); + foreach (var episode in subject.Episodes) { _logger.Debug("Checking current status of episode [{0}] in history", episode.Id); @@ -68,7 +70,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync customFormats, subject.ParsedEpisodeInfo.Quality); - var upgradeable = _upgradableSpecification.IsUpgradable( + var upgradeableRejectReason = _upgradableSpecification.IsUpgradable( subject.Series.QualityProfile, mostRecent.Quality, customFormats, @@ -85,14 +87,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Reject("CDH is disabled and grab event in history already meets cutoff: {0}", mostRecent.Quality); } - if (!upgradeable) - { - if (recent) - { - return Decision.Reject("Recent grab event in history is of equal or higher quality: {0}", mostRecent.Quality); - } + var rejectionSubject = recent ? "Recent" : "CDH is disabled and"; - return Decision.Reject("CDH is disabled and grab event in history is of equal or higher quality: {0}", mostRecent.Quality); + switch (upgradeableRejectReason) + { + case UpgradeableRejectReason.None: + continue; + case UpgradeableRejectReason.BetterQuality: + return Decision.Reject("{0} grab event in history is of equal or higher preference: {1}", rejectionSubject, mostRecent.Quality); + + case UpgradeableRejectReason.BetterRevision: + return Decision.Reject("{0} grab event in history is of equal or higher revision: {1}", rejectionSubject, mostRecent.Quality.Revision); + + case UpgradeableRejectReason.QualityCutoff: + return Decision.Reject("{0} grab event in history meets quality cutoff: {1}", rejectionSubject, qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + + case UpgradeableRejectReason.CustomFormatCutoff: + return Decision.Reject("{0} grab event in history meets Custom Format cutoff: {1}", rejectionSubject, qualityProfile.CutoffFormatScore); + + case UpgradeableRejectReason.CustomFormatScore: + return Decision.Reject("{0} grab event in history has an equal or higher custom format score: {1}", rejectionSubject, qualityProfile.CalculateCustomFormatScore(customFormats)); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index 5b16bb046..c4ecbd19a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public interface IUpgradableSpecification { - bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, List currentCustomFormats, QualityModel newQuality, List newCustomFormats); + UpgradeableRejectReason IsUpgradable(QualityProfile profile, QualityModel currentQuality, List currentCustomFormats, QualityModel newQuality, List newCustomFormats); bool QualityCutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, List currentCustomFormats, QualityModel newQuality = null); bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); @@ -28,22 +28,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } - public bool IsUpgradable(QualityProfile qualityProfile, QualityModel currentQuality, List currentCustomFormats, QualityModel newQuality, List newCustomFormats) + public UpgradeableRejectReason IsUpgradable(QualityProfile qualityProfile, QualityModel currentQuality, List currentCustomFormats, QualityModel newQuality, List newCustomFormats) { var qualityComparer = new QualityModelComparer(qualityProfile); var qualityCompare = qualityComparer.Compare(newQuality?.Quality, currentQuality.Quality); var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; - if (qualityCompare > 0) + if (qualityCompare > 0 && QualityCutoffNotMet(qualityProfile, currentQuality, newQuality)) { _logger.Debug("New item has a better quality. Existing: {0}. New: {1}", currentQuality, newQuality); - return true; + return UpgradeableRejectReason.None; } if (qualityCompare < 0) { _logger.Debug("Existing item has better quality, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); - return false; + return UpgradeableRejectReason.BetterQuality; } var qualityRevisionCompare = newQuality?.Revision.CompareTo(currentQuality.Revision); @@ -54,7 +54,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications qualityRevisionCompare > 0) { _logger.Debug("New item has a better quality revision, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); - return true; + return UpgradeableRejectReason.None; } // Reject unless the user does not prefer propers/repacks and it's a revision downgrade. @@ -62,29 +62,37 @@ namespace NzbDrone.Core.DecisionEngine.Specifications qualityRevisionCompare < 0) { _logger.Debug("Existing item has a better quality revision, skipping. Existing: {0}. New: {1}", currentQuality, newQuality); - return false; + return UpgradeableRejectReason.BetterRevision; + } + + if (qualityCompare > 0) + { + _logger.Debug("Existing item meets cut-off for quality, skipping. Existing: {0}. Cutoff: {1}", + currentQuality, + qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + return UpgradeableRejectReason.QualityCutoff; } var currentFormatScore = qualityProfile.CalculateCustomFormatScore(currentCustomFormats); var newFormatScore = qualityProfile.CalculateCustomFormatScore(newCustomFormats); + if (newFormatScore <= currentFormatScore) + { + _logger.Debug("New item's custom formats [{0}] ({1}) do not improve on [{2}] ({3}), skipping", + newCustomFormats.ConcatToString(), + newFormatScore, + currentCustomFormats.ConcatToString(), + currentFormatScore); + return UpgradeableRejectReason.CustomFormatScore; + } + if (qualityProfile.UpgradeAllowed && currentFormatScore >= qualityProfile.CutoffFormatScore) { _logger.Debug("Existing item meets cut-off for custom formats, skipping. Existing: [{0}] ({1}). Cutoff score: {2}", currentCustomFormats.ConcatToString(), currentFormatScore, qualityProfile.CutoffFormatScore); - return false; - } - - if (newFormatScore <= currentFormatScore) - { - _logger.Debug("New item's custom formats [{0}] ({1}) do not improve on [{2}] ({3}), skipping", - newCustomFormats.ConcatToString(), - newFormatScore, - currentCustomFormats.ConcatToString(), - currentFormatScore); - return false; + return UpgradeableRejectReason.CustomFormatCutoff; } _logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting", @@ -92,7 +100,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications newFormatScore, currentCustomFormats.ConcatToString(), currentFormatScore); - return true; + return UpgradeableRejectReason.None; } public bool QualityCutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) @@ -132,7 +140,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return true; } - _logger.Debug("Existing item meets cut-off, skipping. Existing: {0}", currentQuality); + _logger.Debug("Existing item meets cut-off, skipping. Existing: {0} [{1}] ({2})", + currentQuality, + currentFormats.ConcatToString(), + profile.CalculateCustomFormatScore(currentFormats)); return false; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 9e825cfe6..15168e15f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -26,6 +26,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { + var qualityProfile = subject.Series.QualityProfile.Value; + foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) { if (file == null) @@ -36,15 +38,45 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var customFormats = _formatService.ParseCustomFormat(file); - _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); + _logger.Debug("Comparing file quality with report. Existing file is {0}.", file.Quality); - if (!_upgradableSpecification.IsUpgradable(subject.Series.QualityProfile, - file.Quality, - customFormats, - subject.ParsedEpisodeInfo.Quality, - subject.CustomFormats)) + if (!_upgradableSpecification.CutoffNotMet(qualityProfile, + file.Quality, + _formatService.ParseCustomFormat(file), + subject.ParsedEpisodeInfo.Quality)) { - return Decision.Reject("Existing file on disk is of equal or higher preference: {0}", file.Quality); + _logger.Debug("Cutoff already met, rejecting."); + + var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); + var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; + + return Decision.Reject("Existing file meets cutoff: {0}", qualityCutoff); + } + + var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile, + file.Quality, + customFormats, + subject.ParsedEpisodeInfo.Quality, + subject.CustomFormats); + + switch (upgradeableRejectReason) + { + case UpgradeableRejectReason.None: + continue; + case UpgradeableRejectReason.BetterQuality: + return Decision.Reject("Existing file on disk is of equal or higher preference: {0}", file.Quality); + + case UpgradeableRejectReason.BetterRevision: + return Decision.Reject("Existing file on disk is of equal or higher revision: {0}", file.Quality.Revision); + + case UpgradeableRejectReason.QualityCutoff: + return Decision.Reject("Existing file on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + + case UpgradeableRejectReason.CustomFormatCutoff: + return Decision.Reject("Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); + + case UpgradeableRejectReason.CustomFormatScore: + return Decision.Reject("Existing file on disk has a equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); } } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs new file mode 100644 index 000000000..7ed6d6a0f --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs @@ -0,0 +1,12 @@ +namespace NzbDrone.Core.DecisionEngine +{ + public enum UpgradeableRejectReason + { + None, + BetterQuality, + BetterRevision, + QualityCutoff, + CustomFormatScore, + CustomFormatCutoff + } +} From d903529389866191b0bc4fac0c112c1c87cd3838 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Mon, 2 Sep 2024 20:27:24 +0000 Subject: [PATCH 516/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index f357c07b9..b4f752920 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8513,6 +8513,11 @@ "format": "date-time", "nullable": true }, + "lastSearchTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, "runtime": { "type": "integer", "format": "int32" From 0e384ee3aaeee3c8d5be4895bb0e101a61690d78 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 14:15:54 -0700 Subject: [PATCH 517/762] New: Include seasons and episodes in Trakt import lists Closes #7137 --- .../ImportLists/Trakt/List/TraktListRequestGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs index 7baad110a..08c09d9af 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.List { var link = Settings.BaseUrl.Trim(); - link += $"/users/{Settings.Username.Trim()}/lists/{Settings.Listname.ToUrlSlug()}/items/shows?limit={Settings.Limit}"; + link += $"/users/{Settings.Username.Trim()}/lists/{Settings.Listname.ToUrlSlug()}/items/show,season,episode?limit={Settings.Limit}"; var request = new ImportListRequest(link, HttpAccept.Json); From 0b9a212f33381d07ff67e2453753aaab64cc8041 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 16:04:37 -0700 Subject: [PATCH 518/762] Fixed: Links tooltip closing too quickly --- frontend/src/Components/Tooltip/Tooltip.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/Components/Tooltip/Tooltip.tsx b/frontend/src/Components/Tooltip/Tooltip.tsx index 35cce5738..c3d955ad2 100644 --- a/frontend/src/Components/Tooltip/Tooltip.tsx +++ b/frontend/src/Components/Tooltip/Tooltip.tsx @@ -65,8 +65,7 @@ function Tooltip(props: TooltipProps) { const handleMouseLeave = useCallback(() => { // Still listen for mouse leave on mobile to allow clicks outside to close the tooltip. - - setTimeout(() => { + closeTimeout.current = window.setTimeout(() => { setIsOpen(false); }, 100); }, [setIsOpen]); @@ -111,18 +110,18 @@ function Tooltip(props: TooltipProps) { ); useEffect(() => { - const currentTimeout = closeTimeout.current; - if (updater.current && isOpen) { updater.current(); } + }); + useEffect(() => { return () => { - if (currentTimeout) { - window.clearTimeout(currentTimeout); + if (closeTimeout.current) { + window.clearTimeout(closeTimeout.current); } }; - }); + }, []); return ( From e1e10e195c09ea78179d92ae11c385403096d966 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 16:04:56 -0700 Subject: [PATCH 519/762] Convert NoSeries to TypeScript --- .../src/Series/{NoSeries.js => NoSeries.tsx} | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) rename frontend/src/Series/{NoSeries.js => NoSeries.tsx} (73%) diff --git a/frontend/src/Series/NoSeries.js b/frontend/src/Series/NoSeries.tsx similarity index 73% rename from frontend/src/Series/NoSeries.js rename to frontend/src/Series/NoSeries.tsx index da526c644..54053ad9e 100644 --- a/frontend/src/Series/NoSeries.js +++ b/frontend/src/Series/NoSeries.tsx @@ -1,11 +1,14 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Button from 'Components/Link/Button'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './NoSeries.css'; -function NoSeries(props) { +interface NoSeriesProps { + totalItems: number; +} + +function NoSeries(props: NoSeriesProps) { const { totalItems } = props; if (totalItems > 0) { @@ -25,19 +28,13 @@ function NoSeries(props) {
-
-
@@ -45,8 +42,4 @@ function NoSeries(props) { ); } -NoSeries.propTypes = { - totalItems: PropTypes.number.isRequired -}; - export default NoSeries; From ee99c3895de497bb1c99193ba16c56393b8ff593 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Sep 2024 16:05:19 -0700 Subject: [PATCH 520/762] Convert series images to TypeScript --- frontend/src/Series/Series.ts | 4 +- frontend/src/Series/SeriesBanner.js | 29 ---- frontend/src/Series/SeriesBanner.tsx | 23 ++++ frontend/src/Series/SeriesImage.js | 198 --------------------------- frontend/src/Series/SeriesImage.tsx | 128 +++++++++++++++++ frontend/src/Series/SeriesPoster.js | 29 ---- frontend/src/Series/SeriesPoster.tsx | 23 ++++ 7 files changed, 177 insertions(+), 257 deletions(-) delete mode 100644 frontend/src/Series/SeriesBanner.js create mode 100644 frontend/src/Series/SeriesBanner.tsx delete mode 100644 frontend/src/Series/SeriesImage.js create mode 100644 frontend/src/Series/SeriesImage.tsx delete mode 100644 frontend/src/Series/SeriesPoster.js create mode 100644 frontend/src/Series/SeriesPoster.tsx diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index 9f9148b27..32773e47a 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -19,8 +19,10 @@ export type SeriesStatus = 'continuing' | 'ended' | 'upcoming' | 'deleted'; export type MonitorNewItems = 'all' | 'none'; +export type CoverType = 'poster' | 'banner' | 'fanart' | 'season'; + export interface Image { - coverType: string; + coverType: CoverType; url: string; remoteUrl: string; } diff --git a/frontend/src/Series/SeriesBanner.js b/frontend/src/Series/SeriesBanner.js deleted file mode 100644 index e88e3c327..000000000 --- a/frontend/src/Series/SeriesBanner.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import SeriesImage from './SeriesImage'; - -const bannerPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXsAAABGCAIAAACiz6ObAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAVeSURBVHhe7d3dduI4EEXheaMOfzPv/2ZzpCqLsmULQWjf1P4WkwnEtrhhr7IhnX9uAHAWigPgPBQHwHkoDoDzUBwA56E4AM5DcQCch+IAOA/FwQfuuonfA6ZRHLymuDwej3+r/zp6TI9rAxqElygODtXQ7CRmwNLj+wMdioMdas3uODOPkQe7KA5Wft+aiO5gg+LAfbc1DedZiCgOzF/JTaOD+zrIjeKguF6vmnE8D9+mlKloWsIXQ2IUByU3Rqc/HomviktQneQoTnaXy/Wi/xbfnXQ03eiAfuirL+QLIyWKk1oLQWhOic5XrunoIJvc+DK+ODKiOEmpBY9HuZpbaxByUOnxX0bHLhX74Zbpxuhx3r1Ki+IkZUGJXVAS+i5YPt5io83zsOuztrY00cmJ4mSkIlgdZBWdy/Xn51kHozTMjzuxNSbmRuKvTdTnglwoTkY2ZTS66z2ogdhEx+4oJZu9Gj2uKmmDuuHKj44VirMZmix2SIXipBMHnGZ9TWdbCrPct8M43dVD/cY6QJebnWDZTIQ8KE46R6OKBhBvQ51NdqMzQ3tp1z9/ygHsES26mW4axpxsKE4uuwNO086MajU+iY7vGHIjR7kxelL+5JAAxcnlaMAx+mnrhLVDo8pb0VFoSmxCbhS50ZK8aZUMxcnFX+XH4gVgi04fHD2iH+2WqH/8fn/xFjsnVqlQnETGp1Qmjjk91URTT7vZ2dNgBtKi46lKKE4qFCeR8fWUxt5+6pWTrHqe1d+OqqNF/aBDvGOVB8VJZLI49/CmVWPXdEz5pr91Hx2UmalKKE4eFCeRlyc45hE+EGjsZMpa03/T7vaTzmTjuHicB8VJZLI42syDsShRWXhrluK0R8rdneLMNY7ipEFxEpksjngwFq0pJTXt++4mvsNidqqiOGlQnETeKE78bcxLKU4dZupXad+Y8FPfZUFxsEFxEpkvjjb2ZtRP37QRZvvNMt34XYqDVyhOIm/MOPEN8kFxFu2u77KgONigOIlMvv61lQdj8fzg3xKXzc2Gnf4NcoqDDYqTyHRxdj52XCeZ8mXANw2UEj/o0P1OcbKgOIlMvv61WV+cS/0VTZ9o9iad3Y8d8wlAbFCcRCZf/9rSg7GmqFhcNsXR7POb33LQSEVx8qA4iUwVp7uIE6ksJTdt2Cn12W+N0aIvT+W0gT09ZEBxcnn5+leVvBb1ffH6q+FdU/SA3TqlQOvtX57KUZxUKE4u49e/Xvzts3/KhurRF2Ss7LI+ydKi48xxSpUKxUln8PqPA84HuTHltKte6/H7wzFHz8WfFnKgOOkcFcfObqwRPqoMr9EMLNHx3QeLKkb1SSELipPO7vXjmBspI8r7001ULyo/x5z7wZhjTwl5UJyMNqc5ys36gnHhd6K6r7ZclL+KJ/Vh1Wr1nnrZP/z9X/1Pe/p6CwachChORspEO80Z58Y2VhqOTouMfliPU/8yZ5iV4tFKdG6rde3JIBWKk5SNOfaytyJI35pxaHYpTrE7OuT6sOWYom3qE0EuFCevelLj042SELugMHzQmmj9Z4UL+17UGnKTFsVJzRKwzc31qjnFy/ELatZzifJhZV/ClkZOFCe7koPwLrjK88vpJtKk48et0bGFfGGkRHFwiwPOF3Nj7A0pO7gWshWRFsVBoRzo69dzY1p06lJIjeLA3ef+LYsP2AUdQCgOnhSdv3FWxTtTaCgOtr7VHR3DzqeAhuJgn2Lh5XifgkVrsIvi4JCGHYVD+ZifeLQp51AYoDiYoYyU+hwpPyY0mEBxAJyH4gA4D8UBcB6KA+A8FAfAeSgOgPNQHADnoTgAzkNxAJzldvsfnbIbPuBaveQAAAAASUVORK5CYII='; - -function SeriesBanner(props) { - return ( - - ); -} - -SeriesBanner.propTypes = { - ...SeriesImage.propTypes, - coverType: PropTypes.string, - placeholder: PropTypes.string, - overflow: PropTypes.bool, - size: PropTypes.number.isRequired -}; - -SeriesBanner.defaultProps = { - size: 70 -}; - -export default SeriesBanner; diff --git a/frontend/src/Series/SeriesBanner.tsx b/frontend/src/Series/SeriesBanner.tsx new file mode 100644 index 000000000..8b6637786 --- /dev/null +++ b/frontend/src/Series/SeriesBanner.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import SeriesImage, { SeriesImageProps } from './SeriesImage'; + +const bannerPlaceholder = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXsAAABGCAIAAACiz6ObAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAVeSURBVHhe7d3dduI4EEXheaMOfzPv/2ZzpCqLsmULQWjf1P4WkwnEtrhhr7IhnX9uAHAWigPgPBQHwHkoDoDzUBwA56E4AM5DcQCch+IAOA/FwQfuuonfA6ZRHLymuDwej3+r/zp6TI9rAxqElygODtXQ7CRmwNLj+wMdioMdas3uODOPkQe7KA5Wft+aiO5gg+LAfbc1DedZiCgOzF/JTaOD+zrIjeKguF6vmnE8D9+mlKloWsIXQ2IUByU3Rqc/HomviktQneQoTnaXy/Wi/xbfnXQ03eiAfuirL+QLIyWKk1oLQWhOic5XrunoIJvc+DK+ODKiOEmpBY9HuZpbaxByUOnxX0bHLhX74Zbpxuhx3r1Ki+IkZUGJXVAS+i5YPt5io83zsOuztrY00cmJ4mSkIlgdZBWdy/Xn51kHozTMjzuxNSbmRuKvTdTnglwoTkY2ZTS66z2ogdhEx+4oJZu9Gj2uKmmDuuHKj44VirMZmix2SIXipBMHnGZ9TWdbCrPct8M43dVD/cY6QJebnWDZTIQ8KE46R6OKBhBvQ51NdqMzQ3tp1z9/ygHsES26mW4axpxsKE4uuwNO086MajU+iY7vGHIjR7kxelL+5JAAxcnlaMAx+mnrhLVDo8pb0VFoSmxCbhS50ZK8aZUMxcnFX+XH4gVgi04fHD2iH+2WqH/8fn/xFjsnVqlQnETGp1Qmjjk91URTT7vZ2dNgBtKi46lKKE4qFCeR8fWUxt5+6pWTrHqe1d+OqqNF/aBDvGOVB8VJZLI49/CmVWPXdEz5pr91Hx2UmalKKE4eFCeRlyc45hE+EGjsZMpa03/T7vaTzmTjuHicB8VJZLI42syDsShRWXhrluK0R8rdneLMNY7ipEFxEpksjngwFq0pJTXt++4mvsNidqqiOGlQnETeKE78bcxLKU4dZupXad+Y8FPfZUFxsEFxEpkvjjb2ZtRP37QRZvvNMt34XYqDVyhOIm/MOPEN8kFxFu2u77KgONigOIlMvv61lQdj8fzg3xKXzc2Gnf4NcoqDDYqTyHRxdj52XCeZ8mXANw2UEj/o0P1OcbKgOIlMvv61WV+cS/0VTZ9o9iad3Y8d8wlAbFCcRCZf/9rSg7GmqFhcNsXR7POb33LQSEVx8qA4iUwVp7uIE6ksJTdt2Cn12W+N0aIvT+W0gT09ZEBxcnn5+leVvBb1ffH6q+FdU/SA3TqlQOvtX57KUZxUKE4u49e/Xvzts3/KhurRF2Ss7LI+ydKi48xxSpUKxUln8PqPA84HuTHltKte6/H7wzFHz8WfFnKgOOkcFcfObqwRPqoMr9EMLNHx3QeLKkb1SSELipPO7vXjmBspI8r7001ULyo/x5z7wZhjTwl5UJyMNqc5ys36gnHhd6K6r7ZclL+KJ/Vh1Wr1nnrZP/z9X/1Pe/p6CwachChORspEO80Z58Y2VhqOTouMfliPU/8yZ5iV4tFKdG6rde3JIBWKk5SNOfaytyJI35pxaHYpTrE7OuT6sOWYom3qE0EuFCevelLj042SELugMHzQmmj9Z4UL+17UGnKTFsVJzRKwzc31qjnFy/ELatZzifJhZV/ClkZOFCe7koPwLrjK88vpJtKk48et0bGFfGGkRHFwiwPOF3Nj7A0pO7gWshWRFsVBoRzo69dzY1p06lJIjeLA3ef+LYsP2AUdQCgOnhSdv3FWxTtTaCgOtr7VHR3DzqeAhuJgn2Lh5XifgkVrsIvi4JCGHYVD+ZifeLQp51AYoDiYoYyU+hwpPyY0mEBxAJyH4gA4D8UBcB6KA+A8FAfAeSgOgPNQHADnoTgAzkNxAJzldvsfnbIbPuBaveQAAAAASUVORK5CYII='; + +interface SeriesBannerProps + extends Omit { + size?: 35 | 70; +} + +function SeriesBanner({ size = 70, ...otherProps }: SeriesBannerProps) { + return ( + + ); +} + +export default SeriesBanner; diff --git a/frontend/src/Series/SeriesImage.js b/frontend/src/Series/SeriesImage.js deleted file mode 100644 index b1bd738de..000000000 --- a/frontend/src/Series/SeriesImage.js +++ /dev/null @@ -1,198 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import LazyLoad from 'react-lazyload'; - -function findImage(images, coverType) { - return images.find((image) => image.coverType === coverType); -} - -function getUrl(image, coverType, size) { - const imageUrl = image?.url; - - if (imageUrl) { - return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); - } -} - -class SeriesImage extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const pixelRatio = Math.max(Math.round(window.devicePixelRatio), 1); - - const { - images, - coverType, - size - } = props; - - const image = findImage(images, coverType); - - this.state = { - pixelRatio, - image, - url: getUrl(image, coverType, pixelRatio * size), - isLoaded: false, - hasError: false - }; - } - - componentDidMount() { - if (!this.state.url && this.props.onError) { - this.props.onError(); - } - } - - componentDidUpdate() { - const { - images, - coverType, - placeholder, - size, - onError - } = this.props; - - const { - image, - pixelRatio - } = this.state; - - const nextImage = findImage(images, coverType); - - if (nextImage && (!image || nextImage.url !== image.url)) { - this.setState({ - image: nextImage, - url: getUrl(nextImage, coverType, pixelRatio * size), - hasError: false - // Don't reset isLoaded, as we want to immediately try to - // show the new image, whether an image was shown previously - // or the placeholder was shown. - }); - } else if (!nextImage && image) { - this.setState({ - image: nextImage, - url: placeholder, - hasError: false - }); - - if (onError) { - onError(); - } - } - } - - // - // Listeners - - onError = () => { - this.setState({ - hasError: true - }); - - if (this.props.onError) { - this.props.onError(); - } - }; - - onLoad = () => { - this.setState({ - isLoaded: true, - hasError: false - }); - - if (this.props.onLoad) { - this.props.onLoad(); - } - }; - - // - // Render - - render() { - const { - className, - style, - placeholder, - size, - lazy, - overflow - } = this.props; - - const { - url, - hasError, - isLoaded - } = this.state; - - if (hasError || !url) { - return ( - - ); - } - - if (lazy) { - return ( - - } - > - - - ); - } - - return ( - - ); - } -} - -SeriesImage.propTypes = { - className: PropTypes.string, - style: PropTypes.object, - images: PropTypes.arrayOf(PropTypes.object).isRequired, - coverType: PropTypes.string.isRequired, - placeholder: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - lazy: PropTypes.bool.isRequired, - overflow: PropTypes.bool.isRequired, - onError: PropTypes.func, - onLoad: PropTypes.func -}; - -SeriesImage.defaultProps = { - size: 250, - lazy: true, - overflow: false -}; - -export default SeriesImage; diff --git a/frontend/src/Series/SeriesImage.tsx b/frontend/src/Series/SeriesImage.tsx new file mode 100644 index 000000000..99a6d961f --- /dev/null +++ b/frontend/src/Series/SeriesImage.tsx @@ -0,0 +1,128 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import LazyLoad from 'react-lazyload'; +import { CoverType, Image } from './Series'; + +function findImage(images: Image[], coverType: CoverType) { + return images.find((image) => image.coverType === coverType); +} + +function getUrl(image: Image, coverType: CoverType, size: number) { + const imageUrl = image?.url; + + return imageUrl + ? imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`) + : null; +} + +export interface SeriesImageProps { + className?: string; + style?: object; + images: Image[]; + coverType: CoverType; + placeholder: string; + size?: number; + lazy?: boolean; + overflow?: boolean; + onError?: () => void; + onLoad?: () => void; +} + +const pixelRatio = Math.max(Math.round(window.devicePixelRatio), 1); + +function SeriesImage({ + className, + style, + images, + coverType, + placeholder, + size = 250, + lazy = true, + overflow = false, + onError, + onLoad, +}: SeriesImageProps) { + const [url, setUrl] = useState(null); + const [hasError, setHasError] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const image = useRef(null); + + const handleLoad = useCallback(() => { + setHasError(false); + setIsLoaded(true); + onLoad?.(); + }, [setHasError, setIsLoaded, onLoad]); + + const handleError = useCallback(() => { + setHasError(true); + setIsLoaded(false); + onError?.(); + }, [setHasError, setIsLoaded, onError]); + + useEffect(() => { + const nextImage = findImage(images, coverType); + + if (nextImage && (!image.current || nextImage.url !== image.current.url)) { + // Don't reset isLoaded, as we want to immediately try to + // show the new image, whether an image was shown previously + // or the placeholder was shown. + image.current = nextImage; + + setUrl(getUrl(nextImage, coverType, pixelRatio * size)); + setHasError(false); + } else if (!nextImage) { + if (image.current) { + image.current = null; + setUrl(placeholder); + setHasError(false); + onError?.(); + } + } + }, [images, coverType, placeholder, size, onError]); + + useEffect(() => { + if (!image.current) { + onError?.(); + } + // This should only run once when the component mounts, + // so we don't need to include the other dependencies. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (hasError || !url) { + return ; + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); +} + +export default SeriesImage; diff --git a/frontend/src/Series/SeriesPoster.js b/frontend/src/Series/SeriesPoster.js deleted file mode 100644 index 0f6de504e..000000000 --- a/frontend/src/Series/SeriesPoster.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import SeriesImage from './SeriesImage'; - -const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAKHklEQVR42u2c25bbKhJATTmUPAZKPerBjTMo0fn/n5wHSYBkXUDCnXPWwEPaneVIO0XdKAouzT9kXApoAS2gBbSAFtACWkALaAEtoAW0gBbQAlpAC2gBLaAFtIAW0AJaQAtoAS2gBbSAFtACWkALaAEtoAW0gP4DQLXW+vF4GGMeD6211n87UK2NsW33OlprTB7eSw5I220PmwH2JKh+7EGOoj3Lejkly0hKx/pHQLVpu9RhzbeDHsEc1PU7QbXpDo/WfB/oQWmeUoADoMZ2Z4fV7wfV5zG7ruvMu0FPzvpxoV7+hDiPCDUJVLddzmHfBfqZlzONNAG0VrXNy/lB7wCtifKSth+KKD8oEREpshk5JRFRnRm0VkREJLKR2kYQERF9ZAUdHkokM5EO8iQiQRlBiSG552Yhdf91wfDf2UBrkj+Q6nyk9mPklAzj9PQSqZ/qR0aZtrWXZ0UUZfuXKL9ERBKzkdray/Nf/YcsoIrmpOcsynMKqMZHngfVn4MHJeVJz/jTYN7RORN1GlTb7tM5Eqw86fMg55Pc47jjpGY3698DtV3Xfgo1kjqZEulD4tTKafrVO+cP23WPU6Bm6vSC2SfVJK/w2p8fntPPu6ht13WtPgE6SK0dSeuQlMSn/ZWW1EvHWYGYOxF7AtTOAzMpHpKKRwqm8jpZMfHq7MxhUD+3bXMb9QmwdwIDqrYx6bS1WnhMuoWcrX/JQdBw5RHMPgQyJRKiee6w/rLmM8RclueOSC9RAp1YlPyBKnirEoK0sXZVlk9NQrh/URMhm9mRG/oQ6Mz/tKGehqRESgjVaGPsRLSttU+jGxJCBt+Vap1zy542QJ9/zYTjPL/iWAmasd4EUdNoYx7m68sYrT8/ahJTSlIVIrq/kc18HvQB0AWH3jhBIuN3ehlSSiGEFEoKIYWQcv4FVQGwSjlP3MavS9dBl2Lk5xiiGICPp/EDOQBzetMs6LVOBl2MkL/G5BkAYEmmm0NVAAAuIi1xrov0EmfyLqVQnhThni5Pz7mSgOlE0JXqTaulI0WAW4o8kfGAUz7SKlJroGuxsXUxiXO8Tn3/jjwZIvfypLUXJIKuppvGp+eAHKp4TkDwGaj4ufYCnQS6kWz6xBcQgVWRdsT4FcKMfjXqPpNAN1JN4xRT8CtCnEXdGCD6zI6E3citU0A3lkStEymJKwPGZQSoRBbIk+THRg6TArq5zDA+wxDAcMZZKymlVK82D2Ga9zO5En0A1AYUwYKiF5XAYQgxllbGZCD4FrXJ5d1Lqop2XauDd05EJypkDBgHYIxxrNaU4ra9ZaHjQTdX7a0Vaun1Aq8AAAA4/MGwWvzilimtzv0leea7rq0XRKVuwELQ4aNY4my+CbTTC69HAHgFDVx8sBIxB/YgLinx0/lkscgJiAgAHJEDICICcFyQqdirB0WD7lUWLKlXTgQERARE4IjAAThH5K+zv1+40rGguz0izUxJb6E4e9l6HeBzge7uVz1ygc6VVKBjG37wAHSeuIjdUpCJBd2tJ3yJeWY06OQg10GwAzuIN4Hu1+nMZOrltRclH7S0l2ivrr2Upzq6W7G02UCn1lQxBOQcOCBw4JwDAFwHSg4I04LF/vZfTlA5WWP04R0QARAAOSBcERGG31k493LfBNp8oB9yakq97cxCqDMohpO4tF9VywfaBDISzr4XItNAG/6/IkrV2UDb/wSgdzayIf+7gXYBaH1ng29yUdP/gtjHU+lz05jibz6J6kBEzoHy8AcfP3PEScD/VtBJaKogiJpjZOKBDuDE5X8r6K9dUJyA/j0kegevk5MQ6gIT+3NWryfuiY/JKALiFQA4R47IB2qc+tFvBW3UJDL1wkNAuCLnCPw6ps8c+VRFSex3T70pMlEfQgHh2ufPCFfoQ+iop6JOikzvSkrEIFG4YuDjPSibJCUyX1Kyn48+J6AKt0Mou6WtRBbrZMdAzbRmI9jo7H0kxd5FcYRplkdK7YKabEsRI2aFJeS9jY/pXv+p/3Cdre7Ef78NtJ0v7CUHQOQ4WHmf3l9HhzUv6Ox6fJ1tudzMl8CCuwwKAQBYYFWUvArVuQoQr+t6EnwlhOJrBXLPmtpsJR0jlkpki6CvnKT2KiXxJZ0dl/x7qfZECoE5VzrqwWLdfC8tiS+S7VjTZGk3FSrvSRGBM0Bc/p78sMkqeqSQ+9uKtVK9QAQGDBgDfNmAjq6SJYBul8b1pMo9V8D7XVTVXcwoJ1u82wlUSml8M8EJbV4s7TPVS9u17B5bw0/ZbNice7/RRAoZrJS/Z3bGryHp7Zlp+2Zr7n/7wrhEhvwSsXMrGOdhbrLVhWjTthjX5+Z584L6wafZ+wYpcM6idu5M2qat2d8LVQjIGaoYUKoY8nA7ct1Vp23ars+9EQEnxnIS3QEhIJUm8bTDZa/b7WUn1PW9AiCP5uzzlnD11MaXxQ+0anSurfKlSrdPOqk+r3RApPeULJ8Isr6PGID3IbJe959T5yqmK1Kb0qmx0U60KNJxmdwvN+W+q59F2LBg1sRv1m93ki11JXlDWszg9i0qUBelEwS6BfoqUqP8ImmZUykphRJCSKnUwhfuWAX9Gia+kWyz29Gu7IXUhFxUYjrPSgpxE5Lq/pDKR01S3MR8H1pJuju/r+SjjRXoJuhjbXMJ5+0ZStwENfpp+9H2P/pex9scVnjS2ZaTPdqRa5c7NJBNXy0ENcYud5Dap/mUNznbPxtnQ00TPn0UNHzKw8uTyWnvaGPtViZs22czTU/HjlxFMlyW2OPN2G5mfn+5PlAEFfaQyK+IJufWPijUAAxmX0e1OO/14VsnTznae6ifkqIPtLaGwjYd13AgHak5AzqkewEnHsLsSfzCpb77bkL5tdVBFnsEw/T27uwojEbJ526tDvR0fFKtpN6d+IjTN6brHtJHeOfyqTlyrCU4g+E9v1J62+LjzjNZV2NUXp5KHTrT0nWtVguzo/TuQeZ9UE2vJ1rUoFdHhlHSxVOvs1nO3PW5csgpjnN2nfGezulpplOMpKgO4qYSp07Zt0/n/hGpJlKZDgc2TdM/03m+R3dqtDOZRp0KjjxpK4GP+e5pzq7rjJfpj6wnbRvya50MnF3nZl8BNjlBGz/vpssx/Ow3eUHHc+syD+e4A6SiD9gn3FhARErl4uzXNapu3gDa1IrycXadIXrL1QpN09Q5ORPv/0i7pyQvqH4faM4bVRKvfkm+SyeTUJMvU0q/nSiLUNOvJzpy39Ppi3+OXPh06GIq/fzWWT8Oegb16F1vh295O3Z72uG7087cm6cT7/z66wTm2ZsIU8RqT93vd/puRx0n1/O3O+a4LVM/NmFtlvsyc90/qrUxz5fT4MZku4Q0/42uWue+I/VNoG8aBbSAFtACWkALaAEtoAW0gBbQAlpAC2gBLaAFtIAW0AJaQAtoAS2gBbSAFtACWkALaAEtoAW0gBbQAlpA/99B/wd7kHH8CSaCpAAAAABJRU5ErkJggg=='; - -function SeriesPoster(props) { - return ( - - ); -} - -SeriesPoster.propTypes = { - ...SeriesImage.propTypes, - coverType: PropTypes.string, - placeholder: PropTypes.string, - overflow: PropTypes.bool, - size: PropTypes.number.isRequired -}; - -SeriesPoster.defaultProps = { - size: 250 -}; - -export default SeriesPoster; diff --git a/frontend/src/Series/SeriesPoster.tsx b/frontend/src/Series/SeriesPoster.tsx new file mode 100644 index 000000000..cf9c83a3c --- /dev/null +++ b/frontend/src/Series/SeriesPoster.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import SeriesImage, { SeriesImageProps } from './SeriesImage'; + +const posterPlaceholder = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAKHklEQVR42u2c25bbKhJATTmUPAZKPerBjTMo0fn/n5wHSYBkXUDCnXPWwEPaneVIO0XdKAouzT9kXApoAS2gBbSAFtACWkALaAEtoAW0gBbQAlpAC2gBLaAFtIAW0AJaQAtoAS2gBbSAFtACWkALaAEtoAW0gP4DQLXW+vF4GGMeD6211n87UK2NsW33OlprTB7eSw5I220PmwH2JKh+7EGOoj3Lejkly0hKx/pHQLVpu9RhzbeDHsEc1PU7QbXpDo/WfB/oQWmeUoADoMZ2Z4fV7wfV5zG7ruvMu0FPzvpxoV7+hDiPCDUJVLddzmHfBfqZlzONNAG0VrXNy/lB7wCtifKSth+KKD8oEREpshk5JRFRnRm0VkREJLKR2kYQERF9ZAUdHkokM5EO8iQiQRlBiSG552Yhdf91wfDf2UBrkj+Q6nyk9mPklAzj9PQSqZ/qR0aZtrWXZ0UUZfuXKL9ERBKzkdray/Nf/YcsoIrmpOcsynMKqMZHngfVn4MHJeVJz/jTYN7RORN1GlTb7tM5Eqw86fMg55Pc47jjpGY3698DtV3Xfgo1kjqZEulD4tTKafrVO+cP23WPU6Bm6vSC2SfVJK/w2p8fntPPu6ht13WtPgE6SK0dSeuQlMSn/ZWW1EvHWYGYOxF7AtTOAzMpHpKKRwqm8jpZMfHq7MxhUD+3bXMb9QmwdwIDqrYx6bS1WnhMuoWcrX/JQdBw5RHMPgQyJRKiee6w/rLmM8RclueOSC9RAp1YlPyBKnirEoK0sXZVlk9NQrh/URMhm9mRG/oQ6Mz/tKGehqRESgjVaGPsRLSttU+jGxJCBt+Vap1zy542QJ9/zYTjPL/iWAmasd4EUdNoYx7m68sYrT8/ahJTSlIVIrq/kc18HvQB0AWH3jhBIuN3ehlSSiGEFEoKIYWQcv4FVQGwSjlP3MavS9dBl2Lk5xiiGICPp/EDOQBzetMs6LVOBl2MkL/G5BkAYEmmm0NVAAAuIi1xrov0EmfyLqVQnhThni5Pz7mSgOlE0JXqTaulI0WAW4o8kfGAUz7SKlJroGuxsXUxiXO8Tn3/jjwZIvfypLUXJIKuppvGp+eAHKp4TkDwGaj4ufYCnQS6kWz6xBcQgVWRdsT4FcKMfjXqPpNAN1JN4xRT8CtCnEXdGCD6zI6E3citU0A3lkStEymJKwPGZQSoRBbIk+THRg6TArq5zDA+wxDAcMZZKymlVK82D2Ga9zO5En0A1AYUwYKiF5XAYQgxllbGZCD4FrXJ5d1Lqop2XauDd05EJypkDBgHYIxxrNaU4ra9ZaHjQTdX7a0Vaun1Aq8AAAA4/MGwWvzilimtzv0leea7rq0XRKVuwELQ4aNY4my+CbTTC69HAHgFDVx8sBIxB/YgLinx0/lkscgJiAgAHJEDICICcFyQqdirB0WD7lUWLKlXTgQERARE4IjAAThH5K+zv1+40rGguz0izUxJb6E4e9l6HeBzge7uVz1ygc6VVKBjG37wAHSeuIjdUpCJBd2tJ3yJeWY06OQg10GwAzuIN4Hu1+nMZOrltRclH7S0l2ivrr2Upzq6W7G02UCn1lQxBOQcOCBw4JwDAFwHSg4I04LF/vZfTlA5WWP04R0QARAAOSBcERGG31k493LfBNp8oB9yakq97cxCqDMohpO4tF9VywfaBDISzr4XItNAG/6/IkrV2UDb/wSgdzayIf+7gXYBaH1ng29yUdP/gtjHU+lz05jibz6J6kBEzoHy8AcfP3PEScD/VtBJaKogiJpjZOKBDuDE5X8r6K9dUJyA/j0kegevk5MQ6gIT+3NWryfuiY/JKALiFQA4R47IB2qc+tFvBW3UJDL1wkNAuCLnCPw6ps8c+VRFSex3T70pMlEfQgHh2ufPCFfoQ+iop6JOikzvSkrEIFG4YuDjPSibJCUyX1Kyn48+J6AKt0Mou6WtRBbrZMdAzbRmI9jo7H0kxd5FcYRplkdK7YKabEsRI2aFJeS9jY/pXv+p/3Cdre7Ef78NtJ0v7CUHQOQ4WHmf3l9HhzUv6Ox6fJ1tudzMl8CCuwwKAQBYYFWUvArVuQoQr+t6EnwlhOJrBXLPmtpsJR0jlkpki6CvnKT2KiXxJZ0dl/x7qfZECoE5VzrqwWLdfC8tiS+S7VjTZGk3FSrvSRGBM0Bc/p78sMkqeqSQ+9uKtVK9QAQGDBgDfNmAjq6SJYBul8b1pMo9V8D7XVTVXcwoJ1u82wlUSml8M8EJbV4s7TPVS9u17B5bw0/ZbNice7/RRAoZrJS/Z3bGryHp7Zlp+2Zr7n/7wrhEhvwSsXMrGOdhbrLVhWjTthjX5+Z584L6wafZ+wYpcM6idu5M2qat2d8LVQjIGaoYUKoY8nA7ct1Vp23ars+9EQEnxnIS3QEhIJUm8bTDZa/b7WUn1PW9AiCP5uzzlnD11MaXxQ+0anSurfKlSrdPOqk+r3RApPeULJ8Isr6PGID3IbJe959T5yqmK1Kb0qmx0U60KNJxmdwvN+W+q59F2LBg1sRv1m93ki11JXlDWszg9i0qUBelEwS6BfoqUqP8ImmZUykphRJCSKnUwhfuWAX9Gia+kWyz29Gu7IXUhFxUYjrPSgpxE5Lq/pDKR01S3MR8H1pJuju/r+SjjRXoJuhjbXMJ5+0ZStwENfpp+9H2P/pex9scVnjS2ZaTPdqRa5c7NJBNXy0ENcYud5Dap/mUNznbPxtnQ00TPn0UNHzKw8uTyWnvaGPtViZs22czTU/HjlxFMlyW2OPN2G5mfn+5PlAEFfaQyK+IJufWPijUAAxmX0e1OO/14VsnTznae6ifkqIPtLaGwjYd13AgHak5AzqkewEnHsLsSfzCpb77bkL5tdVBFnsEw/T27uwojEbJ526tDvR0fFKtpN6d+IjTN6brHtJHeOfyqTlyrCU4g+E9v1J62+LjzjNZV2NUXp5KHTrT0nWtVguzo/TuQeZ9UE2vJ1rUoFdHhlHSxVOvs1nO3PW5csgpjnN2nfGezulpplOMpKgO4qYSp07Zt0/n/hGpJlKZDgc2TdM/03m+R3dqtDOZRp0KjjxpK4GP+e5pzq7rjJfpj6wnbRvya50MnF3nZl8BNjlBGz/vpssx/Ow3eUHHc+syD+e4A6SiD9gn3FhARErl4uzXNapu3gDa1IrycXadIXrL1QpN09Q5ORPv/0i7pyQvqH4faM4bVRKvfkm+SyeTUJMvU0q/nSiLUNOvJzpy39Ppi3+OXPh06GIq/fzWWT8Oegb16F1vh295O3Z72uG7087cm6cT7/z66wTm2ZsIU8RqT93vd/puRx0n1/O3O+a4LVM/NmFtlvsyc90/qrUxz5fT4MZku4Q0/42uWue+I/VNoG8aBbSAFtACWkALaAEtoAW0gBbQAlpAC2gBLaAFtIAW0AJaQAtoAS2gBbSAFtACWkALaAEtoAW0gBbQAlpA/99B/wd7kHH8CSaCpAAAAABJRU5ErkJggg=='; + +interface SeriesPosterProps + extends Omit { + size?: 250 | 500; +} + +function SeriesPoster({ size = 250, ...otherProps }: SeriesPosterProps) { + return ( + + ); +} + +export default SeriesPoster; From 55aaaa5c406a71199152a24f5efb4cf16dbe10fd Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Sep 2024 20:19:36 -0700 Subject: [PATCH 521/762] New: Add MDBList link to series details Closes #7162 --- ...DetailsLinks.js => SeriesDetailsLinks.tsx} | 85 ++++++++++--------- frontend/src/Series/Series.ts | 2 +- 2 files changed, 46 insertions(+), 41 deletions(-) rename frontend/src/Series/Details/{SeriesDetailsLinks.js => SeriesDetailsLinks.tsx} (50%) diff --git a/frontend/src/Series/Details/SeriesDetailsLinks.js b/frontend/src/Series/Details/SeriesDetailsLinks.tsx similarity index 50% rename from frontend/src/Series/Details/SeriesDetailsLinks.js rename to frontend/src/Series/Details/SeriesDetailsLinks.tsx index 1aa67f297..b2a725a68 100644 --- a/frontend/src/Series/Details/SeriesDetailsLinks.js +++ b/frontend/src/Series/Details/SeriesDetailsLinks.tsx @@ -1,23 +1,23 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Label from 'Components/Label'; import Link from 'Components/Link/Link'; import { kinds, sizes } from 'Helpers/Props'; +import Series from 'Series/Series'; import styles from './SeriesDetailsLinks.css'; -function SeriesDetailsLinks(props) { - const { - tvdbId, - tvMazeId, - imdbId, - tmdbId - } = props; +type SeriesDetailsLinksProps = Pick< + Series, + 'tvdbId' | 'tvMazeId' | 'imdbId' | 'tmdbId' +>; + +function SeriesDetailsLinks(props: SeriesDetailsLinksProps) { + const { tvdbId, tvMazeId, imdbId, tmdbId } = props; return (
); } -SeriesDetailsLinks.propTypes = { - tvdbId: PropTypes.number.isRequired, - tvMazeId: PropTypes.number, - imdbId: PropTypes.string, - tmdbId: PropTypes.number -}; - export default SeriesDetailsLinks; diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index 32773e47a..b35b90d6b 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -73,7 +73,7 @@ interface Series extends ModelBase { firstAired: string; genres: string[]; images: Image[]; - imdbId: string; + imdbId?: string; monitored: boolean; monitorNewItems: MonitorNewItems; network: string; From a9072ac460f971d3da737de6446153d8cbf1e1c2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 25 Aug 2024 22:28:48 -0700 Subject: [PATCH 522/762] Convert Progress Bars to TypeScript --- .../src/Components/CircularProgressBar.js | 138 ------------------ .../src/Components/CircularProgressBar.tsx | 99 +++++++++++++ frontend/src/Components/ProgressBar.js | 114 --------------- frontend/src/Components/ProgressBar.tsx | 94 ++++++++++++ 4 files changed, 193 insertions(+), 252 deletions(-) delete mode 100644 frontend/src/Components/CircularProgressBar.js create mode 100644 frontend/src/Components/CircularProgressBar.tsx delete mode 100644 frontend/src/Components/ProgressBar.js create mode 100644 frontend/src/Components/ProgressBar.tsx diff --git a/frontend/src/Components/CircularProgressBar.js b/frontend/src/Components/CircularProgressBar.js deleted file mode 100644 index 3af5665a9..000000000 --- a/frontend/src/Components/CircularProgressBar.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './CircularProgressBar.css'; - -class CircularProgressBar extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - progress: 0 - }; - } - - componentDidMount() { - this._progressStep(); - } - - componentDidUpdate(prevProps) { - const progress = this.props.progress; - - if (prevProps.progress !== progress) { - this._cancelProgressStep(); - this._progressStep(); - } - } - - componentWillUnmount() { - this._cancelProgressStep(); - } - - // - // Control - - _progressStep() { - this.requestAnimationFrame = window.requestAnimationFrame(() => { - this.setState({ - progress: this.state.progress + 1 - }, () => { - if (this.state.progress < this.props.progress) { - this._progressStep(); - } - }); - }); - } - - _cancelProgressStep() { - if (this.requestAnimationFrame) { - window.cancelAnimationFrame(this.requestAnimationFrame); - } - } - - // - // Render - - render() { - const { - className, - containerClassName, - size, - strokeWidth, - strokeColor, - showProgressText - } = this.props; - - const progress = this.state.progress; - - const center = size / 2; - const radius = center - strokeWidth; - const circumference = Math.PI * (radius * 2); - const sizeInPixels = `${size}px`; - const strokeDashoffset = ((100 - progress) / 100) * circumference; - const progressText = `${Math.round(progress)}%`; - - return ( -
- - - - - { - showProgressText && -
- {progressText} -
- } -
- ); - } -} - -CircularProgressBar.propTypes = { - className: PropTypes.string, - containerClassName: PropTypes.string, - size: PropTypes.number, - progress: PropTypes.number.isRequired, - strokeWidth: PropTypes.number, - strokeColor: PropTypes.string, - showProgressText: PropTypes.bool -}; - -CircularProgressBar.defaultProps = { - className: styles.circularProgressBar, - containerClassName: styles.circularProgressBarContainer, - size: 60, - strokeWidth: 5, - strokeColor: '#35c5f4', - showProgressText: false -}; - -export default CircularProgressBar; diff --git a/frontend/src/Components/CircularProgressBar.tsx b/frontend/src/Components/CircularProgressBar.tsx new file mode 100644 index 000000000..b14f5fc6a --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import styles from './CircularProgressBar.css'; + +interface CircularProgressBarProps { + className?: string; + containerClassName?: string; + size?: number; + progress: number; + strokeWidth?: number; + strokeColor?: string; + showProgressText?: boolean; +} + +function CircularProgressBar({ + className = styles.circularProgressBar, + containerClassName = styles.circularProgressBarContainer, + size = 60, + strokeWidth = 5, + strokeColor = '#35c5f4', + showProgressText = false, + progress, +}: CircularProgressBarProps) { + const [currentProgress, setCurrentProgress] = useState(0); + const raf = React.useRef(0); + const center = size / 2; + const radius = center - strokeWidth; + const circumference = Math.PI * (radius * 2); + const sizeInPixels = `${size}px`; + const strokeDashoffset = ((100 - currentProgress) / 100) * circumference; + const progressText = `${Math.round(currentProgress)}%`; + + const handleAnimation = useCallback( + (p: number) => { + setCurrentProgress((prevProgress) => { + if (prevProgress < p) { + return prevProgress + Math.min(1, p - prevProgress); + } + + return prevProgress; + }); + }, + [setCurrentProgress] + ); + + useEffect(() => { + if (progress > currentProgress) { + cancelAnimationFrame(raf.current); + + raf.current = requestAnimationFrame(() => handleAnimation(progress)); + } + }, [progress, currentProgress, handleAnimation]); + + useEffect( + () => { + return () => cancelAnimationFrame(raf.current); + }, + // We only want to run this effect once + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( +
+ + + + + {showProgressText && ( +
{progressText}
+ )} +
+ ); +} + +export default CircularProgressBar; diff --git a/frontend/src/Components/ProgressBar.js b/frontend/src/Components/ProgressBar.js deleted file mode 100644 index 171b4c0fa..000000000 --- a/frontend/src/Components/ProgressBar.js +++ /dev/null @@ -1,114 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; -import { kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ProgressBar.css'; - -function ProgressBar(props) { - const { - className, - containerClassName, - title, - progress, - precision, - showText, - text, - kind, - size, - width - } = props; - - const progressPercent = `${progress.toFixed(precision)}%`; - const progressText = text || progressPercent; - const actualWidth = width ? `${width}px` : '100%'; - - return ( - - {(enableColorImpairedMode) => { - return ( -
- { - showText && width ? -
-
-
- {progressText} -
-
-
: - null - } - -
- - { - showText ? -
-
-
- {progressText} -
-
-
: - null - } -
- ); - }} - - ); -} - -ProgressBar.propTypes = { - className: PropTypes.string, - containerClassName: PropTypes.string, - title: PropTypes.string, - progress: PropTypes.number.isRequired, - precision: PropTypes.number.isRequired, - showText: PropTypes.bool.isRequired, - text: PropTypes.string, - kind: PropTypes.oneOf(kinds.all).isRequired, - size: PropTypes.oneOf(sizes.all).isRequired, - width: PropTypes.number -}; - -ProgressBar.defaultProps = { - className: styles.progressBar, - containerClassName: styles.container, - precision: 1, - showText: false, - kind: kinds.PRIMARY, - size: sizes.MEDIUM -}; - -export default ProgressBar; diff --git a/frontend/src/Components/ProgressBar.tsx b/frontend/src/Components/ProgressBar.tsx new file mode 100644 index 000000000..07b20d8a4 --- /dev/null +++ b/frontend/src/Components/ProgressBar.tsx @@ -0,0 +1,94 @@ +import classNames from 'classnames'; +import React from 'react'; +import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; +import translate from 'Utilities/String/translate'; +import styles from './ProgressBar.css'; + +interface ProgressBarProps { + className?: string; + containerClassName?: string; + title?: string; + progress: number; + precision?: number; + showText?: boolean; + text?: string; + kind?: Extract; + size?: Extract; + width?: number; +} + +function ProgressBar({ + className = styles.progressBar, + containerClassName = styles.container, + title, + progress, + precision = 1, + showText = false, + text, + kind = 'primary', + size = 'medium', + width, +}: ProgressBarProps) { + const progressPercent = `${progress.toFixed(precision)}%`; + const progressText = text || progressPercent; + const actualWidth = width ? `${width}px` : '100%'; + + return ( + + {(enableColorImpairedMode) => { + return ( +
+ {showText && width ? ( +
+
+
{progressText}
+
+
+ ) : null} + +
+ + {showText ? ( +
+
+
{progressText}
+
+
+ ) : null} +
+ ); + }} + + ); +} + +export default ProgressBar; From 5513d7bc5dd9a57e385afe253a376a30e9b67055 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 12 Sep 2024 19:25:20 +0000 Subject: [PATCH 523/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: FloatStream <1213193613@qq.com> Co-authored-by: Havok Dan Co-authored-by: Kuzmich55 Co-authored-by: Weblate Co-authored-by: fordas Co-authored-by: genoher Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 7 +- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/ru.json | 288 +++++----- .../Localization/Core/zh_CN.json | 514 +++++++++++------- 4 files changed, 466 insertions(+), 346 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 96e88625a..b96baa9f3 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -78,7 +78,7 @@ "Torrents": "Torrents", "Ui": "Interfaz", "Underscore": "Guion bajo", - "UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script", + "UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script", "Warn": "Advertencia", "AutoTagging": "Etiquetado Automático", "AddAutoTag": "Añadir etiqueta automática", @@ -2110,5 +2110,8 @@ "DeleteSelectedCustomFormatsMessageText": "¿Estás seguro que quieres borrar los {count} formato(s) personalizado(s) seleccionado(s)?", "EditSelectedCustomFormats": "Editar formatos personalizados seleccionados", "ManageCustomFormats": "Gestionar formatos personalizados", - "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) seleccionado(s)" + "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) seleccionado(s)", + "LastSearched": "Último buscado", + "CustomFormatsSpecificationExceptLanguageHelpText": "Coincide si cualquier idioma distinto del seleccionado está presente", + "CustomFormatsSpecificationExceptLanguage": "Excepto idioma" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 85ea050dd..5eded8dff 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2110,5 +2110,6 @@ "EditSelectedCustomFormats": "Editar formatos personalizados selecionados", "ManageCustomFormats": "Gerenciar formatos personalizados", "NoCustomFormatsFound": "Nenhum formato personalizado encontrado", - "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)" + "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)", + "LastSearched": "Última Pesquisa" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 6a3971c60..88209a861 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1,16 +1,16 @@ { - "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {length} символов. Вы можете сделать это через настройки или файл конфигурации", + "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {length} символов в длину. Вы можете сделать это через настройки или файл конфигурации", "DownloadClientSortingHealthCheckMessage": "В клиенте загрузки {downloadClientName} включена сортировка {sortingMode} для категории {appName}. Вам следует отключить сортировку в вашем клиенте загрузки, чтобы избежать проблем с импортом.", "IndexerJackettAllHealthCheckMessage": "Используется не поддерживаемый в Jackett конечный параметр 'all' в индексаторе: {indexerNames}", "IndexerSearchNoAutomaticHealthCheckMessage": "Нет доступных индексаторов с включенным автоматическим поиском, {appName} не будет предоставлять результаты автоматического поиска", "Added": "Добавлено", - "AppDataLocationHealthCheckMessage": "Обновление будет не возможно, во избежание удаления данных программы во время обновления", + "AppDataLocationHealthCheckMessage": "Обновление не позволит сохранить AppData при обновлении", "ApplyChanges": "Применить изменения", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Ни один загрузчик не доступен", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Невозможно связаться с {downloadClientName}. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "Клиент загрузки {downloadClientName} помещает загрузки в корневую папку {rootFolderPath}. Вы не должны загружать в корневую папку.", - "DownloadClientStatusAllClientHealthCheckMessage": "Все клиенты загрузки недоступны из-за сбоев", - "DownloadClientStatusSingleClientHealthCheckMessage": "Клиенты для скачивания недоступны из-за ошибок: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Все клиенты загрузок недоступны из-за ошибок", + "DownloadClientStatusSingleClientHealthCheckMessage": "Клиенты загрузок недоступны из-за ошибок: {downloadClientNames}", "EditSelectedDownloadClients": "Редактировать выбранные клиенты загрузки", "EditSelectedImportLists": "Редактировать выбранные списки импорта", "EditSeries": "Редактировать серию", @@ -25,13 +25,13 @@ "ImportListStatusUnavailableHealthCheckMessage": "Листы недоступны из-за ошибок: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Включить обработку завершенной загрузки, если это возможно", "ImportMechanismHandlingDisabledHealthCheckMessage": "Включить обработку завершенных скачиваний", - "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок более 6 часов", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок более 6 часов: {indexerNames}", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Все RSS индексаторы временно выключены из-за ошибок", "IndexerRssNoIndexersEnabledHealthCheckMessage": "Нет доступных индексаторов с включенной синхронизацией RSS, {appName} не будет автоматически получать новые выпуски", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Все индексаторы с возможностью поиска временно выключены из-за ошибок", "DeleteSelectedImportListsMessageText": "Вы уверены, что хотите удалить {count} выбранных списков импорта?", - "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить {count} выбранных индексатора?", + "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить выбранные индексаторы: {count}?", "EditConditionImplementation": "Редактировать условие - {implementationName}", "EditImportListImplementation": "Редактировать импорт лист - {implementationName}", "Implementation": "Реализация", @@ -39,11 +39,11 @@ "ManageClients": "Управление клиентами", "ManageIndexers": "Управление индексаторами", "MoveAutomatically": "Перемещать автоматически", - "NoDownloadClientsFound": "Клиенты для загрузки не найдены", - "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {notificationNames}", - "CountIndexersSelected": "{count} выбранных индексаторов", + "NoDownloadClientsFound": "Клиенты загрузки не найдены", + "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за ошибок: {notificationNames}", + "CountIndexersSelected": "Выбрано индексаторов: {count}", "EditAutoTag": "Редактировать автоматическую маркировку", - "ManageDownloadClients": "Менеджер клиентов загрузки", + "ManageDownloadClients": "Управление клиентами загрузки", "ManageImportLists": "Управление списками импорта", "ManageLists": "Управление листами", "DeleteImportListMessageText": "Вы уверены, что хотите удалить список '{name}'?", @@ -54,7 +54,7 @@ "DeleteAutoTag": "Удалить автоматическую маркировку", "DeleteAutoTagHelpText": "Вы уверены, что хотите удалить автоматическую метку '{name}'?", "DeleteImportList": "Удалить список импорта", - "DeleteSelectedDownloadClientsMessageText": "Вы уверены, что хотите удалить {count} выбранных клиента загрузки?", + "DeleteSelectedDownloadClientsMessageText": "Вы уверены, что хотите удалить выбранные клиенты загрузки: {count}?", "DeleteRootFolder": "Удалить корневую папку", "DeleteRootFolderMessageText": "Вы уверены, что хотите удалить корневую папку '{path}'?", "DeleteSelectedImportLists": "Удалить списки импорта", @@ -67,9 +67,9 @@ "NoImportListsFound": "Списки импорта не найдены", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Минимальная оценка пользовательского формата", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Минимальная оценка пользовательского формата, необходимая для обхода задержки для предпочитаемого протокола", - "CountDownloadClientsSelected": "{count} выбранных клиентов загрузки", + "CountDownloadClientsSelected": "Выбрано клиентов загрузки: {count}", "ImportUsingScript": "Импорт с помощью скрипта", - "NotificationStatusAllClientHealthCheckMessage": "Все уведомления недоступны из-за сбоев", + "NotificationStatusAllClientHealthCheckMessage": "Все уведомления недоступны из-за ошибок", "EditDownloadClientImplementation": "Редактировать клиент загрузки - {implementationName}", "EditIndexerImplementation": "Редактировать индексатор - {implementationName}", "SelectFolderModalTitle": "{modalTitle} - Выберите директорию", @@ -90,8 +90,8 @@ "RemoveTagsAutomatically": "Автоматическое удаление тегов", "RemoveTagsAutomaticallyHelpText": "Автоматически удалять теги, если условия не выполняются", "DeleteSelectedIndexers": "Удалить индексатор(ы)", - "DeleteTagMessageText": "Вы уверены, что хотите удалить тэг '{label}'?", - "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить Ваш API ключ?", + "DeleteTagMessageText": "Вы уверены, что хотите удалить тег '{label}'?", + "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить ключ API?", "ResetDefinitionTitlesHelpText": "Сбросить названия определений, а также значения", "Socks4": "Socks4 прокси", "ManualGrab": "Ручной захват", @@ -103,12 +103,12 @@ "ParseModalUnableToParse": "Невозможно распознать данное название, попробуйте еще раз.", "QualitiesLoadError": "Невозможно загрузить параметры качества", "ReleaseHash": "Очистить хэш", - "SelectDownloadClientModalTitle": "{modalTitle} - Выберите клиент для загрузки", + "SelectDownloadClientModalTitle": "{modalTitle} - Выберите клиент загрузки", "SelectDropdown": "Выбрать...", "False": "Неверно", "InteractiveImportLoadError": "Невозможно загрузить элементы ручного импорта", "InteractiveImportNoFilesFound": "В выбранной папке не найдено видеофайлов", - "ApplyTagsHelpTextAdd": "Добавить: Добавьте теги в существующий список тегов", + "ApplyTagsHelpTextAdd": "Добавить: Добавить теги к существующему списку тегов", "ApplyTagsHelpTextHowToApplyIndexers": "Как применить теги к выбранным индексаторам", "CalendarOptions": "Настройки календаря", "RootFolderPath": "Путь к корневой папке", @@ -126,18 +126,18 @@ "UnableToLoadAutoTagging": "Не удается загрузить автоматическую маркировку", "Discord": "Discord", "Docker": "Docker", - "OnHealthRestored": "При восстановлении работоспособности", + "OnHealthRestored": "При восстановлении состояния", "PrioritySettings": "Приоритет: {priority}", "File": "Файл", - "ApplyTagsHelpTextReplace": "Заменить: заменить теги введенными тегами (оставьте поле пустым, чтобы удалить все теги)", - "ApplyTagsHelpTextRemove": "Удалить: удалить введенные теги", + "ApplyTagsHelpTextReplace": "Заменить: Заменить теги введёнными тегами (оставьте поле пустым, чтобы удалить все теги)", + "ApplyTagsHelpTextRemove": "Удалить: Удалить введённые теги", "OnManualInteractionRequired": "Требуется ручное управление", "OverrideAndAddToDownloadQueue": "Замена и добавление в очередь загрузки", "Default": "По умолчанию", "OverrideGrabModalTitle": "Переопределить и захватить - {title}", "DeleteBackupMessageText": "Вы уверены, что хотите удалить резервную копию '{name}'?", "DeleteImportListExclusionMessageText": "Вы уверены, что хотите удалить это исключение из списка импорта?", - "DownloadClientsLoadError": "Невозможно загрузить клиенты загрузки", + "DownloadClientsLoadError": "Не удалось загрузить клиенты загрузки", "DownloadWarning": "Предупреждения по скачиванию: {warningMessage}", "InteractiveImportNoImportMode": "Необходимо выбрать режим импорта", "InteractiveImportNoQuality": "Качество должно быть выбрано для каждого выбранного файла", @@ -151,14 +151,14 @@ "Activity": "Активность", "Add": "Добавить", "Actions": "Действия", - "About": "Об", + "About": "О программе", "AddConnection": "Добавить подключение", "AddConditionError": "Невозможно добавить новое условие, попробуйте еще раз.", "AddCustomFormat": "Добавить свой формат", "AddCondition": "Добавить условие", "AddCustomFormatError": "Невозможно добавить новый пользовательский формат, попробуйте ещё раз.", "AddDelayProfile": "Добавить профиль задержки", - "AddDownloadClient": "Добавить программу для скачивания", + "AddDownloadClient": "Добавить клиент загрузки", "AddDownloadClientError": "Не удалось добавить новый клиент загрузки, попробуйте еще раз.", "AddConnectionImplementation": "Добавить подключение - {implementationName}", "AddCustomFilter": "Добавить специальный фильтр", @@ -199,8 +199,8 @@ "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", - "AnalyticsEnabledHelpText": "Отправлять в {appName} анонимную информацию об использовании и ошибках. Анонимная статистика включает в себя информацию о браузере, какие страницы веб-интерфейса {appName} загружены, сообщения об ошибках, а также операционной системе. Мы используем эту информацию для выявления ошибок, а также для разработки нового функционала.", - "AppDataDirectory": "Директория AppData", + "AnalyticsEnabledHelpText": "Отправлять анонимную информацию об использовании и ошибках на серверы {appName}. Анонимная статистика включает в себя информацию о вашем браузере, какие страницы веб-интерфейса {appName} вы используете, отчёты об ошибках, а также версию операционной системы и среды выполнения. Мы будем использовать эту информацию для разработки нового функционала и исправления ошибок.", + "AppDataDirectory": "Каталог AppData", "AddANewPath": "Добавить новый путь", "CustomFormatsLoadError": "Невозможно загрузить Специальные Форматы", "CustomFormatsSpecificationLanguage": "Язык", @@ -218,14 +218,14 @@ "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку.", "Blocklist": "Черный список", "Connect": "Подключить", - "Username": "Пользователь", + "Username": "Имя пользователя", "View": "Просмотр", "EpisodeFileMissingTooltip": "Файл эпизода отсутствует", - "DownloadClientAriaSettingsDirectoryHelpText": "Опциональное местоположение для загрузок. Оставьте пустым, чтобы использовать местоположение Aria2 по умолчанию", + "DownloadClientAriaSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Aria2 по умолчанию", "DownloadClientDownloadStationValidationNoDefaultDestination": "Нет пункта назначения по умолчанию", "DownloadClientFreeboxApiError": "API Freebox вернул ошибку: {errorDescription}", "DownloadClientFreeboxNotLoggedIn": "Не авторизован", - "DownloadClientFreeboxSettingsAppIdHelpText": "Идентификатор приложения, указанный при создании доступа к Freebox API (т. е. 'app_id')", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID приложения, полученный при создании доступа к Freebox API (например, 'app_id')", "Episode": "Эпизод", "DownloadClientNzbgetValidationKeepHistoryOverMax": "Для параметра NzbGet KeepHistory должно быть меньше 25000", "UpgradesAllowedHelpText": "Если отключено, то качества не будут обновляться", @@ -233,24 +233,24 @@ "EpisodeTitleRequired": "Требуется название эпизода", "DownloadClientValidationTestTorrents": "Не удалось получить список торрентов: {exceptionMessage}", "Downloaded": "Скачано", - "AddingTag": "Добавить тэг", + "AddingTag": "Добавление тега", "EnableAutomaticAdd": "Включить автоматическое добавление", "EpisodeTitle": "Название эпизода", "DeleteSelectedEpisodeFilesHelpText": "Вы уверены, что хотите удалить выбранные файлы эпизода?", "Calendar": "Календарь", "CloneProfile": "Клонировать профиль", - "Ended": "Завершен", + "Ended": "Завершён", "Download": "Скачать", - "DownloadClient": "Загрузочный клиент", + "DownloadClient": "Клиент загрузки", "Donate": "Пожертвовать", "AnalyseVideoFiles": "Анализировать видео файлы", "Analytics": "Аналитика", "Anime": "Аниме", "Any": "Любой", - "AppUpdated": "{appName} обновлен", + "AppUpdated": "{appName} обновлён", "AppUpdatedVersion": "Приложение {appName} обновлено до версии `{version}`. Чтобы получить последние изменения, вам необходимо перезагрузить приложение {appName} ", "ApplyTagsHelpTextHowToApplyImportLists": "Как применить теги к выбранным спискам импорта", - "CancelPendingTask": "Вы уверены, что хотите убрать данную задачу из очереди?", + "CancelPendingTask": "Вы уверены, что хотите отменить эту ожидающую задачу?", "CancelProcessing": "Отменить обработку", "CertificateValidationHelpText": "Измените строгую проверку сертификации HTTPS. Не меняйте, если вы не понимаете риски.", "Certification": "Возрастной рейтинг", @@ -263,15 +263,15 @@ "UpdaterLogFiles": "Фалы журналов обновления", "Updates": "Обновления", "UpgradeUntil": "Обновить до качества", - "UrlBaseHelpText": "Для поддержки обратного прокси, по умолчанию пусто", + "UrlBaseHelpText": "Для поддержки обратного прокси, значение по умолчанию - пустая строка", "AbsoluteEpisodeNumbers": "Абсолютные номера эпизодов", "AddReleaseProfile": "Добавить профиль выпуска", "AddRemotePathMapping": "Добавить удаленный путь", "AirDate": "Дата выхода в эфир", "AnimeEpisodeFormat": "Формат аниме-эпизода", - "AuthBasic": "Базовый (всплывающее окно браузера)", + "AuthBasic": "Базовый (Всплывающее окно браузера)", "AuthForm": "Формы (Страница авторизации)", - "Authentication": "Аутентификация", + "Authentication": "Авторизация", "AuthenticationRequired": "Требуется авторизация", "BackupIntervalHelpText": "Периодичность автоматического резервного копирования", "BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены", @@ -293,9 +293,9 @@ "DeleteReleaseProfile": "Удалить профиль релиза", "DeleteSpecificationHelpText": "Вы уверены, что хотите удалить уведомление '{name}'?", "ChooseAnotherFolder": "Выбрать другой каталог", - "DownloadClients": "Клиенты для скачивания", + "DownloadClients": "Клиенты загрузки", "DownloadPropersAndRepacks": "Проперы и репаки", - "Edit": "Изменить", + "Edit": "Редактировать", "Duration": "Длительность", "EnableColorImpairedMode": "Включить режим для слабовидящих", "EnableProfileHelpText": "Установите флажок, чтобы включить профиль релиза", @@ -308,7 +308,7 @@ "ConnectionSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {connectionName}, например {url}", "CopyToClipboard": "Копировать в буфер обмена", "Custom": "Настраиваемый", - "CustomFilter": "Настраиваемые фильтры", + "CustomFilter": "Настраиваемый фильтр", "DeleteEpisodeFromDisk": "Удалить эпизод с диска", "CustomFormatsSettingsSummary": "Пользовательские форматы и настройки", "DeleteSeriesFolder": "Удалить папку сериала", @@ -321,7 +321,7 @@ "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Вы должны войти в свою Diskstation как {username} и вручную настроить ее в настройках DownloadStation в разделе BT/HTTP/FTP/NZB -> Местоположение.", "DownloadClientFloodSettingsPostImportTagsHelpText": "Добавить теги после импорта загрузки.", "DownloadClientFreeboxSettingsApiUrlHelpText": "Определите базовый URL-адрес Freebox API с версией API, например '{url}', по умолчанию — '{defaultApiUrl}'", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Первый и последний Первый", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Первое и последнее сначала", "DownloadClientQbittorrentValidationCategoryAddFailure": "Не удалось настроить категорию", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "Пользователю {appName} не удалось добавить метку в qBittorrent.", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категории не поддерживаются до версии qBittorrent 3.3.0. Пожалуйста, обновите версию или повторите попытку, указав пустую категорию.", @@ -329,7 +329,7 @@ "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent настроен на удаление торрентов, когда они достигают предельного рейтинга (Ratio)", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Отключите опцию «Проверить перед загрузкой» в Sabnbzd", "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Вы должны отключить сортировку фильмов для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог Transmission по умолчанию", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Transmission по умолчанию", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу RPC {clientName}, например {url}, по умолчанию — '{defaultUrl}'", "EditQualityProfile": "Редактировать профиль качества", "DownloadClientValidationAuthenticationFailure": "Ошибка аутентификации", @@ -340,14 +340,14 @@ "DownloadFailed": "Неудачное скачивание", "DownloadStationStatusExtracting": "Извлечение: {progress}%", "EditCustomFormat": "Редактировать пользовательский формат", - "EditConnectionImplementation": "Добавить соединение - {implementationName}", + "EditConnectionImplementation": "Редактировать соединение - {implementationName}", "EditMetadata": "Редактировать метаданные {metadataType}", "EditRemotePathMapping": "Редактировать расположение подключенной папки", "EnableInteractiveSearchHelpTextWarning": "Поиск не поддерживается с этим индексатором", "EpisodeFileDeleted": "Файл эпизода удален", "EpisodeCount": "Количество эпизодов", "UpgradeUntilThisQualityIsMetOrExceeded": "Обновлять, пока это качество не будет достигнуто или превышено", - "AuthenticationRequiredWarning": "Чтобы предотвратить удаленный доступ без авторизации, {appName} теперь требует, чтобы авторизация была включена. При желании вы можете отключить авторизацию с локальных адресов.", + "AuthenticationRequiredWarning": "Чтобы предотвратить удалённый доступ без авторизации, {appName} теперь требует включения авторизации. Вы можете опционально отключить авторизацию для локальных адресов.", "AutoTagging": "Автоматическая маркировка", "AutoTaggingLoadError": "Не удается загрузить автоматическую маркировку", "AutoTaggingRequiredHelpText": "Это условие {implementationName} должно соответствовать правилу автоматической пометки. В противном случае достаточно одного совпадения {implementationName}.", @@ -389,7 +389,7 @@ "DownloadClientDelugeTorrentStateError": "Deluge сообщает об ошибке", "DownloadClientDelugeValidationLabelPluginInactive": "Плагин меток не активирован", "DownloadClientDownloadStationProviderMessage": "Приложение {appName} не может подключиться к Download Station, если в вашей учетной записи DSM включена двухфакторная аутентификация", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "Опциональная общая папка для размещения загрузок. Оставьте пустым, чтобы использовать каталог Download Station по умолчанию", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Необязательный общий каталог для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Download Station по умолчанию", "DownloadClientDownloadStationValidationApiVersion": "Версия Download Station API не поддерживается. Она должна быть не ниже {requiredVersion}. Поддерживается от {minVersion} до {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "Папка не существует", "DownloadClientDownloadStationValidationFolderMissingDetail": "Папка '{downloadDir}' не существует, ее необходимо создать вручную внутри общей папки '{sharedFolder}'.", @@ -397,22 +397,22 @@ "DownloadClientFloodSettingsAdditionalTags": "Дополнительные теги", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавляет свойства мультимедиа в виде тегов. Подсказки являются примерами.", "DownloadClientFloodSettingsStartOnAdd": "Начать добавление", - "DownloadClientFloodSettingsUrlBaseHelpText": "Добавляет префикс к Flood API, например {url}", + "DownloadClientFloodSettingsUrlBaseHelpText": "Добавляет префикс к Flood API, такой как {url}", "DownloadClientFreeboxAuthenticationError": "Не удалось выполнить аутентификацию в API Freebox. Причина: {errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "URL-адрес API", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Токен приложения, полученный при создании доступа к API Freebox (т. е. 'app_token')", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Токен приложения, полученный при создании доступа к Freebox API (например, 'app_token')", "DownloadClientFreeboxSettingsPortHelpText": "Порт, используемый для доступа к интерфейсу Freebox, по умолчанию — '{port}'", "DownloadClientFreeboxUnableToReachFreebox": "Невозможно получить доступ к API Freebox. Проверьте настройки «Хост», «Порт» или «Использовать SSL». (Ошибка: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "Загрузка содержит несколько файлов и находится не в папке задания: {outputPath}", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Для этой опции требуется как минимум NzbGet версии 16.0", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Для работы этого параметра требуется версия NzbGet не ниже 16.0", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Для параметра NzbGet KeepHistory установлено слишком высокое значение.", "DownloadClientNzbgetValidationKeepHistoryZero": "Параметр NzbGet KeepHistory должен быть больше 0", "DownloadClientOptionsLoadError": "Не удалось загрузить параметры клиента загрузки", - "DownloadClientPneumaticSettingsNzbFolderHelpText": "Эта папка должна быть доступна из XBMC", - "DownloadClientPneumaticSettingsStrmFolderHelpText": "Файлы .strm в этой папке будут импортированы дроном", - "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Использовать ли настроенный макет контента qBittorrent, исходный макет из торрента или всегда создавать подпапку (qBittorrent 4.3.2+)", - "DownloadClientQbittorrentSettingsSequentialOrder": "Последовательный порядок", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Используйте безопасное соединение. См. «Параметры» -> «Веб-интерфейс» -> «Использовать HTTPS вместо HTTP» в qBittorrent.", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Этот каталог должен быть доступен из XBMC", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Файлы .strm в этом каталоге будут импортированы дроном", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Выбрать расположение контента: настроенное в qBittorrent, исходный макет из торрента или всегда создавать подкаталог (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Загружать последовательно", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Использовать защищённое соединение. Смотрите 'Параметры' -> 'Веб-интерфейс' -> 'Использовать HTTPS вместо HTTP' в qBittorrent.", "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent не может разрешить магнитную ссылку с отключенным DHT", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent загружает метаданные", "DownloadClientQbittorrentTorrentStatePathError": "Невозможно импортировать. Путь соответствует базовому каталогу загрузки клиента, возможно, для этого торрента отключен параметр «Сохранить папку верхнего уровня» или для параметра «Макет содержимого торрента» НЕ установлено значение «Исходный» или «Создать подпапку»?", @@ -423,7 +423,7 @@ "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} может не поддерживать новые функции, добавленные в SABnzbd при запуске разрабатываемых версий.", "DownloadClientSabnzbdValidationEnableJobFolders": "Включить папки заданий", "DownloadClientSeriesTagHelpText": "Используйте этот загрузочный клиент только для сериалов, имеющих хотя бы один соответствующий тег. Оставьте поле пустым, чтобы использовать его для всех серий.", - "DownloadClientSettings": "Настройки клиента скачиваний", + "DownloadClientSettings": "Настройки клиента загрузки", "DownloadClientValidationApiKeyIncorrect": "Ключ API неверен", "DownloadClientValidationCategoryMissingDetail": "Введенная вами категория не существует в {clientName}. Сначала создайте ее в {clientName}.", "DownloadClientValidationErrorVersion": "Версия {clientName} должна быть не ниже {requiredVersion}. Сообщенная версия: {reportedVersion}", @@ -435,7 +435,7 @@ "EditImportListExclusion": "Редактировать лист исключения для импорта", "EditListExclusion": "Изменить список исключений", "EnableAutomaticAddSeriesHelpText": "Добавляйте сериалы из этого списка в {appName}, когда синхронизация выполняется через пользовательский интерфейс или с помощью {appName}", - "EnableAutomaticSearchHelpText": "Будет использовано для автоматических поисков через интерфейс или {appName}", + "EnableAutomaticSearchHelpText": "Будет использовано при автоматическом поиске через интерфейс или {appName}", "EndedOnly": "Только завершенные", "EpisodeFileDeletedTooltip": "Файл эпизода удален", "EpisodeHistoryLoadError": "Не удалось загрузить историю эпизодов", @@ -448,7 +448,7 @@ "EpisodeNumbers": "Номер(а) эпизодов", "EpisodeSearchResultsLoadError": "Невозможно загрузить результаты для этого поискового запроса. Повторите попытку позже", "Episodes": "Эпизоды", - "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Скачать в последовательном порядке (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Загружать последовательно (qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsContentLayout": "Макет контента", "DownloadClientQbittorrentValidationCategoryUnsupported": "Категория не поддерживается", "DownloadClientValidationGroupMissingDetail": "Введенная вами группа не существует в {clientName}. Сначала создайте ее в {clientName}.", @@ -465,11 +465,11 @@ "AudioInfo": "Информация о аудио", "ChmodFolderHelpText": "Восьмеричный, применяется при импорте / переименовании к медиа-папкам и файлам (без битов выполнения)", "Close": "Закрыть", - "AuthenticationRequiredHelpText": "Отредактируйте, для каких запросов требуется аутентификация. Не меняйте, пока не поймете все риски.", + "AuthenticationRequiredHelpText": "Отредактируйте, для каких запросов требуется авторизация. Не изменяйте, если не понимаете риски.", "Day": "День", - "UpdateStartupNotWritableHealthCheckMessage": "Невозможно установить обновление так как загрузочная папка '{startupFolder}' недоступна для записи для пользователя '{userName}'.", + "UpdateStartupNotWritableHealthCheckMessage": "Невозможно установить обновление, так как каталог автозагрузки '{startupFolder}' недоступен для записи для пользователя '{userName}'.", "UpdateUiNotWritableHealthCheckMessage": "Невозможно установить обновление так как UI папка '{uiFolder}' недоступна для записи для пользователя '{userName}'.", - "UpdateStartupTranslocationHealthCheckMessage": "Не удается установить обновление, поскольку папка автозагрузки \"{startupFolder}\" находится в папке перемещения приложений.", + "UpdateStartupTranslocationHealthCheckMessage": "Невозможно установить обновление, так как каталог автозагрузки '{startupFolder}' находится в каталоге перемещения приложений.", "Uptime": "Время работы", "Uppercase": "Верхний регистр", "UpgradeUntilCustomFormatScoreEpisodeHelpText": "{appName} перестанет скачивать фильмы после достижения указанного количества очков", @@ -487,7 +487,7 @@ "WaitingToImport": "Ожидание импорта", "VisitTheWikiForMoreDetails": "Перейти в wiki: ", "Continuing": "Продолжается", - "BlackholeFolderHelpText": "Папка, в которой {appName} будет хранить файл {extension}", + "BlackholeFolderHelpText": "Каталог, в котором {appName} будет хранить файл {extension}", "BlackholeWatchFolder": "Смотреть папку", "Category": "Категория", "ChangeFileDateHelpText": "Заменить дату файла при импорте/сканировании", @@ -501,7 +501,7 @@ "Cutoff": "Прекращение", "DeleteSelectedSeries": "Удалить выбранный сериал", "DeleteSeriesModalHeader": "Удалить - {title}", - "DeleteTag": "Удалить тэг", + "DeleteTag": "Удалить тег", "CutoffUnmet": "Порог невыполнен", "Dates": "Даты", "DefaultCase": "Случай по умолчанию", @@ -517,8 +517,8 @@ "DownloadClientValidationUnknownException": "Неизвестное исключение: {exception}", "Agenda": "План", "Apply": "Применить", - "ApplyTags": "Применить тэги", - "DownloadClientSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {clientName}, например {url}", + "ApplyTags": "Применить теги", + "DownloadClientSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {clientName}, например, {url}", "UpgradeUntilEpisodeHelpText": "{appName} перестанет скачивать фильмы после достижения указанного качества", "UseProxy": "Использовать прокси", "AddNewSeriesHelpText": "Добавить новый сериал очень просто! Начни печатать название сериала, который хочешь добавить.", @@ -534,7 +534,7 @@ "UseSeasonFolder": "Использовать папку сезона", "AutoTaggingSpecificationTag": "Тэг", "BlocklistReleaseHelpText": "Блокирует повторную загрузку этого релиза пользователем {appName} через RSS или автоматический поиск", - "BuiltIn": "Встроено", + "BuiltIn": "Встроенный", "BypassProxyForLocalAddresses": "Обход прокси для локальных адресов", "ChangeCategory": "Изменить категорию", "Clear": "Очистить", @@ -576,7 +576,7 @@ "CustomFormatsSpecificationRegularExpressionHelpText": "RegEx пользовательского формата не чувствителен к регистру", "Date": "Дата", "DelayProfileProtocol": "Протокол: {preferredProtocol}", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Сначала скачайте первую и последнюю части (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Загружать первые и последние части сначала (qBittorrent 4.1.0+)", "DownloadClientValidationVerifySslDetail": "Проверьте конфигурацию SSL на {clientName} и {appName}", "EnableSsl": "Включить SSL", "Enable": "Включить", @@ -594,32 +594,32 @@ "CalendarLegendEpisodeMissingTooltip": "Эпизод вышел в эфир и отсутствует на диске", "CalendarLegendEpisodeOnAirTooltip": "Эпизод сейчас в эфире", "CalendarLegendEpisodeUnairedTooltip": "Эпизод еще не вышел в эфир", - "Cancel": "Отменить", + "Cancel": "Отмена", "Destination": "Место назначения", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. в {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent.", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Отключить сортировку фильмов", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Отключить сортировку дат", "DownloadClientUTorrentTorrentStateError": "uTorrent сообщает об ошибке", - "DownloadClientPneumaticSettingsNzbFolder": "Nzb папка", - "DownloadClientPneumaticSettingsStrmFolder": "Strm папка", - "DownloadClientFreeboxSettingsHostHelpText": "Имя хоста или IP-адрес хоста Freebox, по умолчанию — '{url}' (будет работать только в той же сети)", - "DownloadClientSettingsDestinationHelpText": "Вручную указывает место назначения загрузки. Оставьте поле пустым, чтобы использовать значение по умолчанию", + "DownloadClientPneumaticSettingsNzbFolder": "Каталог NZB", + "DownloadClientPneumaticSettingsStrmFolder": "Каталог STRM", + "DownloadClientFreeboxSettingsHostHelpText": "Имя хоста или IP-адрес хоста Freebox, по умолчанию — '{url}' (будет работать только если находится в одной сети)", + "DownloadClientSettingsDestinationHelpText": "Ручная установка места загрузки, оставьте пустым для использования значения по умолчанию", "Downloading": "Скачивается", - "DownloadClientSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в {clientName}", + "DownloadClientSettingsInitialStateHelpText": "Начальное состояние для торрентов, добавленных в {clientName}", "Warn": "Предупреждение", "CustomFormatsSettingsTriggerInfo": "Пользовательский формат будет применен к релизу или файлу, если он соответствует хотя бы одному из каждого из выбранных типов условий.", "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не будет пытаться импортировать завершенные загрузки без категории.", "DownloadClientRTorrentSettingsUrlPath": "URL-путь", - "DownloadClientSettingsUseSslHelpText": "Использовать безопасное соединение при подключении к {clientName}", + "DownloadClientSettingsUseSslHelpText": "Использовать защищённое соединение при подключении к {clientName}", "DownloadClientSettingsCategorySubFolderHelpText": "Добавление категории, специфичной для {appName}, позволяет избежать конфликтов с несвязанными загрузками, не относящимися к {appName}. Использование категории не является обязательным, но настоятельно рекомендуется. Создает подкаталог [category] в выходном каталоге.", "DownloadClientSabnzbdValidationUnknownVersion": "Неизвестная версия: {rawVersion}", - "DownloadClientSettingsAddPaused": "Добавить приостановленное", + "DownloadClientSettingsAddPaused": "Добавить приостановленные", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Отключить сортировку сериалов", "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} предпочитает, чтобы каждая загрузка имела отдельную папку. Если к папке/пути добавлен *, Sabnzbd не будет создавать эти папки заданий. Отправляйтесь в Sabnzbd, чтобы исправить это.", "ConnectionLost": "Соединение прервано", "CleanLibraryLevel": "Очистить уровень библиотеки", "ColonReplacementFormatHelpText": "Изменить как {appName} обрабатывает замену двоеточий", - "ConnectionLostReconnect": "{appName} попытается соединиться автоматически или нажмите кнопку внизу.", + "ConnectionLostReconnect": "{appName} попытается соединиться автоматически или нажмите кнопку ниже.", "ConnectSettingsSummary": "Уведомления, подключения к серверам/проигрывателям и настраиваемые скрипты", "ContinuingOnly": "Только в стадии показа", "Connections": "Соединения", @@ -632,7 +632,7 @@ "CutoffUnmetLoadError": "Ошибка при загрузке элементов не выполнивших порог", "DoNotBlocklistHint": "Удалить без внесения в черный список", "DoNotUpgradeAutomatically": "Не обновлять автоматически", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу json deluge, см. {url}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу json Deluge, см.: {url}", "EpisodeInfo": "Информация об эпизоде", "DownloadClientDelugeSettingsDirectory": "Каталог загрузки", "DownloadClientDelugeSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог Deluge по умолчанию", @@ -644,7 +644,7 @@ "BypassDelayIfHighestQualityHelpText": "Игнорирование задержки, когда выпуск имеет максимальное качество в выбранном профиле качества с предпочитаемым протоколом", "UseSsl": "Использовать SSL", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent сообщает об отсутствующих файлах", - "DownloadClientRTorrentSettingsDirectoryHelpText": "Опциональное место для загрузок. Оставьте пустым, чтобы использовать каталог rTorrent по умолчанию", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение rTorrent по умолчанию", "BlocklistAndSearch": "Черный список и поиск", "EnableSslHelpText": "Требуется перезапуск от администратора", "EpisodeDownloaded": "Эпизод скачан", @@ -658,7 +658,7 @@ "AnimeEpisodeTypeDescription": "Эпизоды выпущены с использованием абсолютного номера эпизода", "ApiKey": "API ключ", "AptUpdater": "Используйте apt для установки обновления", - "AuthenticationMethodHelpText": "Необходим логин и пароль для доступа в {appName}", + "AuthenticationMethodHelpText": "Необходимо ввести имя пользователя и пароль для доступа к {appName}", "AutoAdd": "Автоматическое добавление", "AutoTaggingSpecificationOriginalLanguage": "Язык", "AutoTaggingSpecificationQualityProfile": "Профиль качества", @@ -670,7 +670,7 @@ "BlocklistOnly": "Только черный список", "BlocklistOnlyHint": "Черный список без поиска замен", "BrowserReloadRequired": "Требуется перезагрузка браузера", - "DatabaseMigration": "Перенос БД", + "DatabaseMigration": "Миграция базы данных", "CountSelectedFiles": "{selectedCount} выбранные файлы", "CountSeriesSelected": "{count} сериалов выбрано", "CustomFormat": "Настраиваемый формат", @@ -692,7 +692,7 @@ "CustomFormatsSpecificationFlag": "Флаг", "DefaultNameCopiedSpecification": "{name} - Копировать", "Delete": "Удалить", - "DownloadClientFreeboxSettingsAppId": "Идентификатор приложения", + "DownloadClientFreeboxSettingsAppId": "ID приложения", "AddNewSeriesRootFolderHelpText": "Подпапка '{folder}' будет создана автоматически", "AnalyseVideoFilesHelpText": "Извлекать из файлов видео тех. данные. Для этого потребуется, чтобы {appName} прочитал часть файла, что может вызвать высокую активность диска и сети во время сканирования.", "ApplyTagsHelpTextHowToApplyDownloadClients": "Как применить теги к выбранным клиентам загрузки", @@ -710,9 +710,9 @@ "AnEpisodeIsDownloading": "Эпизод загружается", "ConnectionLostToBackend": "{appName} потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", "CustomFormatHelpText": "{appName} оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", - "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите действительный метод аутентификации", + "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите допустимый метод авторизации", "CustomFormatUnknownCondition": "Неизвестное условие пользовательского формата '{implementation}'", - "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это позволяет избежать конфликтов с несвязанными загрузками.", + "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это предотвращает конфликты с несвязанными загрузками.", "DownloadPropersAndRepacksHelpTextCustomFormat": "Используйте 'Не предпочитать' для сортировки по рейтингу пользовательского формата по сравнению с Propers / Repacks", "DownloadPropersAndRepacksHelpTextWarning": "Используйте пользовательский формат для автоматических обновлений до Проперов/Репаков", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматический поиск и попытка загрузки другого релиза, если неудачный релиз был получен из интерактивного поиска", @@ -737,7 +737,7 @@ "DeleteReleaseProfileMessageText": "Вы действительно хотите удалить профиль релиза '{name}'?", "BlocklistAndSearchMultipleHint": "Начать поиск для замены после внесения в черный список", "DeletedReasonEpisodeMissingFromDisk": "{appName} не смог найти файл на диске, поэтому файл был откреплён от эпизода в базе данных", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в qBittorrent. Обратите внимание, что принудительные торренты не подчиняются ограничениям на раздачу", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в qBittorrent. Обратите внимание, что принудительные торренты не соблюдают ограничения на раздачу", "BlocklistRelease": "Релиз из черного списка", "DeletedReasonManual": "Файл был удален с помощью {appName} вручную или с помощью другого инструмента через API", "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", @@ -745,7 +745,7 @@ "DownloadClientDelugeValidationLabelPluginFailureDetail": "Пользователю {appName} не удалось добавить метку к клиенту {clientName}.", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Чтобы использовать категории, у вас должен быть включен плагин меток в {clientName}.", "DownloadClientRTorrentProviderMessage": "rTorrent не будет приостанавливать торренты, если они соответствуют критериям раздачи. {appName} будет обрабатывать автоматическое удаление торрентов на основе текущих критериев раздачи в Настройки->Индексаторы, только если включена опция «Удаление завершенных». После импорта он также установит {importedView} в качестве представления rTorrent, которое можно использовать в сценариях rTorrent для настройки поведения.", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магниты в rTorrent в остановленном состоянии. Это может привести к поломке магнет файлов.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магнет-ссылки в rTorrent в остановленном состоянии. Это может привести к повреждению магнет-файлов.", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Клиент загрузки {downloadClientName} настроен на удаление завершенных загрузок. Это может привести к удалению загрузок из вашего клиента до того, как {appName} сможет их импортировать.", "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Использование функции «Проверка перед загрузкой» влияет на возможность приложения {appName} отслеживать новые загрузки. Также Sabnzbd рекомендует вместо этого «Отменять задания, которые невозможно завершить», поскольку это более эффективно.", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Вы должны отключить сортировку по дате для категории, которую использует {appName}, чтобы избежать проблем с импортом. Отправляйтесь в Sabnzbd, чтобы исправить это.", @@ -753,7 +753,7 @@ "DownloadClientSettingsPostImportCategoryHelpText": "Категория для приложения {appName}, которую необходимо установить после импорта загрузки. {appName} не удалит торренты в этой категории, даже если раздача завершена. Оставьте пустым, чтобы сохранить ту же категорию.", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Приоритет при выборе эпизодов, вышедших в эфир за последние 14 дней", "DayOfWeekAt": "{day} в {time}", - "DeleteDownloadClient": "Удалить программу для скачивания", + "DeleteDownloadClient": "Удалить клиент загрузки", "MetadataSourceSettings": "Настройки источника метаданных", "MetadataSettingsSeriesMetadataUrl": "URL-адрес метаданных сериала", "MetadataSettingsSeriesMetadataEpisodeGuide": "Руководство по эпизодам метаданных сериала", @@ -792,7 +792,7 @@ "ErrorLoadingPage": "Произошла ошибка при загрузке этой страницы", "External": "Внешний", "Never": "Никогда", - "Ok": "Хорошо", + "Ok": "Ок", "UpdateSonarrDirectlyLoadError": "Невозможно обновить {appName} напрямую,", "WeekColumnHeader": "Заголовок столбца недели", "OrganizeModalHeader": "Упорядочить и переименовать", @@ -803,16 +803,16 @@ "NoIssuesWithYourConfiguration": "С вашей конфигурацией нет проблем", "NoLeaveIt": "Нет, оставить", "OnLatestVersion": "Последняя версия {appName} уже установлена", - "OpenBrowserOnStart": "Открывать браузер при запуске", + "OpenBrowserOnStart": "Открыть браузер при запуске", "OrganizeLoadError": "Ошибка при загрузке предпросмотра", "WeekColumnHeaderHelpText": "Отображается над каждым столбцом, когда неделя активна", "Extend": "Продлить", "OptionalName": "Опциональное имя", - "Options": "Опции", + "Options": "Параметры", "ExpandAll": "Развернуть Все", "MoveFiles": "Переместить файлы", "NoMonitoredEpisodes": "В этом сериале нет отслеживаемых эпизодов", - "NotificationTriggersHelpText": "Выберите, какие события должны вызвать это уведомление", + "NotificationTriggersHelpText": "Выберите события, которые должны вызвать это уведомление", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Конфигурационный ключ для решения постоянного хранения. Оставьте пустым, если используются URL-адреса без сохранения состояния.", "NotificationsSignalSettingsPasswordHelpText": "Пароль, используемый для аутентификации запросов к signal-api", "NotificationsSimplepushSettingsEventHelpText": "Настройте поведение push-уведомлений", @@ -843,7 +843,7 @@ "NoChange": "Нет изменений", "NoChanges": "Нет изменений", "NoSeriesHaveBeenAdded": "Вы не добавили никаких сериалов, желаете начать с импорта всех или нескольких сериалов?", - "NoTagsHaveBeenAddedYet": "Теги еще не добавлены", + "NoTagsHaveBeenAddedYet": "Теги ещё не добавлены", "NotificationsAppriseSettingsServerUrlHelpText": "Укажите URL-адрес сервера, включая http(s):// и порт, если необходимо", "NotificationsPushcutSettingsTimeSensitive": "Чувствителен ко времени", "NotificationsPushoverSettingsDevices": "Устройства", @@ -871,7 +871,7 @@ "NotificationsTelegramSettingsBotToken": "Токен бота", "NotificationsTelegramSettingsChatIdHelpText": "Для получения сообщений необходимо начать разговор с ботом или добавить его в свою группу", "NotificationsTelegramSettingsIncludeAppName": "Включить {appName} в заголовок", - "NotificationsTelegramSettingsIncludeAppNameHelpText": "При необходимости добавьте к заголовку сообщения префикс {appName}, чтобы отличать уведомления от разных приложений", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "При необходимости добавить к заголовку сообщения префикс {appName}, чтобы различать уведомления от разных приложений", "NotificationsTelegramSettingsSendSilently": "Отправить молча", "NotificationsTelegramSettingsSendSilentlyHelpText": "Отправляет сообщение молча. Пользователи получат уведомление без звука", "NotificationsTelegramSettingsTopicIdHelpText": "Укажите идентификатор темы, чтобы отправлять уведомления в эту тему. Оставьте пустым, чтобы использовать общую тему (только для супергрупп)", @@ -887,19 +887,19 @@ "OnApplicationUpdate": "При обновлении приложения", "OnEpisodeFileDelete": "При удалении файла эпизода", "OnEpisodeFileDeleteForUpgrade": "При удалении файла эпизода для обновления", - "OnHealthIssue": "О проблемах в системе", + "OnHealthIssue": "При проблемах с состоянием", "OnFileImport": "При импорте файла", "OnSeriesDelete": "При удалении сериала", "OneMinute": "1 минута", "OnImportComplete": "При завершении импорта", "OneSeason": "1 сезон", "OnlyForBulkSeasonReleases": "Только для релизов полных сезонов", - "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления. Вы так же можете установить в Система: Обновления", + "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления Вы по-прежнему сможете выполнить установку из раздела Система: Обновления", "Upcoming": "Предстоящие", - "UnselectAll": "Снять все выделения", + "UnselectAll": "Снять выделение со всех", "UnmonitoredOnly": "Только не отслеживаемые", "UpdateSelected": "Обновление выбрано", - "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который обрабатывает остатки после процесса обновления", + "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который извлекает пакет обновления и обрабатывает оставшуюся часть процесса обновления", "UpdateMonitoring": "Мониторинг обновлений", "UpdateAvailableHealthCheckMessage": "Доступно новое обновление: {version}", "UsenetBlackhole": "Usenet Черная дыра", @@ -907,10 +907,10 @@ "WithFiles": "С файлами", "Wiki": "Wiki", "Week": "Неделя", - "None": "Ничто", + "None": "Ничего", "NotificationTriggers": "Триггеры уведомления", - "UsenetBlackholeNzbFolder": "Nzb папка", - "YesCancel": "Да, отменить", + "UsenetBlackholeNzbFolder": "Каталог NZB", + "YesCancel": "Да, Отмена", "MultiEpisodeStyle": "Стиль для мульти-эпизода", "UpdateFiltered": "Фильтр обновлений", "Year": "Год", @@ -920,8 +920,8 @@ "MonitoringOptions": "Опции отслеживания", "Month": "Месяц", "MonitorNewSeasons": "Следить за новыми сезонами", - "MoreInfo": "Ещё инфо", - "More": "Более", + "MoreInfo": "Больше информации", + "More": "Ещё", "MultiEpisode": "Мульти-эпизод", "MustContainHelpText": "Релиз должен содержать хотя бы одно из этих условий (без учета регистра)", "MustNotContain": "Не должен содержать", @@ -930,7 +930,7 @@ "MoveSeriesFoldersMoveFiles": "Да, перенести файлы", "NoMatchFound": "Совпадений не найдено!", "NoSeasons": "Нет сезонов", - "NextExecution": "Следующее выполнение", + "NextExecution": "Следующий запуск", "NotificationsTelegramSettingsChatId": "Идентификатор чата", "NotificationsSynologyValidationTestFailed": "Не Synology или Synoindex недоступен", "NotificationsTwitterSettingsConnectToTwitter": "Подключиться к Твиттеру / X", @@ -944,11 +944,11 @@ "NotificationsValidationInvalidAccessToken": "Токен доступа недействителен", "NotificationsTwitterSettingsMentionHelpText": "Упоминать этого пользователя в отправленных твитах", "UpdateAll": "Обновить всё", - "UpdateMechanismHelpText": "Используйте встроенную в {appName} функцию обновления или скрипт", + "UpdateMechanismHelpText": "Использовать встроенный инструмент обновления {appName} или скрипт.", "OnRename": "При переименовании", "ErrorLoadingContents": "Ошибка при загрузке контента", "EventType": "Тип события", - "ExistingTag": "Существующий тэг", + "ExistingTag": "Существующий тег", "ErrorLoadingItem": "Произошла ошибка при загрузке этого элемента", "MonitorAllEpisodesDescription": "Следите за всеми эпизодами, кроме специальных", "MonitoredOnly": "Только отслеживаемые", @@ -967,7 +967,7 @@ "NotificationsTwitterSettingsConsumerSecret": "Секрет потребителя", "MonitorNewItems": "Мониторинг новых объектов", "NoSeriesFoundImportOrAdd": "Сериал не найден. Чтобы начать работу, импортируйте существующий или новый сериал.", - "NoLogFiles": "Нет файлов журнала", + "NoLogFiles": "Файлов журнала нет", "NoEventsFound": "Событий не найдено", "NotificationsPushcutSettingsTimeSensitiveHelpText": "Включите, чтобы пометить уведомление как \"чувствительное ко времени\"", "NotificationsPushoverSettingsUserKey": "Пользовательский ключ", @@ -1020,7 +1020,7 @@ "NoBackupsAreAvailable": "Нет резервных копий", "NoDelay": "Без задержки", "NoMonitoredEpisodesSeason": "В этом сезоне нет отслеживаемых эпизодов", - "NoUpdatesAreAvailable": "Нет обновлений", + "NoUpdatesAreAvailable": "Обновления отсутствуют", "NotSeasonPack": "Не полные сезоны", "NotificationsAppriseSettingsServerUrl": "URL-адрес сервера приложений", "SetPermissionsLinuxHelpText": "Следует ли запускать chmod при импорте/переименовании файлов?", @@ -1125,7 +1125,7 @@ "SmartReplaceHint": "Тире или пробел в зависимости от имени", "Sort": "Сортировка", "SourceRelativePath": "Относительный путь источника", - "Source": "Источник", + "Source": "Исходный код", "SourceTitle": "Название источника", "SupportedDownloadClients": "{appName} поддерживает многие популярные торрент и usenet-клиенты для скачивания.", "SupportedImportListsMoreInfo": "Для дополнительной информации по спискам импорта нажмите эту кнопку.", @@ -1150,7 +1150,7 @@ "ToggleUnmonitoredToMonitored": "Не отслеживается, нажмите, чтобы отслеживать", "MatchedToSeason": "Соответствует сезону", "MaximumSizeHelpText": "Максимальный размер загружаемого релиза в МБ. Установите 0, чтобы снять все ограничения", - "LogLevelTraceHelpTextWarning": "Отслеживание журнала следует включать только временно", + "LogLevelTraceHelpTextWarning": "Включение трассировки журнала должно быть временным", "MediaManagementSettingsSummary": "Именование, настройки управления файлами и корневыми папками", "MinimumFreeSpace": "Минимальное свободное место", "Mode": "Режим", @@ -1184,7 +1184,7 @@ "TagCannotBeDeletedWhileInUse": "Невозможно удалить во время использования", "Type": "Тип", "Dash": "Тире", - "Folder": "Папка", + "Folder": "Каталог", "Fixed": "Исправлено", "FullSeason": "Полный сезон", "From": "Из", @@ -1204,8 +1204,8 @@ "Filename": "Имя файла", "FormatShortTimeSpanMinutes": "{minutes} минут(ы)", "FormatShortTimeSpanSeconds": "{seconds} секунд(ы)", - "General": "Основное", - "GeneralSettings": "Основные настройки", + "General": "Общие", + "GeneralSettings": "Общие настройки", "GeneralSettingsLoadError": "Невозможно загрузить основные настройки", "GrabSelected": "Захватить выбранные", "HasMissingSeason": "Отсутствует сезон", @@ -1237,9 +1237,9 @@ "Settings": "Настройки", "StandardEpisodeFormat": "Стандартный формат эпизода", "StartImport": "Начать импорт", - "StartupDirectory": "Каталог автозагрузки", + "StartupDirectory": "Каталог автозапуска", "System": "Система", - "TorrentBlackholeTorrentFolder": "Папка торрента", + "TorrentBlackholeTorrentFolder": "Каталог торрента", "Unavailable": "Недоступно", "Unlimited": "Неограниченно", "IconForFinalesHelpText": "Показывать значок финала сериала/сезона на основе доступной информации об эпизоде", @@ -1252,7 +1252,7 @@ "NotificationsPushcutSettingsNotificationName": "Название уведомления", "LastWriteTime": "Последнее время записи", "LastUsed": "Использовано последний раз", - "IndexerSettingsSeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто: используется значение по умолчанию клиента загрузки", + "IndexerSettingsSeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто — используется значение клиента загрузки по умолчанию", "KeyboardShortcutsOpenModal": "Открыть это модальное окно", "KeyboardShortcutsConfirmModal": "Окно подтверждения", "KeyboardShortcutsCloseModal": "Закрыть текущее окно", @@ -1334,7 +1334,6 @@ "FilterIsBefore": "до", "ImportListsTraktSettingsPopularListTypeTrendingShows": "В тренде", "ImportListsTraktSettingsWatchedListFilterHelpText": "Если тип списка просматривается, выберите тип сериала, который вы хотите импортировать", - "IndexerHDBitsSettingsMediums": "Средний", "FilterDoesNotStartWith": "не начинается с", "ImportListsTraktSettingsUsernameHelpText": "Имя пользователя для списка, из которого нужно импортировать", "ImportListsValidationInvalidApiKey": "Ключ API недействителен", @@ -1373,12 +1372,12 @@ "Permissions": "Разрешения", "HardlinkCopyFiles": "Жесткая ссылка/Копирование файлов", "Here": "здесь", - "Health": "Здоровье", + "Health": "Состояние", "Hostname": "Имя хоста", "Host": "Хост", "IndexerSettingsRssUrlHelpText": "Введите URL-адрес RSS-канала, совместимого с {indexer}", "IndexerSettingsSeasonPackSeedTimeHelpText": "Время, когда торрент сезонного пакета должен быть на раздаче перед остановкой, при пустом значении используется значение по умолчанию клиента загрузки", - "IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пустой использует значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов", + "IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пусто — используется значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов", "IndexerHDBitsSettingsMediumsHelpText": "Если не указано, используются все параметры.", "IndexerSettingsApiUrlHelpText": "Не меняйте это, если вы не знаете, что делаете. Поскольку ваш ключ API будет отправлен на этот хост.", "IndexerSettingsCategoriesHelpText": "Раскрывающийся список. Оставьте пустым, чтобы отключить стандартные/ежедневные шоу", @@ -1404,9 +1403,9 @@ "LocalPath": "Локальный путь", "LocalStorageIsNotSupported": "Локальное хранилище не поддерживается или отключено. Плагин или приватный просмотр могли отключить его.", "LogFilesLocation": "Файлы журнала расположены по адресу: {location}", - "LogLevel": "Уровень журнала", + "LogLevel": "Уровень журналирования", "LogOnly": "Только журнал", - "Logging": "Ведение журнала", + "Logging": "Журналирование", "Logout": "Выйти", "Lowercase": "Нижний регистр", "ManageEpisodes": "Управление эпизодами", @@ -1480,7 +1479,7 @@ "NotificationsPushBulletSettingsDeviceIdsHelpText": "Список идентификаторов устройств (оставьте пустым, чтобы отправить на все устройства)", "NotificationsPushBulletSettingsChannelTagsHelpText": "Список тегов канала для отправки уведомлений", "NotificationsPushcutSettingsNotificationNameHelpText": "Название уведомления на вкладке «Уведомления» приложения Pushcut", - "PendingChangesDiscardChanges": "Не применять изменения и выйти", + "PendingChangesDiscardChanges": "Отменить изменения и выйти", "PortNumber": "Номер порта", "PostImportCategory": "Категория после импорта", "PreferUsenet": "Предпочитать Usenet", @@ -1491,7 +1490,7 @@ "Protocol": "Протокол", "ProxyBadRequestHealthCheckMessage": "Не удалось проверить прокси. Код состояния: {statusCode}", "ProtocolHelpText": "Выберите, какой протокол(ы) использовать и какой из них предпочтительнее при выборе между одинаковыми в остальном релизами", - "ProxyFailedToTestHealthCheckMessage": "Не удалось проверить прокси: {url}", + "ProxyFailedToTestHealthCheckMessage": "Не удалось протестировать прокси: {url}", "QualityDefinitionsLoadError": "Невозможно загрузить определение качества", "QualitiesHelpText": "Качества, стоящие выше в списке, являются более предпочтительными. Качества внутри одной группы равны. Требуются только проверенные качества", "QualityLimitsSeriesRuntimeHelpText": "Ограничения автоматически корректируются в зависимости от продолжительности сериала и количества эпизодов в файле.", @@ -1593,7 +1592,7 @@ "ShowTitle": "Показать название", "SkipFreeSpaceCheck": "Пропустить проверку свободного места", "ShowSizeOnDisk": "Показать размер на диске", - "ShowSearchHelpText": "Показать копку поиска по наведению", + "ShowSearchHelpText": "Показать копку поиска при наведении", "SkipFreeSpaceCheckWhenImportingHelpText": "Используйте, когда {appName} не может обнаружить свободное место в вашей корневой папке во время импорта файлов", "SpecialsFolderFormat": "Формат папки спец. эпизодов", "SmartReplace": "Умная замена", @@ -1604,7 +1603,7 @@ "SupportedIndexers": "{appName} поддерживает любой индексатор, использующий стандарт Newznab, а также другие индексаторы, перечисленные ниже.", "Table": "Таблица", "TableColumnsHelpText": "Выберите, какие столбцы отображаются и в каком порядке", - "TestAllClients": "Тестировать всех клиентов", + "TestAllClients": "Тестировать все клиенты", "TablePageSizeMaximum": "Размер страницы не должен превышать {maximumValue}", "TaskUserAgentTooltip": "User-Agent, представленный приложением, который вызывает API", "TorrentDelayTime": "Задержка торрента: {torrentDelay}", @@ -1616,7 +1615,7 @@ "TvdbId": "Идентификатор TVDB", "Umask755Description": "{octal} - Владелец (запись), остальные (чтение)", "Underscore": "Нижнее подчеркивание", - "UnableToLoadBackups": "Невозможно загрузить резервные копии", + "UnableToLoadBackups": "Не удалось загрузить резервные копии", "UnmappedFilesOnly": "Только несопоставленные файлы", "Unknown": "Неизвестный", "UnknownEventTooltip": "Неизвестное событие", @@ -1665,7 +1664,7 @@ "SizeOnDisk": "Размер на диске", "ShowSearch": "Показать поиск", "SupportedListsSeries": "{appName} поддерживает несколько списков для импорта сериалов в базу данных.", - "StopSelecting": "Прекратить выбор", + "StopSelecting": "Отменить выбор", "Status": "Статус", "SupportedCustomConditions": "{appName} поддерживает настраиваемые условия в соответствии со свойствами релиза, указанными ниже.", "ToggleMonitoredSeriesUnmonitored": "Невозможно переключить отслеживаемое состояние, если сериал не отслеживается", @@ -1704,7 +1703,7 @@ "MetadataSettings": "Настройки метаданных", "NotificationsAppriseSettingsNotificationType": "Тип информирования об уведомлении", "NotificationsEmailSettingsFromAddress": "С адреса", - "NotificationsEmailSettingsUseEncryptionHelpText": "Предпочитать использовать шифрование, если оно настроено на сервере, всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт) или никогда не использовать шифрование", + "NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование.", "NotificationsCustomScriptSettingsProviderMessage": "При тестировании будет выполняться сценарий с типом события, установленным на {eventTypeTest}. Убедитесь, что ваш сценарий обрабатывает это правильно", "NotificationsJoinSettingsApiKeyHelpText": "Ключ API из настроек вашей учетной записи присоединения (нажмите кнопку «Присоединиться к API»).", "NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо", @@ -1713,7 +1712,7 @@ "NotificationsNtfySettingsClickUrlHelpText": "Опциональная ссылка, когда пользователь нажимает уведомление", "PendingDownloadClientUnavailable": "Ожидание – Клиент для загрузки недоступен", "Port": "Порт", - "ProxyBypassFilterHelpText": "Используйте ',' в качестве разделителя и '*.' как подстановочный знак для субдоменов", + "ProxyBypassFilterHelpText": "Используйте ',' как разделитель и '*.' как подстановочный знак для поддоменов", "RefreshSeries": "Обновить сериал", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} не удалось импортировать эпизоды. Проверьте логи для детальной информации.", "SelectReleaseType": "Выберите тип релиза", @@ -1724,8 +1723,8 @@ "SetIndexerFlagsModalTitle": "{modalTitle} — установка флагов индексатора", "Socks5": "Socks5 (Поддержка TOR)", "SomeResultsAreHiddenByTheAppliedFilter": "Некоторые результаты скрыты примененным фильтром", - "TagIsNotUsedAndCanBeDeleted": "Тег не используется и может быть удален", - "TableOptions": "Опции таблицы", + "TagIsNotUsedAndCanBeDeleted": "Тег не используется и может быть удалён", + "TableOptions": "Параметры таблицы", "TablePageSizeHelpText": "Количество элементов, отображаемых на каждой странице", "TablePageSizeMinimum": "Размер страницы должен быть не менее {minimumValue}", "UnmonitorSpecialEpisodes": "Не отслеживать спец. эпизоды", @@ -1760,11 +1759,11 @@ "SeasonPassEpisodesDownloaded": "Скачано эпизодов: {episodeFileCount}/{totalEpisodeCount}", "SeasonNumberToken": "Сезон {seasonNumber}", "SeasonNumber": "Номер сезона", - "Seeders": "Сиды", + "Seeders": "Сидеры", "SeasonPremieresOnly": "Только премьеры сезона", "FailedToLoadUiSettingsFromApi": "Не удалось загрузить настройки пользовательского интерфейса из API", "FailedToLoadTagsFromApi": "Не удалось загрузить теги из API", - "FeatureRequests": "Будущие запросы", + "FeatureRequests": "Запросы функций", "FileManagement": "Управление файлами", "HomePage": "Домашняя страница", "HistoryModalHeaderSeason": "История {season}", @@ -1829,7 +1828,7 @@ "OutputPath": "Выходной путь", "PreferAndUpgrade": "Предпочитать и улучшать", "PreferProtocol": "Предпочитать {preferredProtocol}", - "ProxyUsernameHelpText": "Вам нужно только ввести имя пользователя и пароль, если они необходимы. В противном случае оставьте их пустыми.", + "ProxyUsernameHelpText": "Вы должны указать имя пользователя и пароль только если они необходимы. В противном случае оставьте эти поля пустыми.", "RemoveMultipleFromDownloadClientHint": "Удаляет загрузки и файлы из загрузочного клиента", "FilterEndsWith": "заканчивается", "ImdbId": "Идентификатор IMDB", @@ -1844,7 +1843,7 @@ "MonitorAllSeasons": "Все сезоны", "NotificationsCustomScriptSettingsArgumentsHelpText": "Аргументы для передачи в скрипт", "NotificationsDiscordSettingsAuthor": "Автор", - "PackageVersionInfo": "{packageVersion} от {packageAuthor}", + "PackageVersionInfo": "{packageVersion} создан {packageAuthor}", "Peers": "Пиры", "PreviewRenameSeason": "Предпросмотр переименования этого сезона", "Proxy": "Прокси", @@ -1869,13 +1868,13 @@ "NotificationsCustomScriptValidationFileDoesNotExist": "Файл не существует", "NotificationsPlexSettingsAuthenticateWithPlexTv": "Аутентификация с помощью Plex.tv", "PrefixedRange": "Префиксный диапазон", - "ProxyPasswordHelpText": "Вам нужно только ввести имя пользователя и пароль, если они необходимы. В противном случае оставьте их пустыми.", + "ProxyPasswordHelpText": "Вы должны указать имя пользователя и пароль только если они необходимы. В противном случае оставьте эти поля пустыми.", "Quality": "Качество", "QualityProfilesLoadError": "Невозможно загрузить профили качества", "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Удаленный загрузочный клиент {downloadClientName} размещает загрузки в {path}, но этот каталог не существует. Вероятно, отсутствует или неправильно указан удаленный путь.", "RestartNow": "Перезапустить сейчас", "ResetTitles": "Сбросить заголовки", - "RestoreBackup": "Восстановить из резервной копии", + "RestoreBackup": "Восстановить резервную копию", "RestartSonarr": "Перезапустить {appName}", "RestartRequiredWindowsService": "В зависимости от того, какой пользователь запускает службу {appName}, вам может потребоваться один раз перезапустить {appName} от имени администратора, прежде чем служба запустится автоматически.", "ShowEpisodeInformationHelpText": "Показать название и номер эпизода", @@ -1952,7 +1951,7 @@ "ImportListsTraktSettingsRating": "Рейтинг", "ImportListsTraktSettingsRatingHelpText": "Фильтровать сериалы по диапазону рейтингов (0–100)", "NotificationsDiscordSettingsAvatarHelpText": "Измените аватар, который используется для сообщений из этой интеграции", - "IndexerSettingsSeedRatio": "Рейтинг", + "IndexerSettingsSeedRatio": "Коэффициент раздачи", "ImportListsTraktSettingsWatchedListFilter": "Фильтр списка просмотра", "IndexerHDBitsSettingsCodecs": "Кодеки", "IndexerStatusUnavailableHealthCheckMessage": "Индексаторы недоступны из-за ошибок: {indexerNames}", @@ -2018,15 +2017,15 @@ "TestAllIndexers": "Тестировать все индексаторы", "Tba": "Будет объявлено позже", "LastDuration": "Последняя длительность", - "PendingChangesStayReview": "Оставайтесь и просмотрите изменения", + "PendingChangesStayReview": "Остаться и просмотреть изменения", "QualitySettingsSummary": "Размеры и название качества", "Queue": "Очередь", "RatingVotes": "Рейтинг голосов", "NotificationsPlexSettingsServer": "Сервер", "Search": "Поиск", "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит пользовательский интерфейс во время процесса восстановления.", - "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, щелкнув вики-ссылку (значок книги) в конце строки или проверив свои [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", - "MaintenanceRelease": "Техническая версия: исправлены ошибки и другие улучшения. Дополнительную информацию см. в истории коммитов Github", + "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", + "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.", "Space": "Пробел", "SslCertPasswordHelpText": "Пароль для файла pfx", "SpecialEpisode": "Спец. эпизод", @@ -2036,7 +2035,7 @@ "SslPort": "SSL порт", "SslCertPathHelpText": "Путь к pfx файлу", "ShowTags": "Показать теги", - "TorrentBlackholeSaveMagnetFiles": "Сохранить магнет файлы", + "TorrentBlackholeSaveMagnetFiles": "Сохранить магнет-файлы", "TomorrowAt": "Завтра в {time}", "Tomorrow": "Завтра", "Special": "Спец. эпизод", @@ -2067,7 +2066,7 @@ "Ui": "Пользовательский интерфейс", "ShowSeriesTitleHelpText": "Показать название сериала под постером", "ShowSeasonCount": "Показать количество сезонов", - "TorrentBlackholeSaveMagnetFilesExtension": "Сохранить расширение магнет файлов", + "TorrentBlackholeSaveMagnetFilesExtension": "Сохранить магнет-файлы с расширением", "ShowQualityProfile": "Показать профиль качества", "SeriesTypes": "Типы сериалов", "SelectLanguages": "Выбрать языки", @@ -2081,12 +2080,15 @@ "SystemTimeHealthCheckMessage": "Расхождение системного времени более чем на 1 день. Запланированные задачи могут работать некорректно, пока не будет исправлено время", "UiSettingsSummary": "Параметры календаря, даты и опции для слабовидящих", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Вместо перемещения файлов это даст указание {appName} скопировать или установить жесткую ссылку (в зависимости от настроек/конфигурации системы)", - "TorrentBlackholeSaveMagnetFilesHelpText": "Сохраните магнитную ссылку, если файл .torrent недоступен (полезно только в том случае, если клиент загрузки поддерживает магниты, сохраненные в файле)", + "TorrentBlackholeSaveMagnetFilesHelpText": "Сохранить магнет-ссылку, если файл .torrent недоступен (полезно только в случае, если клиент загрузки поддерживает магнет-ссылки, сохранённые в файле)", "SelectEpisodesModalTitle": "{modalTitle} ‐ выберите эпизод(ы)", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Расширение для магнитных ссылок, по умолчанию «.magnet»", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Расширение для магнет-ссылок, по умолчанию '.magnet'", "OverrideGrabNoSeries": "Необходимо выбрать сериал", "NotificationsPushBulletSettingSenderIdHelpText": "Идентификатор устройства для отправки уведомлений. Используйте device_iden в URL-адресе устройства на pushbullet.com (оставьте пустым, чтобы отправлять уведомления от себя)", "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages поддерживает суффикс `:EN+DE`, позволяющий фильтровать языки, включенные в имя файла. Используйте `-DE`, чтобы исключить определенные языки. Добавление `+` (например, `:EN+`) приведет к выводу `[EN]`/`[EN+--]`/`[--]` в зависимости от исключенных языков. Например `{MediaInfo Full:EN+DE}`.", "MaximumSingleEpisodeAgeHelpText": "Во время поиска по всему сезону будут разрешены только сезонные пакеты, если последний эпизод сезона старше этой настройки. Только стандартные сериалы. Используйте 0 для отключения.", - "SeasonsMonitoredAll": "Все" + "SeasonsMonitoredAll": "Все", + "LogSizeLimit": "Ограничение размера журнала", + "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", + "IndexerHDBitsSettingsMediums": "Mediums" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 4871e00c7..72eb8977c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1,21 +1,21 @@ { "CloneCondition": "克隆条件", "DeleteCondition": "删除条件", - "DeleteConditionMessageText": "你确定要删除条件 “{name}” 吗?", + "DeleteConditionMessageText": "您确认要删除条件 “{name}” 吗?", "DeleteCustomFormatMessageText": "是否确实要删除条件“{name}”?", - "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{length}个字符长。您可以通过设置或配置文件执行此操作", - "RemoveSelectedItemQueueMessageText": "您确定要从队列中删除一个项目吗?", - "RemoveSelectedItemsQueueMessageText": "您确定要从队列中删除{selectedCount}项吗?", + "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少 {length} 个字符长。您可以通过设置或配置文件完成此操作", + "RemoveSelectedItemQueueMessageText": "您确认要从队列中移除 1 项吗?", + "RemoveSelectedItemsQueueMessageText": "您确认要从队列中移除 {selectedCount} 项吗?", "ApplyChanges": "应用更改", "AutomaticAdd": "自动添加", "EditSelectedDownloadClients": "编辑选定的下载客户端", - "EditSelectedIndexers": "编辑选定的索引器", + "EditSelectedIndexers": "编辑选定索引器", "EditSelectedImportLists": "编辑选定的导入列表", "HiddenClickToShow": "隐藏,点击显示", "HideAdvanced": "隐藏高级", "Language": "语言", - "RemoveCompletedDownloads": "删除已完成的下载", - "RemoveFailedDownloads": "删除失败的下载", + "RemoveCompletedDownloads": "移除已完成的下载记录", + "RemoveFailedDownloads": "移除失败下载记录", "ShowAdvanced": "显示高级", "ShownClickToHide": "显示,点击隐藏", "DeleteSelectedDownloadClients": "删除下载客户端", @@ -39,25 +39,25 @@ "ApplyTagsHelpTextRemove": "移除: 移除已输入的标签", "SkipRedownload": "跳过重新下载", "AllTitles": "全部标题", - "ReleaseHash": "发行版 Hash值", - "ApplyTagsHelpTextHowToApplyDownloadClients": "如何将标签应用到已选择的下载客户端", - "ApplyTagsHelpTextHowToApplyImportLists": "如何将标签应用到已选择的导入列表", + "ReleaseHash": "发布资源 Hash", + "ApplyTagsHelpTextHowToApplyDownloadClients": "如何将标签应用到已选中的下载客户端", + "ApplyTagsHelpTextHowToApplyImportLists": "如何将标签应用到已选中的导入列表", "TestParsing": "测试解析", "ApplyTagsHelpTextAdd": "添加: 添加标签至已有的标签列表中", - "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器", - "AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除", - "BlocklistRelease": "黑名单版本", - "BlocklistReleases": "黑名单版本", - "CloneCustomFormat": "复制自定义命名格式", + "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选中的索引器", + "AppDataLocationHealthCheckMessage": "无法更新,以防止在更新时删除 AppData", + "BlocklistRelease": "发布资源黑名单", + "BlocklistReleases": "发布资源黑名单", + "CloneCustomFormat": "复制自定义格式", "Close": "关闭", "Delete": "删除", "Negated": "无效的", "Activity": "活动", - "AddNew": "添加新的", + "AddNew": "添加新项目", "Backup": "备份", "Blocklist": "黑名单", "Calendar": "日历", - "Connect": "通知连接", + "Connect": "连接", "MediaManagement": "媒体管理", "Metadata": "元数据", "CountSeasons": "第 {count} 季", @@ -125,12 +125,12 @@ "NoChange": "无修改", "NoSeasons": "没有季", "PartialSeason": "部分季", - "Monitored": "已监控", + "Monitored": "已追踪", "MultiSeason": "多季", "Path": "路径", "Proper": "合适的", "ReleaseGroup": "发布组", - "ReleaseTitle": "发行版标题", + "ReleaseTitle": "发布资源标题", "RemotePathMappingFileRemovedHealthCheckMessage": "文件{path} 在处理的过程中被部分删除。", "AutoAdd": "自动添加", "Cancel": "取消", @@ -144,12 +144,12 @@ "PreviousAiring": "上一次播出", "Profiles": "配置", "ProxyResolveIpHealthCheckMessage": "无法解析已设置的代理服务器主机{proxyHostName}的IP地址", - "Quality": "媒体质量", + "Quality": "质量", "Queue": "队列", "Real": "真的", - "Release": "发行版", + "Release": "发布资源", "CustomFormatScore": "自定义格式分数", - "ExportCustomFormat": "已有自定义格式", + "ExportCustomFormat": "导出自定义格式", "Age": "年龄", "All": "全部", "AudioInfo": "音频信息", @@ -158,16 +158,16 @@ "Component": "组件", "ContinuingOnly": "仅包含仍在继续的", "CountImportListsSelected": "已选择 {count} 个导入列表", - "CountIndexersSelected": "已选择 {count} 个索引器", + "CountIndexersSelected": "选定 {count} 个索引器", "CurrentlyInstalled": "已安装", - "CustomFormats": "自定义命名格式", - "CutoffUnmet": "未达设定标准", + "CustomFormats": "自定义格式", + "CutoffUnmet": "未达阈值项", "Date": "日期", "DeleteBackup": "删除备份", - "DeleteCustomFormat": "删除自定义命名格式", - "DeleteSelectedIndexersMessageText": "您确定要删除{count}选定的索引器吗?", + "DeleteCustomFormat": "删除自定义格式", + "DeleteSelectedIndexersMessageText": "您确定要删除选定的 {count} 个索引器吗?", "Deleted": "已删除", - "Disabled": "禁用", + "Disabled": "已禁用", "Discord": "分歧", "DiskSpace": "硬盘空间", "Docker": "Docker", @@ -196,12 +196,12 @@ "HasMissingSeason": "有缺失的季", "Ignored": "已忽略", "Imported": "已导入", - "IncludeUnmonitored": "包含未监控的", + "IncludeUnmonitored": "包含未追踪项", "Indexer": "索引器", "Info": "信息", "LatestSeason": "最新季", "MissingEpisodes": "缺失集", - "MonitoredOnly": "仅监控", + "MonitoredOnly": "仅追踪", "NotSeasonPack": "非季包", "OutputPath": "输出路径", "PackageVersion": "Package版本", @@ -212,11 +212,11 @@ "Protocol": "协议", "Queued": "队列中", "Rating": "评分", - "ReadTheWikiForMoreInformation": "查阅Wiki获得更多信息", + "ReadTheWikiForMoreInformation": "请查阅 Wiki 获取更多信息", "Refresh": "刷新", "RejectionCount": "拒绝次数", "RelativePath": "相对路径", - "ReleaseGroups": "制作团队", + "ReleaseGroups": "发布组", "Reload": "重新加载", "DeleteBackupMessageText": "您确定要删除备份“{name}”吗?", "EnableAutomaticSearch": "启用自动搜索", @@ -225,11 +225,11 @@ "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {url}", "About": "关于", "Actions": "动作", - "AppDataDirectory": "AppData目录", + "AppDataDirectory": "AppData 目录", "ApplyTagsHelpTextHowToApplySeries": "如何将标记应用于所选剧集", "AptUpdater": "使用apt安装更新", - "BackupNow": "马上备份", - "Backups": "历史备份", + "BackupNow": "立即备份", + "Backups": "备份", "BeforeUpdate": "更新前", "CancelPendingTask": "您确定要取消这个挂起的任务吗?", "Clear": "清除", @@ -266,8 +266,8 @@ "Message": "信息", "Remove": "移除", "RemoveFromDownloadClient": "从下载客户端中移除", - "RemoveSelectedItem": "删除所选项目", - "Required": "必须的", + "RemoveSelectedItem": "移除选中项", + "Required": "强制匹配", "Settings": "设置", "System": "系统", "Tags": "标签", @@ -280,27 +280,27 @@ "LiberaWebchat": "Libera聊天", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "您正在使用Docker;下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}中的路径。查看Docker路径映射并更新下载客户端设置。", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}报告的文件在{path},但{appName}无法查看此目录。您可能需要调整文件夹的权限。", - "RemovedFromTaskQueue": "从任务队列中删除", + "RemovedFromTaskQueue": "已从任务队列移除", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "本地下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}路径。查看您的下载客户端设置。", "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "远程下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}路径。查看远程路径映射并更新下载客户端设置。", "Restart": "重启", "RestartReloadNote": "注意:{appName}将在恢复过程中自动重启并重新加载UI。", "Restore": "恢复", "RootFolder": "根目录", - "RemoveSelectedItems": "删除所选项目", + "RemoveSelectedItems": "移除选中项", "RemovedSeriesSingleRemovedHealthCheckMessage": "节目{series}已从TVDB中删除", "RootFolderMissingHealthCheckMessage": "缺少根目录: {rootFolderPath}", "RootFolderMultipleMissingHealthCheckMessage": "多个根目录缺失:{rootFolderPaths}", "SkipRedownloadHelpText": "阻止{appName}尝试下载此项目的替代版本", "Tasks": "任务", - "Wanted": "待获取", + "Wanted": "待寻", "Yes": "确定", "AbsoluteEpisodeNumbers": "准确的集数", - "RemoveCompleted": "移除已完成", - "RemoveFailed": "删除失败", + "RemoveCompleted": "移除成功", + "RemoveFailed": "移除失败", "RemovedSeriesMultipleRemovedHealthCheckMessage": "已从TVDB中删除节目{series}", "RemotePathMappingFolderPermissionsHealthCheckMessage": "下载目录{downloadPath}已存在但{appName}无法访问。可能是权限错误。", - "RemovingTag": "移除标签", + "RemovingTag": "正在移除标签", "RemotePathMappingLocalFolderMissingHealthCheckMessage": "远程下载客户端{downloadClientName}将文件下载在{path}中,但此目录似乎不存在。可能缺少或有不正确的远程路径映射。", "Replace": "替换", "Repack": "重新打包", @@ -322,12 +322,12 @@ "Title": "标题", "Type": "类型", "UnmonitoredOnly": "监控中", - "UpdateStartupTranslocationHealthCheckMessage": "无法安装更新,因为启动文件夹“{startupFolder}”在应用程序迁移文件夹中。", + "UpdateStartupTranslocationHealthCheckMessage": "无法安装更新,因为启动文件夹 “{startupFolder}” 位于 App Translocation 文件夹中。", "Updates": "更新", "VideoCodec": "视频编码", "VideoDynamicRange": "视频动态范围", "Warn": "警告", - "Year": "年", + "Year": "年份", "SeasonCount": "季数量", "SystemTimeHealthCheckMessage": "系统时间相差超过1天。在纠正时间之前,计划的任务可能无法正确运行", "TestAll": "测试全部", @@ -355,8 +355,8 @@ "Twitter": "Twitter", "UnableToLoadBackups": "无法加载备份", "UnableToUpdateSonarrDirectly": "无法直接更新{appName},", - "Unmonitored": "未监控", - "TheLogLevelDefault": "默认的日志等级为 \"Info\", 可以在 [常规设置] 中修改 (/settings/general)", + "Unmonitored": "未追踪", + "TheLogLevelDefault": "默认的日志等级为 \"Info\",可以在 [常规设置] 中修改 (/settings/general)", "Time": "时间", "TotalSpace": "总空间", "UpdaterLogFiles": "更新器日志文件", @@ -364,7 +364,7 @@ "Wiki": "Wiki", "WouldYouLikeToRestoreBackup": "是否要还原备份“{name}”?", "YesCancel": "确定,取消", - "UpdateAvailableHealthCheckMessage": "有新的更新可用", + "UpdateAvailableHealthCheckMessage": "有新的更新可用:{version}", "AddAutoTag": "添加自动标签", "AddCondition": "添加条件", "AddRootFolder": "添加根目录", @@ -393,21 +393,21 @@ "AddNotificationError": "无法添加新通知,请稍后重试。", "AddQualityProfile": "添加质量配置", "AddQualityProfileError": "无法添加新质量配置,请稍后重试。", - "AddRemotePathMapping": "添加远程目录映射", - "AddReleaseProfile": "添加发布组配置", + "AddRemotePathMapping": "添加远程路径映射", + "AddReleaseProfile": "添加发布资源配置", "AddRemotePathMappingError": "无法添加远程路径映射,请稍后重试。", "AddSeriesWithTitle": "添加 {title}", "AddToDownloadQueue": "添加到下载队列", "AddedToDownloadQueue": "已加入下载队列", "AllFiles": "全部文件", - "AlreadyInYourLibrary": "已经在你的库中", + "AlreadyInYourLibrary": "已在您的资源库中", "Always": "总是", "AnimeEpisodeFormat": "动漫单集格式", "AnimeEpisodeTypeFormat": "绝对集数 ({format})", - "AppUpdated": "{appName} 升级", + "AppUpdated": "{appName} 已更新", "AppUpdatedVersion": "{appName} 已经更新到 {version} 版本,重新加载 {appName} 使更新生效 ", - "AuthenticationRequired": "需要身份验证", - "AutomaticUpdatesDisabledDocker": "不支持在使用 Docker 容器时直接升级。你需要升级 {appName} 容器镜像或使用脚本(script)", + "AuthenticationRequired": "需要认证", + "AutomaticUpdatesDisabledDocker": "使用 Docker 更新机制时,自动更新不被直接支持。您需要在 {appName} 之外更新容器镜像,或使用脚本进行更新", "BackupIntervalHelpText": "自动备份时间间隔", "Branch": "分支", "BranchUpdate": "更新{appName}的分支", @@ -415,7 +415,7 @@ "BrowserReloadRequired": "浏览器需重新加载", "BuiltIn": "内置的", "BypassDelayIfAboveCustomFormatScore": "若高于自定义格式分数则跳过", - "BypassDelayIfHighestQualityHelpText": "当发布版本的质量中含有已启用的最高质量首选协议时跳过延迟", + "BypassDelayIfHighestQualityHelpText": "当发布资源的质量为 “质量配置” 中最高启用质量且使用首选协议时,忽略延迟", "BypassProxyForLocalAddresses": "对局域网地址不使用代理", "CalendarFeed": "{appName} 日历订阅", "CalendarLegendEpisodeDownloadedTooltip": "集已下载完成并完成排序", @@ -442,7 +442,7 @@ "Condition": "条件", "ConditionUsingRegularExpressions": "此条件使用正则表达式匹配。请注意字符 `\\^$.|?*+()[{` 具有特殊含义,需要用转义符 `\\`", "ConnectSettings": "连接设置", - "ConnectSettingsSummary": "通知、与媒体服务器/播放器的链接、自定义脚本", + "ConnectSettingsSummary": "通知、与媒体服务器/播放器的连接及自定义脚本", "ConnectionLost": "连接丢失", "ConnectionLostReconnect": "{appName} 将会尝试自动连接,您也可以点击下方的重新加载。", "ConnectionLostToBackend": "{appName}失去了与后端的连接,需要重新加载以恢复功能。", @@ -456,12 +456,12 @@ "Custom": "自定义", "CreateGroup": "创建组", "CustomFilters": "自定义过滤器", - "CustomFormatUnknownCondition": "未知自定义格式条件'{0}'", - "CustomFormatUnknownConditionOption": "未知的条件“{1}”的选项“{0}”", + "CustomFormatUnknownCondition": "未知自定义格式条件'{implementation}'", + "CustomFormatUnknownConditionOption": "条件“{implementation}”的未知选项“{key}”", "CustomFormatsLoadError": "无法加载自定义格式", "CustomFormatsSettings": "自定义格式设置", "CustomFormatsSettingsSummary": "自定义格式和设置", - "Cutoff": "截止", + "Cutoff": "阈值", "DailyEpisodeTypeFormat": "日期 ({format})", "Day": "天", "Default": "默认", @@ -473,9 +473,9 @@ "DelayProfiles": "延迟配置", "DelayProfileProtocol": "协议:{preferredProtocol}", "DeleteAutoTag": "删除自动标签", - "DeleteDelayProfileMessageText": "你确定要删除此延迟配置吗?", + "DeleteDelayProfileMessageText": "您确认要删除此延迟配置吗?", "DeleteDownloadClient": "删除下载客户端", - "DeleteDownloadClientMessageText": "你确定要删除下载客户端 “{name}” 吗?", + "DeleteDownloadClientMessageText": "您确认要删除下载客户端 “{name}” 吗?", "DeleteEmptyFolders": "删除空目录", "DeleteEmptySeriesFoldersHelpText": "如果集文件被删除,当磁盘扫描时删除剧集或季的空目录", "DeleteEpisodeFile": "删除集文件", @@ -484,11 +484,11 @@ "DeleteImportList": "删除导入的列表", "DeleteQualityProfile": "删除质量配置", "DeleteQualityProfileMessageText": "您确定要删除质量配置“{name}”吗?", - "DeleteReleaseProfile": "删除发布组配置", - "DeleteReleaseProfileMessageText": "您确定要删除此发布配置文件“{name}”吗?", + "DeleteReleaseProfile": "删除发布资源配置", + "DeleteReleaseProfileMessageText": "您确定要删除发布资源配置 “{name}” 吗?", "DeleteRemotePathMapping": "删除远程路径映射", - "DeleteRemotePathMappingMessageText": "你确定要删除此远程路径映射吗?", - "DeleteRootFolderMessageText": "你确定要删除根目录 “{path}” 吗?", + "DeleteRemotePathMappingMessageText": "您确认要删除此远程路径映射吗?", + "DeleteRootFolderMessageText": "您确认要删除根目录 “{path}” 吗?", "DeleteSelectedEpisodeFilesHelpText": "您确定要删除选定的剧集文件吗?", "DeleteTag": "删除标签", "DownloadClientOptionsLoadError": "无法加载下载客户端选项", @@ -514,9 +514,9 @@ "FileNames": "文件名", "Filter": "过滤", "UpdateMechanismHelpText": "使用 {appName} 内置的更新程序或脚本", - "AuthenticationRequiredHelpText": "修改哪些请求需要认证。除非你了解其中的风险,否则不要更改。", + "AuthenticationRequiredHelpText": "更改需要进行验证的请求。除非您了解其中的风险,否则请勿更改。", "AnEpisodeIsDownloading": "集正在下载", - "AuthenticationRequiredWarning": "为了防止未经身份验证的远程访问,{appName} 现在需要启用身份验证。您可以禁用本地地址的身份验证。", + "AuthenticationRequiredWarning": "为防止未经认证的远程访问,{appName} 现需要启用身份认证。您可以选择禁用本地地址的身份认证。", "AutomaticSearch": "自动搜索", "BackupFolderHelpText": "相对路径将在 {appName} 的 AppData 目录下", "BindAddress": "绑定地址", @@ -528,10 +528,10 @@ "ColonReplacement": "替换冒号", "ColonReplacementFormatHelpText": "修改{appName}如何处理冒号的替换", "CollectionsLoadError": "不能加载收藏", - "CompletedDownloadHandling": "完成下载处理", + "CompletedDownloadHandling": "已完成下载处理", "DeleteDelayProfile": "删除延迟配置", "DownloadClientsLoadError": "无法加载下载客户端", - "DownloadClientsSettingsSummary": "下载客户端,下载处理和远程地址映射", + "DownloadClientsSettingsSummary": "下载客户端、下载处理和远程路径映射", "ClientPriority": "客户端优先级", "AllSeriesInRootFolderHaveBeenImported": "{path} 中的所有剧集都已导入", "Analytics": "分析", @@ -541,16 +541,16 @@ "AnalyseVideoFiles": "分析视频文件", "ApplicationURL": "应用程序 URL", "AnimeEpisodeTypeDescription": "使用绝对集数发布的集数", - "ApiKey": "接口密钥 (API Key)", - "ApplicationUrlHelpText": "此应用的外部URL,包含 http(s)://、端口和基本URL", + "ApiKey": "API 密钥", + "ApplicationUrlHelpText": "此应用的外部 URL,包含 http(s)://、端口和基本 URL", "AuthenticationMethodHelpText": "需要用户名和密码以访问 {appName}", "AuthBasic": "基础(浏览器弹出对话框)", "AuthForm": "表单(登陆页面)", "Authentication": "认证", - "AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的版本", + "AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的发布资源", "AutoTaggingLoadError": "无法加载自动标记", "Automatic": "自动化", - "AutoTaggingRequiredHelpText": "这个{0}条件必须匹配自动标记规则才能应用。否则,一个{0}匹配就足够了。", + "AutoTaggingRequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自动标记规则。否则,单个 {implementationName} 匹配就足够了。", "AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。", "BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理", "BackupsLoadError": "无法加载备份", @@ -571,8 +571,8 @@ "Debug": "调试", "DelayProfilesLoadError": "无法加载延时配置", "DeleteImportListExclusion": "删除导入排除列表", - "DeleteIndexerMessageText": "您确定要删除索引器“{name}”吗?", - "DeleteImportListExclusionMessageText": "你确定要删除这个导入排除列表吗?", + "DeleteIndexerMessageText": "您确定要删除索引器 “{name}” 吗?", + "DeleteImportListExclusionMessageText": "您确认要删除此导入排除列表吗?", "DeleteImportListMessageText": "您确定要删除列表 “{name}” 吗?", "DeleteNotification": "删除消息推送", "DeleteNotificationMessageText": "您确定要删除通知“{name}”吗?", @@ -591,7 +591,7 @@ "AddImportListExclusionError": "无法添加新排除列表,请再试一次。", "AddIndexer": "添加索引器", "AddImportList": "添加导入列表", - "AddIndexerError": "无法添加索引器,请稍后重试。", + "AddIndexerError": "无法添加新索引器,请重试。", "AddImportListExclusion": "添加导入排除列表", "AddList": "添加列表", "AddListError": "无法添加列表,请稍后重试。", @@ -602,9 +602,9 @@ "Agenda": "日程表", "CertificateValidationHelpText": "改变HTTPS证书验证的严格程度。不要更改除非您了解风险。", "CopyUsingHardlinksSeriesHelpText": "硬链接 (Hardlinks) 允许 {appName} 将还在做种中的剧集文件(夹)导入而不占用额外的存储空间或者复制文件(夹)的全部内容。硬链接 (Hardlinks) 仅能在源文件和目标文件在同一磁盘卷中使用", - "CustomFormat": "自定义命名格式", - "CustomFormatHelpText": "{appName}会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数,有相同或更高的影片质量,则{appName}会抓取该发布版本。", - "DeleteAutoTagHelpText": "你确定要删除 “{name}” 自动标签吗?", + "CustomFormat": "自定义格式", + "CustomFormatHelpText": "{appName} 根据与所有自定义格式的匹配程度分数总和为每个资源打分。如果新的资源在相同或更高质量下获得更高分数,{appName} 将会抓取该资源。", + "DeleteAutoTagHelpText": "您确认要删除 “{name}” 自动标签吗?", "DownloadClientSeriesTagHelpText": "仅将此下载客户端用于至少具有一个匹配标签的剧集。留空可用于所有剧集。", "Absolute": "绝对", "AddANewPath": "添加一个新的目录", @@ -625,12 +625,12 @@ "EditListExclusion": "编辑排除列表", "EditImportListExclusion": "编辑导入排除列表", "EditMetadata": "编辑 {metadataType} 元数据", - "EditReleaseProfile": "编辑发布组配置", + "EditReleaseProfile": "编辑发布资源配置", "EnableColorImpairedModeHelpText": "改变样式,以允许有颜色障碍的用户更好地区分颜色编码信息", "EditQualityProfile": "编辑质量配置", "EnableCompletedDownloadHandlingHelpText": "自动从下载客户端导入已完成的下载内容", "EnableProfile": "启用配置", - "EnableProfileHelpText": "检查以启用发布配置文件", + "EnableProfileHelpText": "勾选以启用发布资源配置", "EnableRss": "启用RSS", "EnableHelpText": "启用此元数据类型的元数据文件创建", "EpisodeFileRenamed": "集文件已被重命名", @@ -688,8 +688,8 @@ "ICalShowAsAllDayEventsHelpText": "事件将以全天事件的形式显示在日历中", "ICalSeasonPremieresOnlyHelpText": "每季中只有第一集会出现在订阅中", "IconForFinalesHelpText": "根据可用的集信息为完结的剧集或季显示图标", - "IconForCutoffUnmet": "未达设定标准的图标", - "IconForCutoffUnmetHelpText": "终止监控条件未满足前为文件显示图标", + "IconForCutoffUnmet": "未达阈值的图标", + "IconForCutoffUnmetHelpText": "在文件未达阈值时显示图标", "IconForFinales": "剧集或季完结的图标", "Images": "图像", "IgnoredAddresses": "已忽略地址", @@ -706,7 +706,7 @@ "ImportExistingSeries": "导入已存在的剧集", "DeleteSpecification": "删除规范", "DeleteSpecificationHelpText": "您确定要删除规范 '{name}' 吗?", - "DeletedReasonManual": "文件已通过 UI 删除", + "DeletedReasonManual": "文件已通过 {appName} 手动或使用其它工具通过 API 删除", "DeletedReasonEpisodeMissingFromDisk": "{appName} 在磁盘上找不到该文件,因此已取消数据库中和该文件的集关联", "DownloadPropersAndRepacks": "优化版和重制版", "DownloadPropersAndRepacksHelpText": "是否自动更新至优化版和重制版", @@ -757,17 +757,17 @@ "EnableInteractiveSearchHelpText": "当手动搜索启用时使用", "EnableMediaInfoHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要{appName}读取文件,可能导致扫描期间磁盘或网络出现高负载。", "EpisodeGrabbedTooltip": "集抓取自 {indexer} 并发送至 {downloadClient}", - "HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查[日志]({link})来查找有关这些运行状况检查消息原因的更多信息。如果你在理解这些信息方面有困难,你可以通过下面的链接联系我们的支持。", + "HealthMessagesInfoBox": "您可以通过单击行尾的 wiki 链接(图书图标)或检查 [日志]({link}) 来获取导致此类运行状态消息的更多信息。如果您在理解此类信息方面有困难,您可以通过以下链接联系我们以获得支持。", "StandardEpisodeFormat": "标准单集格式", "DefaultNameCopiedSpecification": "{name} - 复制", - "AddListExclusion": "新增 列表", + "AddListExclusion": "添加列表排除项", "AddListExclusionSeriesHelpText": "防止剧集通过列表添加到{appName}", "AddNewSeriesSearchForCutoffUnmetEpisodes": "开始搜索未达截止条件的集", "AddedDate": "加入于:{date}", "AllSeriesAreHiddenByTheAppliedFilter": "所有结果都被应用的过滤器隐藏", "AlternateTitles": "别名", "Any": "任何", - "AuthenticationMethodHelpTextWarning": "请选择一个有效的身份验证方式", + "AuthenticationMethodHelpTextWarning": "请选择一个有效的认证方式", "AuthenticationMethod": "认证方式", "AuthenticationRequiredPasswordHelpTextWarning": "请输入新密码", "AuthenticationRequiredUsernameHelpTextWarning": "请输入新用户名", @@ -779,11 +779,11 @@ "SeriesDetailsCountEpisodeFiles": "{episodeFileCount}集文件", "SeriesCannotBeFound": "对不起,这个系列找不到。", "SeriesEditRootFolderHelpText": "将系列移动到相同的根文件夹可用于重命名系列文件夹以匹配已更新的标题或命名格式", - "ReplaceWithSpaceDash": "替换为空格破折号", + "ReplaceWithSpaceDash": "使用空格破折号(xx -xx)替换", "RenameEpisodesHelpText": "如果禁用重命名,{appName} 将使用现有的文件名", "RenameEpisodes": "重命名剧集", - "ReplaceWithDash": "替换为破折号", - "ReplaceWithSpaceDashSpace": "替换为空格破折号空格", + "ReplaceWithDash": "使用破折号(xx-xx)替换", + "ReplaceWithSpaceDashSpace": "使用空格破折号空格(xx - xx)替换", "StandardEpisodeTypeFormat": "季数和集数({format})", "DailyEpisodeTypeDescription": "使用年-月-日的每天或更少频率发布的剧集(2023-08-04)", "Dash": "破折号", @@ -793,7 +793,7 @@ "DefaultNameCopiedProfile": "{name} - 复制", "SeriesFinale": "大结局", "SeriesFolderFormat": "剧集文件夹格式", - "ReplaceIllegalCharactersHelpText": "替换非法字符,如未勾选,则会被{appName}移除", + "ReplaceIllegalCharactersHelpText": "替换非法字符。如未勾选,则字符会被 {appName} 移除", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们]({url})。", "DeleteSelectedSeries": "删除选中的剧集", "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", @@ -812,10 +812,10 @@ "ImportScriptPathHelpText": "用于导入的脚本的路径", "IncludeCustomFormatWhenRenaming": "重命名时包含自定义格式", "RemotePathMappingsInfo": "很少需要远程路径映射,如果 {appName} 和您的下载客户端在同一系统上,则最好匹配您的路径。更多信息,请参阅[wiki]({wikiLink})", - "IndexerPriorityHelpText": "索引器优先级从1(最高)到50(最低),默认25。当资源连接中断时寻找同等资源时使用,否则{appName}将依旧使用已启用的索引器进行RSS同步并搜索", + "IndexerPriorityHelpText": "索引器优先级从1(最高)到50(最低),默认25。当资源连接中断时寻找同等资源时使用,否则 {appName} 将依旧使用已启用的索引器进行 RSS 同步与搜索", "IndexerTagSeriesHelpText": "仅对至少有一个匹配标记的剧集使用此索引器。留空则适用于所有剧集。", "InteractiveImportNoFilesFound": "在所选文件夹中找不到视频文件", - "RemoveSelectedBlocklistMessageText": "你确定你想从过滤清单中删除选中的项目吗?", + "RemoveSelectedBlocklistMessageText": "您确认您想要从阻止列表中移除选中的项目吗?", "InteractiveImportNoQuality": "必须为每个选定的文件选择质量", "RestartSonarr": "重启{appName}", "InteractiveSearchModalHeaderSeason": "手动搜索 - {season}", @@ -854,7 +854,7 @@ "SupportedIndexersMoreInfo": "若需要查看有关索引器的详细信息,请点击“更多信息”按钮。", "NoEpisodeInformation": "没有可用的集信息。", "TableColumnsHelpText": "选择哪些列可见以及它们的显示顺序", - "NoMinimumForAnyRuntime": "运行环境没有最小限制", + "NoMinimumForAnyRuntime": "时间没有最低要求", "Theme": "主题", "UseSeasonFolderHelpText": "将集排序整理到季文件夹中", "None": "无", @@ -884,7 +884,7 @@ "OrganizeNamingPattern": "命名格式: \"{episodeFormat}\"", "Parse": "解析", "OrganizeSelectedSeriesModalHeader": "整理选定的剧集", - "Original": "原始的", + "Original": "原始", "Other": "其他", "PreferredProtocol": "首选协议", "Posters": "海报", @@ -892,7 +892,7 @@ "QualitiesLoadError": "无法加载质量", "QualitiesHelpText": "列表中的质量排序越高优先级也越高。同组内的质量优先级相同。质量只有选中才标记为追踪", "QualityDefinitionsLoadError": "无法加载质量定义", - "RemotePathMappingRemotePathHelpText": "下载客户端访问的目录的根路径", + "RemotePathMappingRemotePathHelpText": "下载客户端访问目录的根路径", "RemoveFilter": "移除过滤器", "RestartNow": "马上重启", "SetReleaseGroupModalTitle": "{modalTitle} - 设置发布组", @@ -936,7 +936,7 @@ "HourShorthand": "时", "ImportedTo": "导入到", "Importing": "导入中", - "IndexerDownloadClientHealthCheckMessage": "有无效下载客户端的索引器:{indexerNames}。", + "IndexerDownloadClientHealthCheckMessage": "使用无效下载客户端的索引器:{indexerNames}。", "IndexersSettingsSummary": "索引器和索引器选项", "InteractiveImport": "手动导入", "InstanceNameHelpText": "选项卡及日志应用名称", @@ -957,7 +957,7 @@ "Links": "链接", "ListExclusionsLoadError": "无法加载排除列表", "ListOptionsLoadError": "无法加载列表选项", - "ListWillRefreshEveryInterval": "列表将每隔 {refreshInterval} 刷新一次", + "ListWillRefreshEveryInterval": "列表将每 {refreshInterval} 刷新一次", "Local": "本地", "LocalStorageIsNotSupported": "不支持或禁用本地存储。插件或私人浏览可能已将其禁用。", "ManualGrab": "手动抓取", @@ -979,7 +979,7 @@ "MinimumCustomFormatScore": "最小自定义格式分数", "Min": "最小的", "MinimumAge": "最低间隔", - "MinimumAgeHelpText": "仅限Usenet:抓取NewzBin文件的最小时间间隔(分钟)。开启此功能会让新版本有时间传播到你的usenet提供商。", + "MinimumAgeHelpText": "仅限 Usenet:抓取 NewzBin 文件的最小时间间隔(分钟)。开启此功能会让新发布资源有时间传播到您的 Usenet 提供商。", "MinutesSixty": "60分钟: {sixty}", "MinutesFortyFive": "45分钟: {fortyFive}", "MinutesThirty": "30分钟: {thirty}", @@ -987,7 +987,7 @@ "MonitorAllEpisodesDescription": "监控除特别节目以外的所有集", "MonitorMissingEpisodesDescription": "监控没有文件或尚未播出的剧集", "MonitorFutureEpisodes": "未来剧集", - "MonitoredStatus": "已监控的/状态", + "MonitoredStatus": "已追踪/状态", "Monitoring": "监控中", "MonitoringOptions": "监控选项", "MoreDetails": "更多详细信息", @@ -999,11 +999,11 @@ "MultiEpisodeStyle": "多集风格", "MultiLanguages": "多种语言", "MultiEpisodeInvalidFormat": "多集:无效格式", - "MustNotContain": "必须不包含", + "MustNotContain": "不得包含", "MustContain": "必须包含", - "MustContainHelpText": "发布的剧集必须至少包含一个这些项目(不区分大小写)", + "MustContainHelpText": "发布资源必须包含此类项目其中之一(不区分大小写)", "NamingSettingsLoadError": "无法加载命名设置", - "NegateHelpText": "如勾选,当条件 {implementationName} 满足时不会应用自定义格式。", + "NegateHelpText": "勾选后,若匹配该 {implementationName} 条件,自定义格式将不生效。", "Never": "永不", "NoChanges": "无修改", "NoDelay": "无延迟", @@ -1043,7 +1043,7 @@ "OverrideGrabNoSeries": "必须选择剧集", "Overview": "概览", "OverviewOptions": "概览选项", - "ParseModalHelpText": "在上面的输入框中输入一个发行版标题", + "ParseModalHelpText": "在上面的输入框中输入一个发布资源标题", "ParseModalErrorParsing": "解析错误,请重试。", "ParseModalUnableToParse": "无法解析提供的标题,请重试。", "Paused": "暂停", @@ -1069,28 +1069,28 @@ "ProfilesSettingsSummary": "质量、元数据、延迟、发行配置", "ProxyPasswordHelpText": "如果需要,您只需要输入用户名和密码,否则就让它们为空。", "ProxyUsernameHelpText": "如果需要,您只需要输入用户名和密码。否则就让它们为空。", - "Qualities": "影片质量", - "QualityCutoffNotMet": "影片还未满足质量配置", - "QualitySettingsSummary": "质量尺寸和命名", + "Qualities": "质量", + "QualityCutoffNotMet": "未达到质量阈值", + "QualitySettingsSummary": "质量标准和命名", "QueueIsEmpty": "空队列", - "QualitySettings": "媒体质量设置", + "QualitySettings": "质量设置", "Reason": "原因", "RecyclingBin": "回收站", "RecyclingBinCleanupHelpTextWarning": "回收站中的文件在超出选择的天数后会被自动清理", "RefreshAndScan": "刷新并扫描", "RefreshAndScanTooltip": "刷新信息并扫描磁盘", - "ReleaseProfileIndexerHelpText": "指定配置文件应用于哪个索引器", - "ReleaseRejected": "发布被拒绝", + "ReleaseProfileIndexerHelpText": "指定该配置应用于哪个索引器", + "ReleaseRejected": "发布资源已拒绝", "ReleaseSceneIndicatorAssumingTvdb": "推测TVDB编号。", "ReleaseSceneIndicatorMappedNotRequested": "在此搜索中未包含已映射的剧集。", "ReleaseSceneIndicatorSourceMessage": "发布版本{message}的编号模糊,无法正确地识别剧集。", "ReleaseSceneIndicatorUnknownSeries": "未知的剧集或系列。", - "RemotePathMappingLocalPathHelpText": "{appName}用于访问远程路径的本地路径", + "RemotePathMappingLocalPathHelpText": "{appName} 在本地访问远程路径时应该使用的路径", "RemoveFromBlocklist": "从黑名单中移除", - "RemoveCompletedDownloadsHelpText": "从下载客户端记录中移除已导入的下载", + "RemoveCompletedDownloadsHelpText": "从下载客户端记录中移除已导入的下载记录", "RemoveFromQueue": "从队列中移除", "RemoveQueueItem": "移除 - {sourceTitle}", - "RemoveTagsAutomatically": "自动删除标签", + "RemoveTagsAutomatically": "自动移除标签", "Repeat": "重复", "ResetDefinitions": "重置定义", "RescanAfterRefreshSeriesHelpText": "刷新剧集信息后重新扫描剧集文件夹", @@ -1100,7 +1100,7 @@ "RootFolderSelectFreeSpace": "{freeSpace} 空闲", "RootFolders": "根目录", "RootFoldersLoadError": "无法加载根目录", - "RssIsNotSupportedWithThisIndexer": "该索引器不支持RSS", + "RssIsNotSupportedWithThisIndexer": "该索引器不支持 RSS", "Save": "保存", "SaveChanges": "保存更改", "SaveSettings": "保存设置", @@ -1134,8 +1134,8 @@ "SeriesPremiere": "剧集首播", "ShortDateFormat": "短日期格式", "ShowEpisodes": "显示剧集", - "ShowMonitored": "显示监控中的", - "ShowMonitoredHelpText": "在海报下显示监控状态", + "ShowMonitored": "显示已追踪项", + "ShowMonitoredHelpText": "在海报下显示追踪状态", "ShowNetwork": "显示网络", "ShowPreviousAiring": "显示上一次播出", "ShowQualityProfileHelpText": "在海报下方显示媒体质量配置", @@ -1151,7 +1151,7 @@ "Small": "小", "SingleEpisodeInvalidFormat": "单集:非法格式", "SkipFreeSpaceCheck": "跳过剩余空间检查", - "Space": "空间", + "Space": "空格", "SpecialEpisode": "特别集", "StandardEpisodeTypeDescription": "以SxxEyy模式发布的集", "StartImport": "开始导入", @@ -1189,16 +1189,16 @@ "UnmonitorDeletedEpisodesHelpText": "从磁盘删除的集将在 {appName} 中自动取消监控", "UnmonitorSpecialEpisodes": "取消监控特别节目", "UpdateAll": "全部更新", - "UpdateAutomaticallyHelpText": "自动下载并安装更新。你还可以在“系统:更新”中安装", + "UpdateAutomaticallyHelpText": "自动下载并安装更新。您还可以在 “系统:更新” 中安装", "UpdateSelected": "更新选择的内容", - "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到影片质量超出或者满足", + "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到质量达标或高于标准", "UpgradesAllowed": "允许升级", "UpgradesAllowedHelpText": "如关闭,则质量不做升级", "Uppercase": "大写字母", "UseSeasonFolder": "使用季文件夹", "UseProxy": "使用代理", "Username": "用户名", - "UsenetDelayTime": "Usenet延时:{0}", + "UsenetDelayTime": "Usenet 延迟:{usenetDelay}", "UsenetDisabled": "Usenet已关闭", "UtcAirDate": "UTC 播出日期", "VersionNumber": "版本 {version}", @@ -1212,7 +1212,7 @@ "ReleaseProfile": "发行配置文件", "ReleaseSceneIndicatorAssumingScene": "推测场景编号。", "ReleaseSceneIndicatorUnknownMessage": "这一集的编号各不相同,版本与任何已知的映射都不匹配。", - "RemoveFailedDownloadsHelpText": "从下载客户端中删除已失败的下载", + "RemoveFailedDownloadsHelpText": "从下载客户端的历史记录中移除失败的下载记录", "RemoveTagsAutomaticallyHelpText": "如果条件不满足,则自动移除标签", "ResetAPIKeyMessageText": "您确定要重置您的 API 密钥吗?", "Rss": "RSS", @@ -1234,11 +1234,11 @@ "UpdateScriptPathHelpText": "自定义脚本的路径,该脚本处理获取的更新包并处理更新过程的其余部分", "UpdateSonarrDirectlyLoadError": "无法直接更新{appName},", "View": "查看", - "Negate": "相反的", + "Negate": "反选", "ListTagsHelpText": "从此列表导入时将添加标记", "ManageEpisodesSeason": "管理本季的集文件", "ManualImportItemsLoadError": "无法加载手动导入项目", - "MassSearchCancelWarning": "一旦启动,如果不重启{appName}或禁用所有索引器,就无法取消此操作。", + "MassSearchCancelWarning": "在不重启 {appName} 或禁用所有索引器的情况下,一旦启动便无法取消此操作。", "ListsLoadError": "无法加载列表", "LocalAirDate": "当地播出日期", "LocalPath": "本地路径", @@ -1248,7 +1248,7 @@ "Period": "时期", "PrefixedRange": "前缀范围", "ProtocolHelpText": "在其他相同版本之间进行选择时,选择要使用的协议以及首选的协议", - "QualityDefinitions": "媒体质量定义", + "QualityDefinitions": "质量定义", "Range": "范围", "OpenSeries": "打开剧集", "OptionalName": "可选名称", @@ -1267,42 +1267,42 @@ "HistoryModalHeaderSeason": "{season} 历史记录", "ImportSeries": "导入剧集", "ImportUsingScript": "使用脚本导入", - "IndexerDownloadClientHelpText": "指定索引器的下载客户端", + "IndexerDownloadClientHelpText": "指定从此索引器抓取的下载客户端", "IndexersLoadError": "无法加载索引器", "InfoUrl": "信息 URL", "InstanceName": "应用名称", "InteractiveImportNoSeason": "必须为每个选中的文件选择季", "InteractiveSearch": "手动搜索", - "InteractiveSearchModalHeader": "交互式搜索", + "InteractiveSearchModalHeader": "手动搜索", "InvalidFormat": "格式不合法", "InvalidUILanguage": "您的UI设置为无效语言,请纠正并保存设置", "KeyboardShortcuts": "快捷键", "KeyboardShortcutsSaveSettings": "保存设置", - "ListRootFolderHelpText": "根目录文件夹列表项需添加", + "ListRootFolderHelpText": "根目录列表中的项目将被添加", "Logout": "注销", "ManageEpisodes": "管理集", - "MaximumSizeHelpText": "抓取影片最大多少MB,设置为0则不限制", + "MaximumSizeHelpText": "抓取发布资源的最大大小(以 MB 为单位)。设置为零表示无限制", "MetadataProvidedBy": "元数据由 {provider} 提供", "MidseasonFinale": "季中完结", "MinimumFreeSpaceHelpText": "如果导入的磁盘空间不足,则禁止导入", "MinimumLimits": "最小限制", - "MissingLoadError": "加载缺失项目错误", + "MissingLoadError": "因加载缺失项目出错", "MissingNoItems": "没有缺失项目", "MonitorExistingEpisodesDescription": "监控有文件或尚未播出的剧集", "MonitorFirstSeason": "第一季", "MonitorNoEpisodes": "无", "MonitorNoEpisodesDescription": "没有集被监控", "MonitorPilotEpisode": "试播集", - "MonitorSelected": "监控选中的", + "MonitorSelected": "追踪选中项", "MonitorSeries": "监控剧集", "MonitoredEpisodesHelpText": "下载本剧集中监控的集", "Month": "月", "MoveSeriesFoldersToRootFolder": "是否将剧集文件夹移动到 '{destinationRootFolder}' ?", - "MustNotContainHelpText": "如版本包含一个或多个条件则丢弃(无视大小写)", + "MustNotContainHelpText": "若发布资源包含一个或多个条件则丢弃(不区分大小写)", "MyComputer": "我的电脑", "NamingSettings": "命名设置", "NoEpisodeOverview": "无片段概述", - "NotificationStatusSingleClientHealthCheckMessage": "由于失败导致通知不可用:{notificationNames}", + "NotificationStatusSingleClientHealthCheckMessage": "由于故障通知不可用:{notificationNames}", "NotificationTriggers": "通知触发器", "NotificationsLoadError": "无法加载通知连接", "OnApplicationUpdate": "程序更新时", @@ -1312,13 +1312,13 @@ "PendingDownloadClientUnavailable": "挂起 - 下载客户端不可用", "QueueLoadError": "加载队列失败", "QuickSearch": "快速搜索", - "ReleaseProfiles": "发行版概要", - "ReleaseProfilesLoadError": "无法加载发行版概要", + "ReleaseProfiles": "发布资源配置", + "ReleaseProfilesLoadError": "无法加载发布资源配置", "RemotePath": "远程路径", "RemoveDownloadsAlert": "移除设置被移至上表中的单个下载客户端设置。", "RemoveRootFolder": "移除根目录", - "RemoveSelected": "移除已选", - "Reorder": "重新排序Reorder", + "RemoveSelected": "移除选中项", + "Reorder": "重新排序", "RestartLater": "稍后重启", "RestrictionsLoadError": "无法加载限制条件", "RssSync": "RSS同步", @@ -1340,7 +1340,7 @@ "SslCertPathHelpText": "pfx文件路径", "SupportedAutoTaggingProperties": "{appName}支持自动标记规则的以下属性", "SupportedDownloadClientsMoreInfo": "若需要查看有关下载客户端的详细信息,请点击“更多信息”按钮。", - "SupportedIndexers": "{appName}支持任何使用Newznab标准的索引器,以及下面列出的其他索引器。", + "SupportedIndexers": "{appName} 支持任何使用 Newznab 标准的索引器,以及以下列出的其他索引器。", "SupportedListsSeries": "{appName}支持将多个列表中的剧集导入数据库。", "TableOptionsButton": "表格选项按钮", "TheTvdb": "TheTVDB", @@ -1356,14 +1356,14 @@ "UsenetDelay": "Usenet延时", "UsenetDelayHelpText": "延迟几分钟才能等待从Usenet获取发布", "OrganizeModalHeader": "整理并重命名", - "RssSyncIntervalHelpText": "间隔时间以分钟为单位,设置为0则关闭该功能(会停止所有剧集的自动抓取下载)", + "RssSyncIntervalHelpText": "间隔时间(以分钟为单位),设置为零则禁用(这会停止自动抓取发布资源)", "RssSyncIntervalHelpTextWarning": "这将适用于所有索引器,请遵循他们所制定的规则", "SceneNumberNotVerified": "场景编号未确认", "RssSyncInterval": "RSS同步间隔", "SearchAll": "搜索全部", - "AgeWhenGrabbed": "年龄(在被抓取后)", - "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "绕过首选协议延迟所需的最小自定义格式分数", - "BypassDelayIfAboveCustomFormatScoreHelpText": "当发布版本的评分高于配置的最小自定义格式评分时,跳过延时", + "AgeWhenGrabbed": "年龄(抓取后)", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "绕过延迟所需的最小自定义格式分数(适用于首选协议)", + "BypassDelayIfAboveCustomFormatScoreHelpText": "当发布资源的评分高于所配置的最小自定义格式分数时,跳过延时", "SearchForAllMissingEpisodes": "搜索所有缺失的剧集", "SearchForMissing": "搜索缺少", "SearchForQuery": "搜索{query}", @@ -1373,11 +1373,11 @@ "OneMinute": "1分钟", "RemotePathMappings": "远程路径映射", "RemotePathMappingsLoadError": "无法加载远程路径映射", - "RemoveQueueItemConfirmation": "您确定要从队列中移除“{sourceTitle}”吗?", + "RemoveQueueItemConfirmation": "您确认要从队列中移除 “{sourceTitle}” 吗?", "RequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自定义格式。 否则,单个 {implementationName} 匹配就足够了。", "RescanSeriesFolderAfterRefresh": "刷新后重新扫描剧集文件夹", "RestartRequiredToApplyChanges": "{appName}需要重新启动才能应用更改,您想现在重新启动吗?", - "RescanAfterRefreshHelpTextWarning": "当没有设置为“总是”时,{appName}将不会自动检测文件的更改", + "RescanAfterRefreshHelpTextWarning": "当未设置为 “总是” 时,{appName} 将不会自动检测文件的更改", "Retention": "保留", "RetentionHelpText": "仅限Usenet:设置为零以设置无限保留", "RetryingDownloadOn": "于 {date} {time} 重试下载", @@ -1408,7 +1408,7 @@ "WaitingToImport": "等待导入", "WaitingToProcess": "等待处理", "SendAnonymousUsageData": "发送匿名使用数据", - "UnmonitorSelected": "取消监控选中的", + "UnmonitorSelected": "取消追踪选中项", "UnsavedChanges": "未保存更改", "UnselectAll": "取消选择全部", "UpcomingSeriesDescription": "剧集已宣布,但尚未确定具体的播出日期", @@ -1426,7 +1426,7 @@ "LibraryImportTipsDontUseDownloadsFolder": "不要使用该方法从下载客户端导入影片,本方法只限于导入现有的已整理的库,不能导入未整理的文件。", "MinimumFreeSpace": "最小剩余空间", "Monday": "星期一", - "Monitor": "是否监控", + "Monitor": "追踪", "NotificationTriggersHelpText": "选择触发此通知的事件", "ImportListsSettingsSummary": "从另一个 {appName} 实例或 Trakt 列表导入并管理列表排除项", "ParseModalHelpTextDetails": "{appName} 将尝试解析标题并向您显示有关详情", @@ -1443,21 +1443,21 @@ "ShowEpisodeInformationHelpText": "显示集号和标题", "ShowPath": "显示路径", "QualityProfileInUseSeriesListCollection": "无法删除已指定给剧集、列表、收藏的质量配置", - "QualityProfiles": "媒体质量配置", - "QualityProfilesLoadError": "无法加载质量配置文件", + "QualityProfiles": "质量配置", + "QualityProfilesLoadError": "无法加载质量配置", "RecentChanges": "最近修改", "RecyclingBinCleanupHelpText": "设置为0关闭自动清理", "RecyclingBinHelpText": "文件将在删除时移动到此处,而不是永久删除", "RegularExpressionsCanBeTested": "正则表达式可在[此处]({url})测试。", "SslPort": "SSL端口", "TablePageSizeMinimum": "页面大小必须至少为 {minimumValue}", - "TorrentDelayTime": "Torrent延时:{torrentDelay}", + "TorrentDelayTime": "种子延迟:{torrentDelay}", "Torrents": "种子", "TotalFileSize": "文件总大小", "TotalRecords": "记录总数: {totalRecords}", "Trace": "追踪", - "CutoffUnmetLoadError": "加载未达设定标准项目时出错", - "CutoffUnmetNoItems": "没有未达设定标准的项目", + "CutoffUnmetLoadError": "因加载未达阈值项出错", + "CutoffUnmetNoItems": "没有未达阈值的项目", "DeleteSeriesFolderHelpText": "删除剧集文件夹及其所含文件", "DeleteSeriesModalHeader": "删除 - {title}", "DeletedSeriesDescription": "剧集已从 TheTVDB 移除", @@ -1470,28 +1470,28 @@ "Files": "文件", "SeriesDetailsOneEpisodeFile": "1个集文件", "UrlBase": "基本URL", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {appName} 导入之前,已下载的文件会被从您的客户端中移除。", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {appName} 导入之前,已下载的文件会被您的客户端移除。", "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", "AutoRedownloadFailed": "重新下载失败", - "AutoRedownloadFailedFromInteractiveSearch": "手动搜索重新下载失败", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从手动搜索中获取失败的发行版时,自动搜索并尝试下载不同的发行版", + "AutoRedownloadFailedFromInteractiveSearch": "来自手动搜索的资源重新下载失败", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从手动搜索中抓取的发布资源下载失败时,自动搜索并尝试下载不同的发布资源", "ImportListSearchForMissingEpisodes": "搜索缺失集", - "QueueFilterHasNoItems": "选定的队列过滤器没有项目", + "QueueFilterHasNoItems": "所选的队列过滤器中无项目", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "确认新密码", "Category": "分类", "Destination": "目标", "Directory": "目录", - "DownloadClientDelugeSettingsUrlBaseHelpText": "向 deluge json url 添加前缀,请参阅 {url}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "向 Deluge JSON URL 添加前缀,请参阅 {url}", "DownloadClientDelugeValidationLabelPluginInactive": "标签插件未激活", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} 无法将标签添加到 {clientName}。", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "你需要在 {clientName} 启用标签插件才可以使用分类。", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的可选共享文件夹,留空以使用默认的 Download Station 位置", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "您必须在 {clientName} 中启用标签插件才可以使用分类。", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的共享文件夹可选择,留空使用 Download Station 默认位置", "DownloadClientDownloadStationValidationApiVersion": "Download Station API 版本不受支持,至少应为 {requiredVersion}。 它支持从 {minVersion} 到 {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "文件夹不存在", "DownloadClientDownloadStationValidationSharedFolderMissing": "共享文件夹不存在", - "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation 没有名为“{sharedFolder}”的共享文件夹,您确定您的指定是正确的吗?", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation 上没有名为 “{sharedFolder}” 的共享文件夹,您确定输入正确吗?", "DownloadClientFloodSettingsAdditionalTagsHelpText": "添加媒体属性作为标签。 提示是示例。", - "DownloadClientFloodSettingsRemovalInfo": "{appName} 将根据“设置”->“索引器”中的当前种子标准自动删除种子", + "DownloadClientFloodSettingsRemovalInfo": "{appName} 将根据“设置”->“索引器”中的当前做种规则自动删除种子", "DownloadClientFloodSettingsUrlBaseHelpText": "为 Flood API 添加前缀,例如 {url}", "DownloadClientFreeboxApiError": "Freebox API 返回错误:{errorDescription}", "DownloadClientFreeboxNotLoggedIn": "未登录", @@ -1502,12 +1502,12 @@ "DownloadClientFreeboxSettingsPortHelpText": "用于访问 Freebox 接口的端口,默认为 '{port}'", "DownloadClientFreeboxSettingsHostHelpText": "Freebox 的主机名或主机 IP 地址,默认为“{url}”(仅在同一网络上有效)", "DownloadClientNzbVortexMultipleFilesMessage": "下载包含多个文件且不在作业文件夹中:{outputPath}", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet 设置 KeepHistory 设置为 0。这会阻止 {appName} 查看已完成的下载。", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet 已将 KeepHistory 设置为 0。这会阻止 {appName} 查看已完成的下载。", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "按顺序下载文件(qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsUseSslHelpText": "使用安全连接。 请参阅 qBittorrent 中的「选项 -> Web UI -> “使用 HTTPS 而不是 HTTP”」。", "DownloadClientQbittorrentTorrentStateError": "qBittorrent 报告错误", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent 正在下载元数据", - "DownloadClientQbittorrentTorrentStatePathError": "无法导入。 路径与客户端基本下载目录匹配,可能为此 torrent 禁用了“保留顶级文件夹”或“Torrent 内容布局”未设置为“原始”或“创建子文件夹”?", + "DownloadClientQbittorrentTorrentStatePathError": "无法导入。路径与客户端的基础下载目录匹配,可能是该种子的 “保留顶级文件夹” 功能已禁用,或 “种子内容布局” 未设置为 “原始” 或 “创建子文件夹”?", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} 无法将标签添加到 qBittorrent。", "DownloadClientQbittorrentValidationCategoryRecommended": "推荐分类", "DownloadClientQbittorrentValidationCategoryUnsupported": "不支持分类", @@ -1516,21 +1516,21 @@ "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent 配置为在达到共享比率限制时删除种子", "DownloadClientRTorrentSettingsAddStopped": "添加后暂停", "DownloadClientRTorrentSettingsUrlPath": "URL 地址", - "DownloadClientRTorrentSettingsDirectoryHelpText": "用于放置下载的可选位置,留空以使用默认的 rTorrent 位置", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "使用“下载前检查”会影响 {appName} 跟踪新下载的能力。 Sabnzbd 还建议“中止无法完成的作业”,因为这样更有效。", + "DownloadClientRTorrentSettingsDirectoryHelpText": "用于放置下载的可选位置,留空使用 rTorrent 默认位置", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "使用 “下载前检查” 会影响 {appName} 跟踪新下载内容的能力。 此外,Sabnzbd 建议启用 “中止无法完成的任务”,因为这样更有效。", "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd开发版本,假设版本3.0.0或更高。", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "您必须禁用 {appName} 使用的分类的日期排序,以防止出现导入问题。 在 Sabnzbd 修复它。", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "禁用日期排序", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "禁用电影排序", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "禁止电视节目排序", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "您必须对 {appName} 使用的分类禁用电影排序,以防止出现导入问题。 在 Sabnzbd 修复它。", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "您必须对 {appName} 使用的分类禁用电影排序,以防止出现导入问题。 请前往 Sabnzbd 进行修复。", "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} 希望每次下载都有一个单独的文件夹。 如果文件夹/路径后附加 *,Sabnzbd 将不会创建这些作业文件夹。 在 Sabnzbd 修复它。", "DownloadClientSabnzbdValidationUnknownVersion": "未知版本:{rawVersion}", "DownloadClientSettingsDestinationHelpText": "手动指定下载目录,留空使用默认值", "DownloadClientSettingsRecentPriority": "最近优先", "DownloadClientSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}", "DownloadClientSettingsUseSslHelpText": "连接到 {clientName} 时使用安全连接", - "DownloadClientTransmissionSettingsDirectoryHelpText": "可选的下载位置,留空以使用默认传输位置", + "DownloadClientTransmissionSettingsDirectoryHelpText": "下载位置可选择,留空使用 Transmission 默认位置", "DownloadClientTransmissionSettingsUrlBaseHelpText": "向 {clientName} RPC URL 添加前缀,例如 {url},默认为 '{defaultUrl}'", "DownloadClientValidationApiKeyIncorrect": "API Key 不正确", "DownloadClientValidationApiKeyRequired": "需要 API Key", @@ -1538,7 +1538,7 @@ "DownloadClientValidationCategoryMissing": "分类不存在", "DownloadClientValidationCategoryMissingDetail": "您输入的分类在 {clientName} 中不存在。 请先在 {clientName} 中创建。", "DownloadClientValidationErrorVersion": "{clientName} 版本应至少为 {requiredVersion}。 而客户端报告的版本为 {reportedVersion}", - "DownloadClientValidationGroupMissingDetail": "您输入的组在 {clientName} 中不存在。 首先在 {clientName} 中创建它。", + "DownloadClientValidationGroupMissingDetail": "您输入的组在 {clientName} 中不存在。 请先在 {clientName} 中创建。", "DownloadClientValidationGroupMissing": "组不存在", "DownloadClientValidationTestNzbs": "无法获取 NZB 列表:{exceptionMessage}", "DownloadClientValidationSslConnectFailureDetail": "{appName} 无法使用 SSL 连接到 {clientName}。 此问题可能与计算机配置有关。 请尝试将 {appName} 和 {clientName} 配置为不使用 SSL。", @@ -1577,7 +1577,7 @@ "IndexerSettingsApiPathHelpText": "API 的路径,通常是 {url}", "IndexerSettingsCookie": "Cookie", "IndexerSettingsCategoriesHelpText": "下拉列表,留空以禁用标准/每日节目", - "IndexerSettingsSeedRatioHelpText": "种子在停止之前应达到的比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", + "IndexerSettingsSeedRatioHelpText": "停止之前应达到的做种比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", "IndexerValidationTestAbortedDueToError": "测试因错误而中止:{exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "索引器不支持所需的搜索参数", "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {exceptionMessage}。", @@ -1588,15 +1588,15 @@ "IndexerHDBitsSettingsCategories": "分类", "IndexerHDBitsSettingsCodecs": "编解码器", "DownloadClientDownloadStationProviderMessage": "如果您的 DSM 账户启用了二步认证,{appName} 将无法连接到 Download Station", - "DownloadClientDownloadStationValidationFolderMissingDetail": "文件夹“{downloadDir}”不存在,必须在共享文件夹“{sharedFolder}”内手动创建。", + "DownloadClientDownloadStationValidationFolderMissingDetail": "文件夹 “{downloadDir}” 不存在,必须在共享文件夹 “{sharedFolder}” 内手动创建。", "DownloadClientDownloadStationValidationNoDefaultDestination": "没有默认目标", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "您必须以 {username} 身份登录您的 Diskstation,并在 Download Station 中手动设置 BT/HTTP/FTP/NZB -> 位置。", "DownloadClientFloodSettingsAdditionalTags": "附加标签", "DownloadClientFloodSettingsPostImportTags": "导入后标签", "DownloadClientFloodSettingsPostImportTagsHelpText": "导入下载后附加标签。", "DownloadClientFloodSettingsStartOnAdd": "添加并开始", - "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。 要被识别,下载必须具有所有初始标签。 这可以避免与不相关的下载发生冲突。", - "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因 {errorDescription}", + "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。下载必须具有所有初始标签才可被识别。 这可以避免与不相关的下载发生冲突。", + "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因:{errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "API 地址", "DownloadClientFreeboxSettingsAppToken": "App Token", "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查“主机名”、“端口”或“使用 SSL”的设置(错误: {exceptionMessage})", @@ -1615,7 +1615,7 @@ "DownloadClientQbittorrentTorrentStateUnknown": "未知下载状态:{state}", "DownloadClientQbittorrentValidationCategoryAddFailure": "分类配置失败", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "qBittorrent 版本 3.3.0 之前不支持分类。 请升级后重试或者留空。", - "DownloadClientRTorrentProviderMessage": "当种子满足种子标准时,rTorrent 不会暂停种子。 仅当启用“删除已完成”时,{appName} 才会根据“设置”->“索引器”中的当前种子条件自动删除种子。 导入后,它还会将 {importedView} 设置为 rTorrent 视图,可以使用 rTorrent 脚本来自定义行为。", + "DownloadClientRTorrentProviderMessage": "当种子满足做种规则时,rTorrent 不会暂停做种。 仅当启用 “删除已完成项” 时,{appName} 才会根据 “设置 -> 索引器” 中的当前做种规则自动删除种子。 导入后,它还会将 {importedView} 设置为 rTorrent 视图,可以使用 rTorrent 脚本来自定义行为。", "DownloadClientRTorrentSettingsAddStoppedHelpText": "启用将在停止状态下向 rTorrent 添加 torrent 和磁力链接。 这可能会破坏磁力文件。", "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC 端点的路径,请参阅 {url}。 使用 ruTorrent 时,这通常是 RPC2 或 [ruTorrent 路径]{url2}。", "DownloadClientSabnzbdValidationCheckBeforeDownload": "禁用 Sabnbzd 中的“下载前检查”选项", @@ -1670,7 +1670,7 @@ "UsenetBlackhole": "Usenet 黑洞", "UsenetBlackholeNzbFolder": "Nzb 文件夹", "BlackholeWatchFolder": "监视文件夹", - "BlackholeWatchFolderHelpText": "{appName} 应从中导入已完成下载的文件夹", + "BlackholeWatchFolderHelpText": "{appName} 用来导入已完成下载的文件夹", "BlackholeFolderHelpText": "{appName} 将在其中存储 {extension} 文件的文件夹", "DownloadClientFreeboxUnableToReachFreeboxApi": "无法访问 Freebox API。 请检查“API 地址”的基础地址和版本。", "IndexerSettingsAllowZeroSize": "允许零大小", @@ -1680,14 +1680,14 @@ "IndexerSettingsApiUrlHelpText": "除非您知道自己在做什么,否则请勿更改此设置。 因为您的 API Key 将被发送到该主机。", "IndexerSettingsAllowZeroSizeHelpText": "启用此选项将允许您使用不指定发布大小的订阅源,但请注意,也将不会执行与大小相关的检查。", "NzbgetHistoryItemMessage": "PAR 状态:{parStatus} - 解压状态:{unpackStatus} - 移动状态:{moveStatus} - 脚本状态:{scriptStatus} - 删除状态:{deleteStatus} - 标记状态:{markStatus}", - "DownloadClientSettingsCategoryHelpText": "添加专用于 {appName} 的分类可以避免与不相关的非 {appName} 的下载发生冲突。 使用类别是可选的,但强烈建议使用。", + "DownloadClientSettingsCategoryHelpText": "添加专用于 {appName} 的分类可以避免与不相关的非 {appName} 的下载发生冲突。 使用分类是可选的,但强烈建议使用。", "ClearBlocklist": "清空黑名单", "DownloadClientQbittorrentTorrentStateStalled": "下载因无连接而停止", - "DownloadClientSettingsCategorySubFolderHelpText": "添加专用于 {appName} 的分类可以避免与不相关的非 {appName} 的下载发生冲突。 使用类别是可选的,但强烈建议使用。在输出目录中创建 [分类] 子目录。", - "ClearBlocklistMessageText": "你确定要将黑名单中的所有项目清空吗?", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} 不会尝试导入没有分类的已完成下载。", + "DownloadClientSettingsCategorySubFolderHelpText": "添加专用于 {appName} 的分类可以避免与不相关的非 {appName} 的下载发生冲突。 使用分类是可选的,但强烈建议使用。在输出目录中创建 [分类] 子目录。", + "ClearBlocklistMessageText": "您确认要将黑名单中的所有项目清空吗?", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} 不会尝试导入未分类的已完成下载。", "DownloadClientSettingsPostImportCategoryHelpText": "导入下载后要设置的 {appName} 的分类。 即使做种完成,{appName} 也不会删除该分类中的种子。 留空以保留同一分类。", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} 将无法按照配置执行完成下载处理。 您可以在 qBittorrent 中修复此问题(菜单中的“工具 -> 选项...”),方法是将“选项 -> BitTorrent -> 做种限制 -> 然后”从“删除 torrent”更改为“暂停 torrent”", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} 将无法按照 “已完成下载处理” 配置执行。 您可以在 qBittorrent 中修复此问题(菜单中的 “工具 -> 选项...”),通过将 “选项 -> BitTorrent -> 分享率限制” 从 “删除种子” 更改为 “暂停种子”", "DownloadClientDelugeTorrentStateError": "Deluge 正在报告错误", "TorrentBlackholeSaveMagnetFilesExtension": "保存磁力链接文件扩展名", "DownloadClientDelugeValidationLabelPluginFailure": "标签配置失败", @@ -1695,7 +1695,7 @@ "MonitorNewItems": "监控新项目", "AddRootFolderError": "无法添加根文件夹", "DownloadClientQbittorrentSettingsContentLayout": "内容布局", - "DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 配置的内容布局,使用种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 的已配置内容布局、种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)", "NotificationsAppriseSettingsUsernameHelpText": "HTTP Basic Auth 用户名", "NotificationsCustomScriptSettingsName": "自定义脚本", "NotificationsCustomScriptValidationFileDoesNotExist": "文件不存在", @@ -1704,13 +1704,13 @@ "NotificationsAppriseSettingsStatelessUrlsHelpText": "用逗号分隔的一个或多个URL,以标识应将通知发送到何处。如果使用持久化存储,则留空。", "NotificationsEmailSettingsName": "邮箱", "EpisodeFileMissingTooltip": "分集文件缺失", - "NotificationsAppriseSettingsTagsHelpText": "可选地,只对这些标签发送通知。", + "NotificationsAppriseSettingsTagsHelpText": "可选,仅通知已标记的项目。", "NotificationsCustomScriptSettingsArguments": "参数", "NotificationsCustomScriptSettingsArgumentsHelpText": "传给脚本的参数", - "NotificationsCustomScriptSettingsProviderMessage": "测试中会将 EventType 设为 {eventTypeTest} 后执行脚本,请确保其能正确处理。", + "NotificationsCustomScriptSettingsProviderMessage": "测试将 EventType 设为 {eventTypeTest} 后执行脚本,请确保您的脚本能够正确处理此设置", "NotificationsDiscordSettingsAuthor": "作者", - "NotificationsDiscordSettingsAuthorHelpText": "覆盖此通知中出现的作者,留空则为实例名称。", - "NotificationsDiscordSettingsAvatarHelpText": "更改此集成的消息所使用的头像", + "NotificationsDiscordSettingsAuthorHelpText": "覆盖此通知中出现的内嵌作者,留空则为实例名称", + "NotificationsDiscordSettingsAvatarHelpText": "更改此集成消息所使用的头像", "NotificationsDiscordSettingsUsernameHelpText": "发送消息所用的用户名,默认使用 Discord webhook 的缺省值", "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord 频道 webhook URL", "NotificationsEmailSettingsCcAddressHelpText": "电子邮件的 Cc 对象,以逗号分开", @@ -1723,7 +1723,7 @@ "NotificationsEmailSettingsServer": "服务器", "NotificationsEmailSettingsServerHelpText": "邮件服务器的主机名或 IP", "NotificationsEmbySettingsSendNotifications": "发送通知", - "NotificationsEmbySettingsSendNotificationsHelpText": "让 Emby 向配置的提供者发送通知。不支持 Jellyfin。", + "NotificationsEmbySettingsSendNotificationsHelpText": "让 Emby 向已配置的提供者发送通知。Jellyfin 不支持此功能。", "NotificationsEmbySettingsUpdateLibraryHelpText": "在导入、重命名、删除时更新库", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "在消息中包括剧集海报", "NotificationsGotifySettingIncludeSeriesPoster": "包括剧集海报", @@ -1752,8 +1752,8 @@ "NotificationsNotifiarrSettingsApiKeyHelpText": "个人资料中的 API Key", "NotificationsNtfySettingsAccessToken": "访问令牌", "NotificationsNtfySettingsAccessTokenHelpText": "可以选择令牌验证,并覆盖用户名、密码验证", - "NotificationsNtfySettingsClickUrl": "通知链接", - "NotificationsNtfySettingsClickUrlHelpText": "点击通知时打开的链接,可选", + "NotificationsNtfySettingsClickUrl": "点击 URL", + "NotificationsNtfySettingsClickUrlHelpText": "点击通知时打开的链接可选择", "NotificationsNtfySettingsServerUrl": "服务器 URL", "NotificationsNtfySettingsPasswordHelpText": "密码,可选", "NotificationsPushBulletSettingSenderIdHelpText": "发送通知的设备 ID,使用 pushbullet.com 设备 URL 中的 device_iden 参数值,或者留空来自行发送", @@ -1770,41 +1770,41 @@ "NotificationsAppriseSettingsTags": "Apprise 标签", "NotificationsNtfySettingsServerUrlHelpText": "留空使用公共服务器 {url}", "NotificationsNtfySettingsTagsEmojis": "Ntfy 标签和 emoji", - "NotificationsNtfySettingsTagsEmojisHelpText": "附加标签或 emoji,可选", + "NotificationsNtfySettingsTagsEmojisHelpText": "可选择使用的标签或表情符号列表", "NotificationsNtfySettingsTopics": "话题", - "NotificationsNtfySettingsTopicsHelpText": "通知的目标话题,可选", + "NotificationsNtfySettingsTopicsHelpText": "用于发送通知的主题列表", "NotificationsNtfySettingsUsernameHelpText": "用户名,可选", "NotificationsNtfyValidationAuthorizationRequired": "需要授权", "NotificationsPlexSettingsAuthToken": "验证令牌", "NotificationsPlexSettingsAuthenticateWithPlexTv": "使用 Plex.tv 验证身份", "NotificationsPlexValidationNoTvLibraryFound": "需要至少一个电视资源库", - "NotificationsPushBulletSettingSenderId": "发送 ID", - "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", + "NotificationsPushBulletSettingSenderId": "发送者 ID", + "DownloadClientAriaSettingsDirectoryHelpText": "下载位置可选择,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", - "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", + "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时拒绝列入黑名单的种子散列值", "ChangeCategory": "修改分类", "IgnoreDownload": "忽略下载", "IgnoreDownloads": "忽略下载", "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", "DoNotBlocklist": "不要列入黑名单", "DoNotBlocklistHint": "删除而不列入黑名单", - "RemoveQueueItemRemovalMethod": "删除方法", + "RemoveQueueItemRemovalMethod": "移除方法", "BlocklistAndSearch": "黑名单和搜索", - "BlocklistAndSearchMultipleHint": "列入黑名单后开始搜索替代版本", + "BlocklistAndSearchMultipleHint": "列入黑名单后开始多次搜索替换", "BlocklistMultipleOnlyHint": "无需搜索替换的黑名单", "BlocklistOnly": "仅限黑名单", - "BlocklistOnlyHint": "无需寻找替代版本的黑名单", + "BlocklistOnlyHint": "无需一次搜索替换的黑名单", "ChangeCategoryMultipleHint": "将下载从下载客户端更改为“导入后类别”", "CustomFormatsSpecificationRegularExpressionHelpText": "自定义格式正则表达式不区分大小写", "CustomFormatsSpecificationRegularExpression": "正则表达式", - "RemoveFromDownloadClientHint": "从下载客户端删除下载和文件", + "RemoveFromDownloadClientHint": "从下载客户端中移除下载记录和文件", "RemoveMultipleFromDownloadClientHint": "从下载客户端删除下载和文件", - "RemoveQueueItemsRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", - "BlocklistAndSearchHint": "列入黑名单后开始寻找一个替代版本", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "“从下载客户端中移除” 将从下载客户端中移除下载记录和文件。", + "BlocklistAndSearchHint": "列入黑名单后开始一次搜索替换", "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", - "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "即使种子的散列值被列入黑名单,某些索引器在 RSS 同步或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取种子之后但在将其发送到下载客户端之前拒绝它。", + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端中移除”将从下载客户端中移除下载记录和文件。", "AutoTaggingSpecificationOriginalLanguage": "语言", "CustomFormatsSpecificationFlag": "标记", "CustomFormatsSpecificationLanguage": "语言", @@ -1826,12 +1826,126 @@ "AutoTaggingSpecificationSeriesType": "系列类型", "AutoTaggingSpecificationStatus": "状态", "ClickToChangeIndexerFlags": "点击修改索引器标志", - "ConnectionSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}", + "ConnectionSettingsUrlBaseHelpText": "向 {connectionName} URL 添加前缀,例如 {url}", "BlocklistFilterHasNoItems": "所选的黑名单过滤器没有项目", "CustomFormatsSpecificationReleaseGroup": "发布组", "MetadataSettingsSeriesMetadata": "季元数据", "CustomFormatsSpecificationResolution": "分辨率", "CustomFormatsSpecificationSource": "来源", "ClickToChangeReleaseType": "点击更改发布类型", - "CustomFormatsSettingsTriggerInfo": "当一个发布版本或文件至少匹配其中一个条件时,自定义格式将会被应用到这个版本或文件上。" + "CustomFormatsSettingsTriggerInfo": "当一个发布版本或文件至少匹配其中一个条件时,自定义格式将会被应用到此版本或文件上。", + "NotificationsTraktSettingsAuthenticateWithTrakt": "使用 Trakt 进行认证", + "NotificationsTwitterSettingsConsumerKey": "Consumer 密钥", + "TodayAt": "今天 {time}", + "TomorrowAt": "明天 {time}", + "NotificationsAppriseSettingsServerUrl": "Apprise 服务器 URL", + "NotificationsSignalSettingsSenderNumber": "发送者号码", + "NotificationsSlackSettingsChannel": "渠道", + "NotificationsTelegramSettingsSendSilently": "静默发送", + "CountCustomFormatsSelected": "已选择 {count} 自定义格式", + "EditSelectedCustomFormats": "编辑选择的自定义格式", + "ManageCustomFormats": "管理自定义格式", + "NoCustomFormatsFound": "未找到自定义格式", + "NotificationsAppriseSettingsConfigurationKey": "Apprise 配置密钥", + "NotificationsSettingsWebhookMethodHelpText": "向 Web 服务提交数据时使用哪种 HTTP 方法", + "NotificationsTelegramSettingsTopicId": "主题 ID", + "NotificationsValidationInvalidApiKeyExceptionMessage": "API 密钥无效: {exceptionMessage}", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "更改用于“抓取”通知的字段", + "NotificationsGotifySettingsAppToken": "App Token", + "NotificationsPushoverSettingsRetry": "重试", + "NotificationsPushoverSettingsDevices": "设备", + "NotificationsPushoverSettingsUserKey": "用户密钥", + "NotificationsSendGridSettingsApiKeyHelpText": "SendGrid 生成的 API 密钥", + "NotificationsSettingsUpdateMapPathsFrom": "路径映射自", + "NotificationsSettingsUpdateMapPathsTo": "路径映射到", + "NotificationsSlackSettingsIcon": "图标", + "NotificationsTelegramSettingsTopicIdHelpText": "指定一个主题 ID 以将通知发送到该主题。如果留空,则使用通用主题(仅限超级群组)", + "NotificationsValidationInvalidUsernamePassword": "用户名或密码无效", + "NotificationsPushcutSettingsTimeSensitive": "紧急", + "NotificationsSettingsUpdateLibrary": "更新资源库", + "NotificationsPushoverSettingsSound": "声音", + "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", + "NotificationsSettingsUseSslHelpText": "使用 HTTPS 而非 HTTP 连接至 {serviceName}", + "NotificationsSignalSettingsSenderNumberHelpText": "发送者在 Signal API 中注册的电话号码", + "NotificationsSlackSettingsChannelHelpText": "覆盖传入 Webhook 的默认渠道(#other-channel)", + "NotificationsSettingsWebhookUrl": "Webhook URL", + "NotificationsSlackSettingsUsernameHelpText": "在 Slack 上发布时使用的用户名", + "NotificationsSlackSettingsWebhookUrlHelpText": "Slack 频道的 Webhook URL", + "NotificationsTelegramSettingsChatIdHelpText": "您必须与机器人开始对话或将其添加到您的群组中,以接收消息", + "NotificationsTelegramSettingsBotToken": "机器人 Token", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "可选,在消息标题前加上 {appName} 以区分来自不同应用的通知", + "NotificationsTraktSettingsAuthUser": "认证用户", + "NotificationsTraktSettingsExpires": "过期时间", + "NotificationsTwitterSettingsAccessTokenSecret": "访问 Token Secret", + "NotificationsTwitterSettingsConnectToTwitter": "连接至 Twitter/X", + "NotificationsTwitterSettingsConsumerKeyHelpText": "来自 Twitter 应用的 Consumer 密钥", + "NotificationsTwitterSettingsConsumerSecretHelpText": "来自 Twitter 应用的 Consumer Secret", + "NotificationsValidationUnableToSendTestMessage": "无法发送测试消息:{exceptionMessage}", + "SelectIndexerFlags": "选择索引器标志", + "NotificationsDiscordSettingsOnManualInteractionFields": "手动操作时字段", + "NotificationsEmailSettingsUseEncryption": "启用加密", + "ReleaseGroupFootNote": "可选,控制使用省略号(`...`)的最大截断字数。支持从末尾截断(例如:`{Release Group:30}`)或从开头截断(例如:`{Release Group:-30}`)。", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "启用以将通知标记为 “紧急”", + "NotificationsSimplepushSettingsKey": "密钥", + "NotificationsTraktSettingsRefreshToken": "刷新 Token", + "CountVotes": "{votes} 票", + "NotificationsPushoverSettingsExpire": "过期", + "NotificationsPushoverSettingsExpireHelpText": "紧急警报的最大重试时间,最长为86400秒(24小时)", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "组 ID/接收者的电话号码", + "NotificationsSignalSettingsGroupIdPhoneNumber": "组 ID/电话号码", + "NotificationsTelegramSettingsIncludeAppName": "标题中包含 {appName}", + "NotificationsSynologySettingsUpdateLibraryHelpText": "在本地调用 synoindex 以更新库文件", + "NotificationsValidationInvalidHttpCredentials": "HTTP 认证凭据无效:{exceptionMessage}", + "NotificationsSignalValidationSslRequired": "似乎需要启用 SSL", + "NotificationsValidationUnableToSendTestMessageApiResponse": "无法发送测试消息。来自 API 的响应:{error}", + "SetIndexerFlagsModalTitle": "{modalTitle} - 设置索引器标志", + "NoBlocklistItems": "无黑名单项目", + "DownloadClientDelugeSettingsDirectoryHelpText": "下载位置可选择,留空使用 Deluge 默认位置", + "IndexerSettingsMultiLanguageReleaseHelpText": "此索引器的多版本中通常包含哪些语言?", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "更改用于“手动操作”通知的字段", + "NotificationsTwitterSettingsConsumerSecret": "Consumer Secret", + "NotificationsTwitterSettingsDirectMessage": "私信", + "NotificationsTwitterSettingsDirectMessageHelpText": "发送私信而非公共消息", + "YesterdayAt": "昨天 {time}", + "DownloadClientDelugeSettingsDirectoryCompleted": "完成后移动至目录", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "下载完成后移动至可选位置,留空使用 Deluge 默认位置", + "DownloadClientDelugeSettingsDirectory": "下载目录", + "IndexerFlags": "索引器标志", + "NotificationsPushoverSettingsSoundHelpText": "通知声音,留空使用默认声音", + "NotificationsSignalSettingsUsernameHelpText": "用于认证 Signal API 请求的用户名", + "NotificationsSynologyValidationTestFailed": "不是 Synology(群辉)设备或 synoindex 不可用", + "NotificationsTelegramSettingsChatId": "聊天 ID", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent 正在报告缺失的文件", + "NotificationsSimplepushSettingsEventHelpText": "自定义推送通知行为", + "NotificationsSettingsWebhookMethod": "方法", + "NotificationsSignalSettingsPasswordHelpText": "用于认证 Signal API 请求的密码", + "NotificationsAppriseSettingsNotificationType": "Apprise 通知类型", + "NotificationsEmailSettingsUseEncryptionHelpText": "是否优先使用加密(如果服务器已配置),始终使用通过SSL(仅端口465)或StartTLS(任何其他端口)进行加密,或从不使用加密", + "SetIndexerFlags": "设置索引器标志", + "UnableToImportAutomatically": "无法自动导入", + "DeleteSelectedImportListExclusionsMessageText": "您确认要删除选中的导入排除列表吗?", + "LabelIsRequired": "需要标签", + "NotificationsPlexSettingsServerHelpText": "认证后从 plex.tv 账户中选择服务器", + "NotificationsValidationInvalidAuthenticationToken": "认证 Token 无效", + "NotificationsValidationInvalidAccessToken": "访问 Token 无效", + "NotificationsValidationUnableToConnect": "无法连接:{exceptionMessage}", + "NotificationsValidationInvalidApiKey": "API 密钥无效", + "NotificationsDiscordSettingsOnImportFields": "导入时字段", + "NotificationsSynologyValidationInvalidOs": "必须是 Synology(群辉)设备", + "NotificationsValidationUnableToConnectToApi": "无法连接至 {service} API。服务器连接失败:({responseCode}) {exceptionMessage}", + "NotificationsValidationUnableToConnectToService": "无法连接至 {serviceName}", + "ReleaseProfileIndexerHelpTextWarning": "在发布资源配置中,设置特定索引器将使该配置仅用于来自该索引器的资源。", + "DayOfWeekAt": "{day} {time}", + "NotificationsDiscordSettingsOnGrabFields": "抓取时字段", + "LogSizeLimit": "日志大小限制", + "LogSizeLimitHelpText": "存档前的最大日志文件大小(MB)。默认值为 1 MB。", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "更改用于“导入”通知的字段", + "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", + "NotificationsPushoverSettingsRetryHelpText": "紧急警报的重试间隔,最少 30 秒", + "NotificationsSlackSettingsIconHelpText": "更改用于发送到 Slack 的消息图标(表情符号或 URL)", + "NotificationsTelegramSettingsSendSilentlyHelpText": "静默发送消息。用户将收到没有声音的通知", + "NotificationsTwitterSettingsMentionHelpText": "在发送的推文中提及此用户", + "NotificationsTwitterSettingsMention": "提及", + "ShowTags": "显示标签", + "ShowTagsHelpText": "在海报下显示标签" } From 24f03fc1e96eba215f96312c791cf167f10499c7 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sun, 15 Sep 2024 12:19:08 -0500 Subject: [PATCH 524/762] Add 'qualitydefinition/limits' endpoint to get size limitations --- .gitignore | 3 + .../QualityDefinitionResourceValidatorTest.cs | 198 ++++++++++++++++++ .../Qualities/QualityDefinitionLimits.cs | 7 + .../Qualities/QualityDefinitionController.cs | 23 +- .../QualityDefinitionLimitsResource.cs | 6 + .../QualityDefinitionResourceValidator.cs | 31 +++ src/Sonarr.Http/REST/RestController.cs | 12 +- 7 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs create mode 100644 src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs create mode 100644 src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs diff --git a/.gitignore b/.gitignore index 4094c46a6..d17209556 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ src/.idea/ # API doc generation .config/ + +# Ignore Jetbrains IntelliJ Workspace Directories +.idea/ diff --git a/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs new file mode 100644 index 000000000..a331de82a --- /dev/null +++ b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs @@ -0,0 +1,198 @@ +using FluentValidation.TestHelper; +using NUnit.Framework; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Qualities; + +namespace NzbDrone.Api.Test.v3.Qualities; + +[Parallelizable(ParallelScope.All)] +public class QualityDefinitionResourceValidatorTests +{ + private readonly QualityDefinitionResourceValidator _validator = new (); + + [Test] + public void Validate_fails_when_min_size_is_below_min_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = QualityDefinitionLimits.Min - 1, + PreferredSize = null, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_fails_when_min_size_is_above_preferred_size_and_below_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = 10, + PreferredSize = 5, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("LessThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_min_size_is_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = QualityDefinitionLimits.Min, + PreferredSize = null, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_max_size_is_below_preferred_size_and_above_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = 10, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("LessThanOrEqualTo"); + } + + [Test] + public void Validate_fails_when_max_size_exceeds_max_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = null, + MaxSize = QualityDefinitionLimits.Max + 1 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("LessThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_max_size_is_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = null, + MaxSize = QualityDefinitionLimits.Max + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_preferred_size_is_below_min_size_and_above_max_size() + { + var resource = new QualityDefinitionResource + { + MinSize = 10, + PreferredSize = 7, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_preferred_size_is_null_and_other_sizes_are_valid() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = null, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_passes_when_preferred_size_equals_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = 5, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_all_sizes_are_provided_and_invalid() + { + var resource = new QualityDefinitionResource + { + MinSize = 15, + PreferredSize = 10, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("LessThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_preferred_size_is_valid_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = 7, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs new file mode 100644 index 000000000..418dbc837 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Qualities; + +public static class QualityDefinitionLimits +{ + public const int Min = 0; + public const int Max = 1000; +} diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs index 5910ee896..e0e26d7cb 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs @@ -12,14 +12,21 @@ using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Qualities { [V3ApiController] - public class QualityDefinitionController : RestControllerWithSignalR, IHandle + public class QualityDefinitionController : + RestControllerWithSignalR, + IHandle { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) + public QualityDefinitionController( + IQualityDefinitionService qualityDefinitionService, + IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { _qualityDefinitionService = qualityDefinitionService; + + SharedValidator.RuleFor(c => c) + .SetValidator(new QualityDefinitionResourceValidator()); } [RestPutById] @@ -45,9 +52,7 @@ namespace Sonarr.Api.V3.Qualities public object UpdateMany([FromBody] List resource) { // Read from request - var qualityDefinitions = resource - .ToModel() - .ToList(); + var qualityDefinitions = resource.ToModel().ToList(); _qualityDefinitionService.UpdateMany(qualityDefinitions); @@ -55,6 +60,14 @@ namespace Sonarr.Api.V3.Qualities .ToResource()); } + [HttpGet("limits")] + public ActionResult GetLimits() + { + return Ok(new QualityDefinitionLimitsResource( + QualityDefinitionLimits.Min, + QualityDefinitionLimits.Max)); + } + [NonAction] public void Handle(CommandExecutedEvent message) { diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs new file mode 100644 index 000000000..1ccacbf28 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs @@ -0,0 +1,6 @@ +namespace Sonarr.Api.V3.Qualities; + +// SA1313 still applies to records until https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3181 is available in a release build. +#pragma warning disable SA1313 +public record QualityDefinitionLimitsResource(int Min, int Max); +#pragma warning restore SA1313 diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs new file mode 100644 index 000000000..dfe5b82b5 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using NzbDrone.Core.Qualities; + +namespace Sonarr.Api.V3.Qualities; + +public class QualityDefinitionResourceValidator : AbstractValidator +{ + public QualityDefinitionResourceValidator() + { + RuleFor(c => c.MinSize) + .GreaterThanOrEqualTo(QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.MinSize is not null); + + RuleFor(c => c.PreferredSize) + .GreaterThanOrEqualTo(c => c.MinSize ?? QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(c => c.MaxSize ?? QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.PreferredSize is not null); + + RuleFor(c => c.MaxSize) + .GreaterThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.MaxSize is not null); + } +} diff --git a/src/Sonarr.Http/REST/RestController.cs b/src/Sonarr.Http/REST/RestController.cs index 7632d8b7f..e08790a45 100644 --- a/src/Sonarr.Http/REST/RestController.cs +++ b/src/Sonarr.Http/REST/RestController.cs @@ -70,11 +70,15 @@ namespace Sonarr.Http.REST var skipValidate = skipAttribute?.Skip ?? false; var skipShared = skipAttribute?.SkipShared ?? false; - if (Request.Method == "POST" || Request.Method == "PUT") + if (Request.Method is "POST" or "PUT") { - var resourceArgs = context.ActionArguments.Values.Where(x => x.GetType() == typeof(TResource)) - .Select(x => x as TResource) - .ToList(); + var resourceArgs = context.ActionArguments.Values + .SelectMany(x => x switch + { + TResource single => new[] { single }, + IEnumerable multiple => multiple, + _ => Enumerable.Empty() + }); foreach (var resource in resourceArgs) { From 8b20a9449c1ae5ffd1e8d12f1ca771727b8c52a5 Mon Sep 17 00:00:00 2001 From: somniumV <179984073+somniumV@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:20:03 +0200 Subject: [PATCH 525/762] New: Minimum Upgrade Score for Custom Formats Closes #6800 --- .../Quality/EditQualityProfileModalContent.js | 20 ++++ frontend/src/typings/QualityProfile.ts | 1 + .../UpgradeSpecificationFixture.cs | 103 +++++++++++++++++- ...pgrade_format_score_to_quality_profiles.cs | 14 +++ .../Specifications/UpgradableSpecification.cs | 11 ++ .../UpgradeDiskSpecification.cs | 3 + .../DecisionEngine/UpgradeableRejectReason.cs | 3 +- .../CleanupQualityProfileFormatItems.cs | 3 +- src/NzbDrone.Core/Localization/Core/en.json | 2 + .../Profiles/Qualities/QualityProfile.cs | 1 + .../Qualities/QualityProfileService.cs | 2 + .../Quality/QualityProfileController.cs | 5 +- .../Quality/QualityProfileResource.cs | 3 + src/Sonarr.Api.V3/openapi.json | 4 + 14 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/212_add_minium_upgrade_format_score_to_quality_profiles.cs diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index ece0e8728..947b2ff54 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -123,6 +123,7 @@ class EditQualityProfileModalContent extends Component { upgradeAllowed, cutoff, minFormatScore, + minUpgradeFormatScore, cutoffFormatScore, items, formatItems @@ -244,6 +245,25 @@ class EditQualityProfileModalContent extends Component { } + { + upgradeAllowed.value && formatItems.value.length > 0 ? + + + {translate('MinimumCustomFormatScoreIncrement')} + + + + : + null + } +
{getCustomFormatRender(formatItems, otherProps)}
diff --git a/frontend/src/typings/QualityProfile.ts b/frontend/src/typings/QualityProfile.ts index ec4e46648..41063cb3e 100644 --- a/frontend/src/typings/QualityProfile.ts +++ b/frontend/src/typings/QualityProfile.ts @@ -16,6 +16,7 @@ interface QualityProfile { items: QualityProfileQualityItem[]; minFormatScore: number; cutoffFormatScore: number; + minUpgradeFormatScore: number; formatItems: QualityProfileFormatItem[]; id: number; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index 9bf70fa13..e3a7a71ee 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -79,9 +80,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests GivenAutoDownloadPropers(ProperDownloadTypes.DoNotPrefer); var profile = new QualityProfile - { - Items = Qualities.QualityFixture.GetDefaultQualities(), - }; + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; Subject.IsUpgradable( profile, @@ -96,9 +97,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_return_false_if_release_and_existing_file_are_the_same() { var profile = new QualityProfile - { - Items = Qualities.QualityFixture.GetDefaultQualities() - }; + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }; Subject.IsUpgradable( profile, @@ -146,5 +147,95 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List()) .Should().Be(UpgradeableRejectReason.QualityCutoff); } + + [Test] + public void should_return_false_if_minimum_custom_score_is_not_met() + { + var customFormatOne = new CustomFormat + { + Id = 1, + Name = "One" + }; + + var customFormatTwo = new CustomFormat + { + Id = 2, + Name = "Two" + }; + + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + MinUpgradeFormatScore = 11, + CutoffFormatScore = 100, + FormatItems = new List + { + new ProfileFormatItem + { + Format = customFormatOne, + Score = 10 + }, + new ProfileFormatItem + { + Format = customFormatTwo, + Score = 20 + } + } + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.DVD), + new List { customFormatOne }, + new QualityModel(Quality.DVD), + new List { customFormatTwo }) + .Should().Be(UpgradeableRejectReason.MinCustomFormatScore); + } + + [Test] + public void should_return_true_if_minimum_custom_score_is_met() + { + var customFormatOne = new CustomFormat + { + Id = 1, + Name = "One" + }; + + var customFormatTwo = new CustomFormat + { + Id = 2, + Name = "Two" + }; + + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + MinUpgradeFormatScore = 10, + CutoffFormatScore = 100, + FormatItems = new List + { + new ProfileFormatItem + { + Format = customFormatOne, + Score = 10 + }, + new ProfileFormatItem + { + Format = customFormatTwo, + Score = 20 + } + } + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.DVD), + new List { customFormatOne }, + new QualityModel(Quality.DVD), + new List { customFormatTwo }) + .Should().Be(UpgradeableRejectReason.None); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/212_add_minium_upgrade_format_score_to_quality_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/212_add_minium_upgrade_format_score_to_quality_profiles.cs new file mode 100644 index 000000000..a6cb5492b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/212_add_minium_upgrade_format_score_to_quality_profiles.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(212)] + public class add_minium_upgrade_format_score_to_quality_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("QualityProfiles").AddColumn("MinUpgradeFormatScore").AsInt32().WithDefaultValue(1); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index c4ecbd19a..d0eb1603a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -95,6 +95,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return UpgradeableRejectReason.CustomFormatCutoff; } + if (newFormatScore < currentFormatScore + qualityProfile.MinUpgradeFormatScore) + { + _logger.Debug("New item's custom formats [{0}] ({1}) do not meet minimum custom format score increment of {3} required for upgrade, skipping. Existing: [{4}] ({5}).", + newCustomFormats.ConcatToString(), + newFormatScore, + qualityProfile.MinUpgradeFormatScore, + currentCustomFormats.ConcatToString(), + currentFormatScore); + return UpgradeableRejectReason.MinCustomFormatScore; + } + _logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting", newCustomFormats.ConcatToString(), newFormatScore, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 15168e15f..c316a2fb7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -77,6 +77,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications case UpgradeableRejectReason.CustomFormatScore: return Decision.Reject("Existing file on disk has a equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); + + case UpgradeableRejectReason.MinCustomFormatScore: + return Decision.Reject("Existing file differential between new release does not meet minimum Custom Format score increment: {0}", qualityProfile.MinFormatScore); } } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs index 7ed6d6a0f..2b1b1cfe9 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.DecisionEngine BetterRevision, QualityCutoff, CustomFormatScore, - CustomFormatCutoff + CustomFormatCutoff, + MinCustomFormatScore } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs index 3c8cef581..a08f52aa7 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs @@ -65,6 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } updatedProfiles.Add(profile); @@ -73,7 +74,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers if (updatedProfiles.Any()) { - _repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore); + _repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore, p => p.MinUpgradeFormatScore); } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index aba736a67..8d58e8c9b 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1176,6 +1176,8 @@ "MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.", "MinimumCustomFormatScore": "Minimum Custom Format Score", "MinimumCustomFormatScoreHelpText": "Minimum custom format score allowed to download", + "MinimumCustomFormatScoreIncrement": "Minimum Custom Format Score Increment", + "MinimumCustomFormatScoreIncrementHelpText": "Minimum required improvement of the custom format score between existing and new releases before {appName} considers it an upgrade", "MinimumFreeSpace": "Minimum Free Space", "MinimumFreeSpaceHelpText": "Prevent import if it would leave less than this amount of disk space available", "MinimumLimits": "Minimum Limits", diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs index f8a214042..68eecfb49 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Profiles.Qualities public int Cutoff { get; set; } public int MinFormatScore { get; set; } public int CutoffFormatScore { get; set; } + public int MinUpgradeFormatScore { get; set; } public List FormatItems { get; set; } public List Items { get; set; } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index 67b7dc18f..c53362b47 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -174,6 +174,7 @@ namespace NzbDrone.Core.Profiles.Qualities { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } Update(profile); @@ -232,6 +233,7 @@ namespace NzbDrone.Core.Profiles.Qualities Items = items, MinFormatScore = 0, CutoffFormatScore = 0, + MinUpgradeFormatScore = 1, FormatItems = formatItems }; diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs index 46a1c6730..ac75f7fb0 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -15,20 +15,19 @@ namespace Sonarr.Api.V3.Profiles.Quality public class QualityProfileController : RestController { private readonly IQualityProfileService _profileService; - private readonly ICustomFormatService _formatService; public QualityProfileController(IQualityProfileService profileService, ICustomFormatService formatService) { _profileService = profileService; - _formatService = formatService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.MinUpgradeFormatScore).GreaterThanOrEqualTo(1); SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); SharedValidator.RuleFor(c => c.Items).ValidItems(); SharedValidator.RuleFor(c => c.FormatItems).Must(items => { - var all = _formatService.All().Select(f => f.Id).ToList(); + var all = formatService.All().Select(f => f.Id).ToList(); var ids = items.Select(i => i.Format); return all.Except(ids).Empty(); diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs index 8f7fef948..e0707b7f8 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -15,6 +15,7 @@ namespace Sonarr.Api.V3.Profiles.Quality public List Items { get; set; } public int MinFormatScore { get; set; } public int CutoffFormatScore { get; set; } + public int MinUpgradeFormatScore { get; set; } public List FormatItems { get; set; } } @@ -56,6 +57,7 @@ namespace Sonarr.Api.V3.Profiles.Quality Items = model.Items.ConvertAll(ToResource), MinFormatScore = model.MinFormatScore, CutoffFormatScore = model.CutoffFormatScore, + MinUpgradeFormatScore = model.MinUpgradeFormatScore, FormatItems = model.FormatItems.ConvertAll(ToResource) }; } @@ -103,6 +105,7 @@ namespace Sonarr.Api.V3.Profiles.Quality Items = resource.Items.ConvertAll(ToModel), MinFormatScore = resource.MinFormatScore, CutoffFormatScore = resource.CutoffFormatScore, + MinUpgradeFormatScore = resource.MinUpgradeFormatScore, FormatItems = resource.FormatItems.ConvertAll(ToModel) }; } diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index b4f752920..399e3f6ed 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10673,6 +10673,10 @@ "type": "integer", "format": "int32" }, + "minUpgradeFormatScore": { + "type": "integer", + "format": "int32" + }, "formatItems": { "type": "array", "items": { From f20ac9dc348e1f5ded635f12ab925d982b1b8957 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 4 Sep 2024 21:02:58 -0700 Subject: [PATCH 526/762] Fixed: Series links not opening on iOS --- frontend/src/Components/Tooltip/Tooltip.tsx | 25 +++++++++++++------ .../src/Series/Details/SeriesDetailsLinks.css | 7 ++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/src/Components/Tooltip/Tooltip.tsx b/frontend/src/Components/Tooltip/Tooltip.tsx index c3d955ad2..43150c755 100644 --- a/frontend/src/Components/Tooltip/Tooltip.tsx +++ b/frontend/src/Components/Tooltip/Tooltip.tsx @@ -34,7 +34,7 @@ function Tooltip(props: TooltipProps) { canFlip = false, } = props; - const closeTimeout = useRef(0); + const closeTimeout = useRef>(); const updater = useRef<(() => void) | null>(null); const [isOpen, setIsOpen] = useState(false); @@ -48,16 +48,25 @@ function Tooltip(props: TooltipProps) { }); }, [setIsOpen]); - const handleMouseEnter = useCallback(() => { + const handleMouseEnterAnchor = useCallback(() => { // Mobile will fire mouse enter and click events rapidly, // this causes the tooltip not to open on the first press. // Ignore the mouse enter event on mobile. + if (isMobileUtil()) { return; } if (closeTimeout.current) { - window.clearTimeout(closeTimeout.current); + clearTimeout(closeTimeout.current); + } + + setIsOpen(true); + }, [setIsOpen]); + + const handleMouseEnterTooltip = useCallback(() => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); } setIsOpen(true); @@ -65,7 +74,9 @@ function Tooltip(props: TooltipProps) { const handleMouseLeave = useCallback(() => { // Still listen for mouse leave on mobile to allow clicks outside to close the tooltip. - closeTimeout.current = window.setTimeout(() => { + + clearTimeout(closeTimeout.current); + closeTimeout.current = setTimeout(() => { setIsOpen(false); }, 100); }, [setIsOpen]); @@ -118,7 +129,7 @@ function Tooltip(props: TooltipProps) { useEffect(() => { return () => { if (closeTimeout.current) { - window.clearTimeout(closeTimeout.current); + clearTimeout(closeTimeout.current); } }; }, []); @@ -131,7 +142,7 @@ function Tooltip(props: TooltipProps) { ref={ref} className={className} onClick={handleClick} - onMouseEnter={handleMouseEnter} + onMouseEnter={handleMouseEnterAnchor} onMouseLeave={handleMouseLeave} > {anchor} @@ -181,7 +192,7 @@ function Tooltip(props: TooltipProps) { : styles.horizontalContainer )} style={style} - onMouseEnter={handleMouseEnter} + onMouseEnter={handleMouseEnterTooltip} onMouseLeave={handleMouseLeave} >
Date: Thu, 5 Sep 2024 20:39:01 -0700 Subject: [PATCH 527/762] Fixed: Don't parse language from series title for v2 releases Closes #7182 --- src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 10 ++++++++++ src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index a87010eb9..18259eaff 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -100,5 +101,14 @@ namespace NzbDrone.Core.Test.ParserTests var seriesTitleInfo = Parser.Parser.ParseTitle(postTitle).SeriesTitleInfo; seriesTitleInfo.AllTitles.Should().BeEquivalentTo(titles); } + + [TestCase("[Reza] Series in Russian - S01E08 [WEBRip 1080p HEVC AAC] (Dual Audio) (Tokidoki Bosotto Russiago de Dereru Tonari no Alya-san)", "Unknown")] + public void should_parse_language_after_parsing_title(string postTitle, string expectedLanguage) + { + var result = Parser.Parser.ParseTitle(postTitle); + + result.Languages.Count.Should().Be(1); + result.Languages.Should().Contain((Language)expectedLanguage); + } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 4ea515ef2..3528cc080 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -767,7 +767,7 @@ namespace NzbDrone.Core.Parser result.Special = true; } - result.Languages = LanguageParser.ParseLanguages(releaseTitle); + result.Languages = LanguageParser.ParseLanguages(result.ReleaseTokens); Logger.Debug("Languages parsed: {0}", string.Join(", ", result.Languages)); result.Quality = QualityParser.ParseQuality(title); From a929548ae38d2c5504b961f22131897508d26470 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 5 Sep 2024 14:54:39 +0300 Subject: [PATCH 528/762] Fixed: Linking autotags with tag specification to all tags --- src/NzbDrone.Core/Tags/TagService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index b97b279a5..fb9d6712d 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -198,7 +198,7 @@ namespace NzbDrone.Core.Tags { foreach (var specification in autoTag.Specifications) { - if (specification is TagSpecification) + if (specification is TagSpecification tagSpecification && tagSpecification.Value == tag.Id) { autoTagIds.Add(autoTag.Id); } From 6a332b40ac94e6e7c23217074da8e18e0ca3a319 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 5 Sep 2024 14:56:36 +0300 Subject: [PATCH 529/762] Fixed: Refresh tags after updating autotags --- src/NzbDrone.Core/AutoTagging/AutoTaggingService.cs | 11 +++++++++++ src/NzbDrone.Core/AutoTagging/AutoTagsUpdatedEvent.cs | 8 ++++++++ src/Sonarr.Api.V3/Tags/TagController.cs | 11 ++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core/AutoTagging/AutoTagsUpdatedEvent.cs diff --git a/src/NzbDrone.Core/AutoTagging/AutoTaggingService.cs b/src/NzbDrone.Core/AutoTagging/AutoTaggingService.cs index 10c10060b..066a6e322 100644 --- a/src/NzbDrone.Core/AutoTagging/AutoTaggingService.cs +++ b/src/NzbDrone.Core/AutoTagging/AutoTaggingService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; @@ -22,14 +23,18 @@ namespace NzbDrone.Core.AutoTagging { private readonly IAutoTaggingRepository _repository; private readonly RootFolderService _rootFolderService; + private readonly IEventAggregator _eventAggregator; private readonly ICached> _cache; public AutoTaggingService(IAutoTaggingRepository repository, RootFolderService rootFolderService, + IEventAggregator eventAggregator, ICacheManager cacheManager) { _repository = repository; _rootFolderService = rootFolderService; + _eventAggregator = eventAggregator; + _cache = cacheManager.GetCache>(typeof(AutoTag), "autoTags"); } @@ -51,13 +56,17 @@ namespace NzbDrone.Core.AutoTagging public void Update(AutoTag autoTag) { _repository.Update(autoTag); + _cache.Clear(); + _eventAggregator.PublishEvent(new AutoTagsUpdatedEvent()); } public AutoTag Insert(AutoTag autoTag) { var result = _repository.Insert(autoTag); + _cache.Clear(); + _eventAggregator.PublishEvent(new AutoTagsUpdatedEvent()); return result; } @@ -65,7 +74,9 @@ namespace NzbDrone.Core.AutoTagging public void Delete(int id) { _repository.Delete(id); + _cache.Clear(); + _eventAggregator.PublishEvent(new AutoTagsUpdatedEvent()); } public List AllForTag(int tagId) diff --git a/src/NzbDrone.Core/AutoTagging/AutoTagsUpdatedEvent.cs b/src/NzbDrone.Core/AutoTagging/AutoTagsUpdatedEvent.cs new file mode 100644 index 000000000..730e3423d --- /dev/null +++ b/src/NzbDrone.Core/AutoTagging/AutoTagsUpdatedEvent.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.AutoTagging +{ + public class AutoTagsUpdatedEvent : IEvent + { + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagController.cs b/src/Sonarr.Api.V3/Tags/TagController.cs index 46da11ce9..c4880657b 100644 --- a/src/Sonarr.Api.V3/Tags/TagController.cs +++ b/src/Sonarr.Api.V3/Tags/TagController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tags; @@ -11,7 +12,9 @@ using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Tags { [V3ApiController] - public class TagController : RestControllerWithSignalR, IHandle + public class TagController : RestControllerWithSignalR, + IHandle, + IHandle { private readonly ITagService _tagService; @@ -60,5 +63,11 @@ namespace Sonarr.Api.V3.Tags { BroadcastResourceChange(ModelAction.Sync); } + + [NonAction] + public void Handle(AutoTagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } } } From 4d8a4436810828494e99f0854cf6de3269668fe4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 5 Sep 2024 20:46:10 -0700 Subject: [PATCH 530/762] Fixed: Replace illegal characters even when renaming is disabled Closes #7183 --- .../FileNameBuilderFixture.cs | 16 +++++++++++++++- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 3b0cdb0af..710dfe30a 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -274,7 +274,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.RenameEpisodes = false; _episodeFile.RelativePath = null; - _episodeFile.Path = @"C:\Test\Unsorted\Series - S01E01 - Test"; + _episodeFile.Path = @"C:\Test\Unsorted\Series - S01E01 - Test".AsOsAgnostic(); Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(Path.GetFileNameWithoutExtension(_episodeFile.Path)); @@ -291,6 +291,20 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be("30.Rock.S01E01.xvid-LOL"); } + [Test] + public void should_replace_illegal_characters_when_renaming_is_disabled() + { + _namingConfig.RenameEpisodes = false; + _namingConfig.ReplaceIllegalCharacters = true; + _namingConfig.ColonReplacementFormat = ColonReplacementFormat.Smart; + + _episodeFile.SceneName = "30.Rock.S01E01.xvid:LOL"; + _episodeFile.RelativePath = "30 Rock - S01E01 - Test"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("30.Rock.S01E01.xvid-LOL"); + } + [Test] public void should_use_airDate_if_series_isDaily_and_not_a_special() { diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 952b8f593..2ee358bef 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1116,10 +1116,10 @@ namespace NzbDrone.Core.Organizer { if (episodeFile.SceneName.IsNullOrWhiteSpace()) { - return GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); + return CleanFileName(GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback)); } - return episodeFile.SceneName; + return CleanFileName(episodeFile.SceneName); } private string GetOriginalFileName(EpisodeFile episodeFile, bool useCurrentFilenameAsFallback) From 4b5ff3927d3c123f9e3a2bc74328323fab1b0745 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 15 Sep 2024 10:20:42 -0700 Subject: [PATCH 531/762] New: Check for available space before grabbing Closes #7177 --- .../MediaManagement/MediaManagement.js | 32 +++---- .../FreeSpaceSpecificationFixture.cs | 94 +++++++++++++++++++ .../Configuration/ConfigService.cs | 1 + .../Specifications/FreeSpaceSpecification.cs | 66 +++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 +- 5 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index e39ed837d..e903079b5 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -210,25 +210,21 @@ class MediaManagement extends Component { /> - { - isWindows ? - null : - - {translate('SkipFreeSpaceCheck')} + + {translate('SkipFreeSpaceCheck')} - - - } + + + { + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _remoteEpisode = new RemoteEpisode() { Release = new ReleaseInfo(), Series = new Series { Path = @"C:\Test\TV\Series".AsOsAgnostic() } }; + } + + private void WithMinimumFreeSpace(int size) + { + Mocker.GetMock().SetupGet(c => c.MinimumFreeSpaceWhenImporting).Returns(size); + } + + private void WithAvailableSpace(int size) + { + Mocker.GetMock().Setup(s => s.GetAvailableSpace(It.IsAny())).Returns(size.Megabytes()); + } + + private void WithSize(int size) + { + _remoteEpisode.Release.Size = size.Megabytes(); + } + + [Test] + public void should_return_true_when_available_space_is_more_than_size() + { + WithMinimumFreeSpace(0); + WithAvailableSpace(200); + WithSize(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_available_space_minus_size_is_more_than_minimum_free_space() + { + WithMinimumFreeSpace(50); + WithAvailableSpace(200); + WithSize(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_available_space_is_less_than_size() + { + WithMinimumFreeSpace(0); + WithAvailableSpace(200); + WithSize(1000); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_false_when_available_space_minus_size_is_less_than_minimum_free_space() + { + WithMinimumFreeSpace(150); + WithAvailableSpace(200); + WithSize(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_skip_free_space_check_is_true() + { + Mocker.GetMock() + .Setup(s => s.SkipFreeSpaceCheckWhenImporting) + .Returns(true); + + WithMinimumFreeSpace(150); + WithAvailableSpace(200); + WithSize(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 65eb7a5be..f52480e19 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -186,6 +186,7 @@ namespace NzbDrone.Core.Configuration set { SetValue("DownloadClientHistoryLimit", value); } } + // TODO: Rename to 'Skip Free Space Check' public bool SkipFreeSpaceCheckWhenImporting { get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs new file mode 100644 index 000000000..c01c94601 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs @@ -0,0 +1,66 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class FreeSpaceSpecification : IDecisionEngineSpecification + { + private readonly IConfigService _configService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public FreeSpaceSpecification(IConfigService configService, IDiskProvider diskProvider, Logger logger) + { + _configService = configService; + _diskProvider = diskProvider; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Default; + public RejectionType Type => RejectionType.Permanent; + + public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (_configService.SkipFreeSpaceCheckWhenImporting) + { + _logger.Debug("Skipping free space check"); + return Decision.Accept(); + } + + var size = subject.Release.Size; + var freeSpace = _diskProvider.GetAvailableSpace(subject.Series.Path); + + if (!freeSpace.HasValue) + { + _logger.Debug("Unable to get available space for {0}. Skipping", subject.Series.Path); + + return Decision.Accept(); + } + + var minimumSpace = _configService.MinimumFreeSpaceWhenImporting.Megabytes(); + var remainingSpace = freeSpace.Value - size; + + if (remainingSpace <= 0) + { + var message = "Importing after download will exceed available disk space"; + + _logger.Debug(message); + return Decision.Reject(message); + } + + if (remainingSpace < minimumSpace) + { + var message = $"Not enough free space ({minimumSpace.SizeSuffix()}) to import after download: {remainingSpace.SizeSuffix()}. (Settings: Media Management: Minimum Free Space)"; + + _logger.Debug(message); + return Decision.Reject(message); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 8d58e8c9b..9aecc80ef 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1903,7 +1903,7 @@ "SizeLimit": "Size Limit", "SizeOnDisk": "Size on disk", "SkipFreeSpaceCheck": "Skip Free Space Check", - "SkipFreeSpaceCheckWhenImportingHelpText": "Use when {appName} is unable to detect free space of your root folder during file import", + "SkipFreeSpaceCheckHelpText": "Use when {appName} is unable to detect free space of your root folder", "SkipRedownload": "Skip Redownload", "SkipRedownloadHelpText": "Prevents {appName} from trying to download an alternative release for this item", "Small": "Small", From 71a19377d9c12f76a6d252ab2d634c10ab60262e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 15 Sep 2024 10:21:01 -0700 Subject: [PATCH 532/762] New: Add Bluray 576p quality Closes #6203 --- .../214_add_bluray576p_in_profileFixture.cs | 41 ++++++ .../ParserTests/QualityParserFixture.cs | 8 +- ...214_add_blurary576p_quality_in_profiles.cs | 131 ++++++++++++++++++ src/NzbDrone.Core/Parser/QualityParser.cs | 26 ++-- .../Qualities/QualityProfileService.cs | 6 +- src/NzbDrone.Core/Qualities/Quality.cs | 41 +++--- 6 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs new file mode 100644 index 000000000..8fd403b91 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs @@ -0,0 +1,41 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_bluray576p_in_profileFixture : MigrationTest + { + private string GenerateQualityJson(int quality, bool allowed) + { + return $"{{ \"quality\": {quality}, \"allowed\": {allowed.ToString().ToLowerInvariant()} }}"; + } + + [Test] + public void should_add_bluray576p_to_old_profile() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityJson((int)Quality.DVD, true)}, {GenerateQualityJson((int)Quality.Bluray480p, true)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(4); + items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p, (int)Quality.Bluray720p); + items.Select(v => v.Allowed).Should().Equal(true, true, true, false); + items.Select(v => v.Name).Should().Equal(null, null, null, null); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 98475053a..b9f99a146 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.ParserTests new object[] { Quality.DVD }, new object[] { Quality.WEBDL480p }, new object[] { Quality.Bluray480p }, + new object[] { Quality.Bluray576p }, new object[] { Quality.HDTV720p }, new object[] { Quality.HDTV1080p }, new object[] { Quality.HDTV2160p }, @@ -105,7 +106,6 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("SERIES.S03E01-06.DUAL.BDRip.AC3.-HELLYWOOD", false)] [TestCase("SERIES.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD.avi", false)] [TestCase("SERIES.S03E01-06.DUAL.XviD.Bluray.AC3.-HELLYWOOD.avi", false)] - [TestCase("The.Series.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] [TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)] @@ -124,6 +124,12 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.WEBRip480p, proper); } + [TestCase("The.Series.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] + public void should_parse_bluray576p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.Bluray576p, proper); + } + [TestCase("Series - S01E01 - Title [HDTV]", false)] [TestCase("Series - S01E01 - Title [HDTV-720p]", false)] [TestCase("The Series S04E87 REPACK 720p HDTV x264 aAF", true)] diff --git a/src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs new file mode 100644 index 000000000..8024313cd --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(214)] + public class add_blurary576p_quality_in_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertProfile); + } + + private void ConvertProfile(IDbConnection conn, IDbTransaction tran) + { + var updater = new ProfileUpdater214(conn, tran); + + updater.InsertQualityAfter(13, 22); // Group Bluray576p with Bluray480p + updater.Commit(); + } + } + + public class Profile214 + { + public int Id { get; set; } + public string Name { get; set; } + public int Cutoff { get; set; } + public List Items { get; set; } + } + + public class ProfileItem214 + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Id { get; set; } + + public string Name { get; set; } + public int? Quality { get; set; } + public List Items { get; set; } + public bool Allowed { get; set; } + + public ProfileItem214() + { + Items = new List(); + } + } + + public class ProfileUpdater214 + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + + private List _profiles; + private HashSet _changedProfiles = new HashSet(); + + public ProfileUpdater214(IDbConnection conn, IDbTransaction tran) + { + _connection = conn; + _transaction = tran; + + _profiles = GetProfiles(); + } + + public void Commit() + { + var profilesToUpdate = _changedProfiles.Select(p => new + { + Id = p.Id, + Name = p.Name, + Cutoff = p.Cutoff, + Items = p.Items.ToJson() + }); + + var updateSql = $"UPDATE \"QualityProfiles\" SET \"Name\" = @Name, \"Cutoff\" = @Cutoff, \"Items\" = @Items WHERE \"Id\" = @Id"; + _connection.Execute(updateSql, profilesToUpdate, transaction: _transaction); + + _changedProfiles.Clear(); + } + + public void InsertQualityAfter(int find, int quality) + { + foreach (var profile in _profiles) + { + var findIndex = profile.Items.FindIndex(v => v.Quality == find); + + if (findIndex > -1) + { + profile.Items.Insert(findIndex + 1, new ProfileItem214 + { + Quality = quality, + Allowed = profile.Items[findIndex].Allowed + }); + } + + _changedProfiles.Add(profile); + } + } + + private List GetProfiles() + { + var profiles = new List(); + + using (var getProfilesCmd = _connection.CreateCommand()) + { + getProfilesCmd.Transaction = _transaction; + getProfilesCmd.CommandText = "SELECT \"Id\", \"Name\", \"Cutoff\", \"Items\" FROM \"QualityProfiles\""; + + using (var profileReader = getProfilesCmd.ExecuteReader()) + { + while (profileReader.Read()) + { + profiles.Add(new Profile214 + { + Id = profileReader.GetInt32(0), + Name = profileReader.GetString(1), + Cutoff = profileReader.GetInt32(2), + Items = Json.Deserialize>(profileReader.GetString(3)) + }); + } + } + } + + return profiles; + } + } +} diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 575727297..d922d86ce 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -147,8 +147,14 @@ namespace NzbDrone.Core.Parser return result; } - if (resolution == Resolution.R360P || resolution == Resolution.R480P || - resolution == Resolution.R540p || resolution == Resolution.R576p) + if (resolution == Resolution.R576p) + { + result.Quality = Quality.Bluray576p; + return result; + } + + if (resolution == Resolution.R360p || resolution == Resolution.R480p || + resolution == Resolution.R540p) { result.Quality = Quality.Bluray480p; return result; @@ -315,7 +321,7 @@ namespace NzbDrone.Core.Parser { result.SourceDetectionSource = QualityDetectionSource.Unknown; - if (resolution == Resolution.R480P) + if (resolution == Resolution.R480p) { result.Quality = Quality.Bluray480p; return result; @@ -345,7 +351,7 @@ namespace NzbDrone.Core.Parser { result.SourceDetectionSource = QualityDetectionSource.Name; - if (resolution == Resolution.R360P || resolution == Resolution.R480P || + if (resolution == Resolution.R360p || resolution == Resolution.R480p || resolution == Resolution.R540p || resolution == Resolution.R576p || normalizedName.ContainsIgnoreCase("480p")) { @@ -387,7 +393,7 @@ namespace NzbDrone.Core.Parser { result.SourceDetectionSource = QualityDetectionSource.Name; - if (resolution == Resolution.R360P || resolution == Resolution.R480P || + if (resolution == Resolution.R360p || resolution == Resolution.R480p || resolution == Resolution.R540p || resolution == Resolution.R576p || normalizedName.ContainsIgnoreCase("480p")) { @@ -477,7 +483,7 @@ namespace NzbDrone.Core.Parser return result; } - if (resolution == Resolution.R360P || resolution == Resolution.R480P || + if (resolution == Resolution.R360p || resolution == Resolution.R480p || resolution == Resolution.R540p || resolution == Resolution.R576p) { result.ResolutionDetectionSource = QualityDetectionSource.Name; @@ -603,12 +609,12 @@ namespace NzbDrone.Core.Parser if (match.Groups["R360p"].Success) { - return Resolution.R360P; + return Resolution.R360p; } if (match.Groups["R480p"].Success) { - return Resolution.R480P; + return Resolution.R480p; } if (match.Groups["R540p"].Success) @@ -707,8 +713,8 @@ namespace NzbDrone.Core.Parser public enum Resolution { - R360P = 360, - R480P = 480, + R360p = 360, + R480p = 480, R540p = 540, R576p = 576, R720p = 720, diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index c53362b47..4fb494c56 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -98,6 +98,8 @@ namespace NzbDrone.Core.Profiles.Qualities Quality.WEBRip480p, Quality.WEBDL480p, Quality.DVD, + Quality.Bluray480p, + Quality.Bluray576p, Quality.HDTV720p, Quality.HDTV1080p, Quality.WEBRip720p, @@ -112,7 +114,9 @@ namespace NzbDrone.Core.Profiles.Qualities Quality.SDTV, Quality.WEBRip480p, Quality.WEBDL480p, - Quality.DVD); + Quality.DVD, + Quality.Bluray480p, + Quality.Bluray576p); AddDefaultProfile("HD-720p", Quality.HDTV720p, diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index c04f8d1d7..c8537840b 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -97,6 +97,11 @@ namespace NzbDrone.Core.Qualities get { return new Quality(13, "Bluray-480p", QualitySource.Bluray, 480); } } + public static Quality Bluray576p + { + get { return new Quality(22, "Bluray-576p", QualitySource.Bluray, 576); } + } + public static Quality WEBRip720p { get { return new Quality(14, "WEBRip-720p", QualitySource.WebRip, 720); } @@ -128,6 +133,7 @@ namespace NzbDrone.Core.Qualities WEBRip480p, WEBDL480p, Bluray480p, + Bluray576p, HDTV720p, WEBRip720p, WEBDL720p, @@ -153,23 +159,24 @@ namespace NzbDrone.Core.Qualities new QualityDefinition(Quality.SDTV) { Weight = 2, MinSize = 2, MaxSize = 100, PreferredSize = 95 }, new QualityDefinition(Quality.WEBRip480p) { Weight = 3, MinSize = 2, MaxSize = 100, PreferredSize = 95, GroupName = "WEB 480p" }, new QualityDefinition(Quality.WEBDL480p) { Weight = 3, MinSize = 2, MaxSize = 100, PreferredSize = 95, GroupName = "WEB 480p" }, - new QualityDefinition(Quality.DVD) { Weight = 4, MinSize = 2, MaxSize = 100, PreferredSize = 95, GroupName = "DVD" }, - new QualityDefinition(Quality.Bluray480p) { Weight = 5, MinSize = 2, MaxSize = 100, PreferredSize = 95, GroupName = "DVD" }, - new QualityDefinition(Quality.HDTV720p) { Weight = 6, MinSize = 3, MaxSize = 125, PreferredSize = 95 }, - new QualityDefinition(Quality.HDTV1080p) { Weight = 7, MinSize = 4, MaxSize = 125, PreferredSize = 95 }, - new QualityDefinition(Quality.RAWHD) { Weight = 8, MinSize = 4, MaxSize = null, PreferredSize = 95 }, - new QualityDefinition(Quality.WEBRip720p) { Weight = 9, MinSize = 3, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 720p" }, - new QualityDefinition(Quality.WEBDL720p) { Weight = 9, MinSize = 3, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 720p" }, - new QualityDefinition(Quality.Bluray720p) { Weight = 10, MinSize = 4, MaxSize = 130, PreferredSize = 95 }, - new QualityDefinition(Quality.WEBRip1080p) { Weight = 11, MinSize = 4, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 1080p" }, - new QualityDefinition(Quality.WEBDL1080p) { Weight = 11, MinSize = 4, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 1080p" }, - new QualityDefinition(Quality.Bluray1080p) { Weight = 12, MinSize = 4, MaxSize = 155, PreferredSize = 95 }, - new QualityDefinition(Quality.Bluray1080pRemux) { Weight = 13, MinSize = 35, MaxSize = null, PreferredSize = 95 }, - new QualityDefinition(Quality.HDTV2160p) { Weight = 14, MinSize = 35, MaxSize = 199.9, PreferredSize = 95 }, - new QualityDefinition(Quality.WEBRip2160p) { Weight = 15, MinSize = 35, MaxSize = null, PreferredSize = 95, GroupName = "WEB 2160p" }, - new QualityDefinition(Quality.WEBDL2160p) { Weight = 15, MinSize = 35, MaxSize = null, PreferredSize = 95, GroupName = "WEB 2160p" }, - new QualityDefinition(Quality.Bluray2160p) { Weight = 16, MinSize = 35, MaxSize = null, PreferredSize = 95 }, - new QualityDefinition(Quality.Bluray2160pRemux) { Weight = 17, MinSize = 35, MaxSize = null, PreferredSize = 95 } + new QualityDefinition(Quality.DVD) { Weight = 4, MinSize = 2, MaxSize = 100, PreferredSize = 95 }, + new QualityDefinition(Quality.Bluray480p) { Weight = 5, MinSize = 2, MaxSize = 100, PreferredSize = 95 }, + new QualityDefinition(Quality.Bluray576p) { Weight = 6, MinSize = 2, MaxSize = 100, PreferredSize = 95 }, + new QualityDefinition(Quality.HDTV720p) { Weight = 7, MinSize = 3, MaxSize = 125, PreferredSize = 95 }, + new QualityDefinition(Quality.HDTV1080p) { Weight = 8, MinSize = 4, MaxSize = 125, PreferredSize = 95 }, + new QualityDefinition(Quality.RAWHD) { Weight = 9, MinSize = 4, MaxSize = null, PreferredSize = 95 }, + new QualityDefinition(Quality.WEBRip720p) { Weight = 10, MinSize = 3, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 720p" }, + new QualityDefinition(Quality.WEBDL720p) { Weight = 10, MinSize = 3, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 720p" }, + new QualityDefinition(Quality.Bluray720p) { Weight = 11, MinSize = 4, MaxSize = 130, PreferredSize = 95 }, + new QualityDefinition(Quality.WEBRip1080p) { Weight = 12, MinSize = 4, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 1080p" }, + new QualityDefinition(Quality.WEBDL1080p) { Weight = 12, MinSize = 4, MaxSize = 130, PreferredSize = 95, GroupName = "WEB 1080p" }, + new QualityDefinition(Quality.Bluray1080p) { Weight = 13, MinSize = 4, MaxSize = 155, PreferredSize = 95 }, + new QualityDefinition(Quality.Bluray1080pRemux) { Weight = 14, MinSize = 35, MaxSize = null, PreferredSize = 95 }, + new QualityDefinition(Quality.HDTV2160p) { Weight = 15, MinSize = 35, MaxSize = 199.9, PreferredSize = 95 }, + new QualityDefinition(Quality.WEBRip2160p) { Weight = 16, MinSize = 35, MaxSize = null, PreferredSize = 95, GroupName = "WEB 2160p" }, + new QualityDefinition(Quality.WEBDL2160p) { Weight = 16, MinSize = 35, MaxSize = null, PreferredSize = 95, GroupName = "WEB 2160p" }, + new QualityDefinition(Quality.Bluray2160p) { Weight = 17, MinSize = 35, MaxSize = null, PreferredSize = 95 }, + new QualityDefinition(Quality.Bluray2160pRemux) { Weight = 18, MinSize = 35, MaxSize = null, PreferredSize = 95 } }; } From 750a9353f82da4e016bee25e0c625cd6d8613b57 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Sep 2024 10:17:09 -0700 Subject: [PATCH 533/762] New: Add additional archive exentions Closes #7191 --- src/NzbDrone.Core/MediaFiles/FileExtensions.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index a6b0c1991..77d787645 100644 --- a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs @@ -7,12 +7,19 @@ namespace NzbDrone.Core.MediaFiles { private static List _archiveExtensions = new List { - ".rar", - ".r00", - ".zip", - ".tar", + ".7z", + ".bz2", ".gz", - ".tar.gz" + ".r00", + ".rar", + ".tar.bz2", + ".tar.gz", + ".tar", + ".tb2", + ".tbz2", + ".tgz", + ".zip", + ".zipx" }; private static List _executableExtensions = new List From 3c857135c59029635b0972f959f9a8255bcff21f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Sep 2024 12:08:29 -0700 Subject: [PATCH 534/762] Gotify notification updates New: Option to include links for Gotify notifications New: Include images and links for Android Closes #7190 --- src/NzbDrone.Core/Localization/Core/en.json | 4 + .../Notifications/Gotify/Gotify.cs | 97 +++++++++++++++---- .../Notifications/Gotify/GotifyMessage.cs | 31 ++++++ .../Notifications/Gotify/GotifySettings.cs | 36 +++++++ .../Notifications/MetadataLinkType.cs | 19 ++++ .../Telegram/TelegramSettings.cs | 15 --- 6 files changed, 169 insertions(+), 33 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/MetadataLinkType.cs diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 9aecc80ef..364ffe5c2 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1347,6 +1347,10 @@ "NotificationsGotifySettingsAppToken": "App Token", "NotificationsGotifySettingsAppTokenHelpText": "The Application Token generated by Gotify", "NotificationsGotifySettingsPriorityHelpText": "Priority of the notification", + "NotificationsGotifySettingsMetadataLinks": "Metadata Links", + "NotificationsGotifySettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications", + "NotificationsGotifySettingsPreferredMetadataLink": "Preferred Metadata Link", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Metadata link for clients that only support a single link", "NotificationsGotifySettingsServer": "Gotify Server", "NotificationsGotifySettingsServerHelpText": "Gotify server URL, including http(s):// and port if needed", "NotificationsJoinSettingsApiKeyHelpText": "The API Key from your Join account settings (click Join API button).", diff --git a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs index 47060065c..83b3568bd 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; @@ -12,6 +13,8 @@ namespace NzbDrone.Core.Notifications.Gotify { public class Gotify : NotificationBase { + private const string SonarrImageUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/128.png"; + private readonly IGotifyProxy _proxy; private readonly ILocalizationService _localizationService; private readonly Logger _logger; @@ -88,20 +91,30 @@ namespace NzbDrone.Core.Notifications.Gotify var sb = new StringBuilder(); sb.AppendLine("This is a test message from Sonarr"); + var payload = new GotifyMessage + { + Title = title, + Priority = Settings.Priority + }; + if (Settings.IncludeSeriesPoster) { isMarkdown = true; - sb.AppendLine("\r![](https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/128.png)"); + sb.AppendLine($"\r![]({SonarrImageUrl})"); + payload.SetImage(SonarrImageUrl); } - var payload = new GotifyMessage + if (Settings.MetadataLinks.Any()) { - Title = title, - Message = sb.ToString(), - Priority = Settings.Priority - }; + isMarkdown = true; + sb.AppendLine(""); + sb.AppendLine("[Sonarr.tv](https://sonarr.tv)"); + payload.SetClickUrl("https://sonarr.tv"); + } + + payload.Message = sb.ToString(); payload.SetContentType(isMarkdown); _proxy.SendNotification(payload, Settings); @@ -122,24 +135,72 @@ namespace NzbDrone.Core.Notifications.Gotify sb.AppendLine(message); - if (Settings.IncludeSeriesPoster && series != null) - { - var poster = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl; - - if (poster != null) - { - isMarkdown = true; - sb.AppendLine($"\r![]({poster})"); - } - } - var payload = new GotifyMessage { Title = title, - Message = sb.ToString(), Priority = Settings.Priority }; + if (series != null) + { + if (Settings.IncludeSeriesPoster) + { + var poster = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl; + + if (poster != null) + { + isMarkdown = true; + sb.AppendLine($"\r![]({poster})"); + payload.SetImage(poster); + } + } + + if (Settings.MetadataLinks.Any()) + { + isMarkdown = true; + sb.AppendLine(""); + + foreach (var link in Settings.MetadataLinks) + { + var linkType = (MetadataLinkType)link; + var linkText = ""; + var linkUrl = ""; + + if (linkType == MetadataLinkType.Imdb && series.ImdbId.IsNotNullOrWhiteSpace()) + { + linkText = "IMDb"; + linkUrl = $"https://www.imdb.com/title/{series.ImdbId}"; + } + + if (linkType == MetadataLinkType.Tvdb && series.TvdbId > 0) + { + linkText = "TVDb"; + linkUrl = $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}"; + } + + if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0) + { + linkText = "TVMaze"; + linkUrl = $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show"; + } + + if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0) + { + linkText = "Trakt"; + linkUrl = $"http://www.tvmaze.com/shows/{series.TvMazeId}/_"; + } + + sb.AppendLine($"[{linkText}]({linkUrl})"); + + if (link == Settings.PreferredMetadataLink) + { + payload.SetClickUrl(linkUrl); + } + } + } + } + + payload.Message = sb.ToString(); payload.SetContentType(isMarkdown); _proxy.SendNotification(payload, Settings); diff --git a/src/NzbDrone.Core/Notifications/Gotify/GotifyMessage.cs b/src/NzbDrone.Core/Notifications/Gotify/GotifyMessage.cs index 170ce1367..576294946 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/GotifyMessage.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/GotifyMessage.cs @@ -20,12 +20,27 @@ namespace NzbDrone.Core.Notifications.Gotify Extras.ClientDisplay = new GotifyClientDisplay(contentType); } + + public void SetImage(string imageUrl) + { + Extras.ClientNotification ??= new GotifyClientNotification(); + Extras.ClientNotification.BigImageUrl = imageUrl; + } + + public void SetClickUrl(string url) + { + Extras.ClientNotification ??= new GotifyClientNotification(); + Extras.ClientNotification.Click = new GotifyClientNotificationClick(url); + } } public class GotifyExtras { [JsonProperty("client::display")] public GotifyClientDisplay ClientDisplay { get; set; } + + [JsonProperty("client::notification")] + public GotifyClientNotification ClientNotification { get; set; } } public class GotifyClientDisplay @@ -37,4 +52,20 @@ namespace NzbDrone.Core.Notifications.Gotify ContentType = contentType; } } + + public class GotifyClientNotification + { + public string BigImageUrl { get; set; } + public GotifyClientNotificationClick Click { get; set; } + } + + public class GotifyClientNotificationClick + { + public string Url { get; set; } + + public GotifyClientNotificationClick(string url) + { + Url = url; + } + } } diff --git a/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs b/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs index b152ef5fe..94efd379f 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/GotifySettings.cs @@ -1,4 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -10,6 +14,30 @@ namespace NzbDrone.Core.Notifications.Gotify { RuleFor(c => c.Server).IsValidUrl(); RuleFor(c => c.AppToken).NotEmpty(); + + RuleFor(c => c.MetadataLinks).Custom((links, context) => + { + foreach (var link in links) + { + if (!Enum.IsDefined(typeof(MetadataLinkType), link)) + { + context.AddFailure("MetadataLinks", $"MetadataLink is not valid: {link}"); + } + } + }); + + RuleFor(c => c).Custom((c, context) => + { + if (c.MetadataLinks.Empty()) + { + return; + } + + if (!c.MetadataLinks.Contains(c.PreferredMetadataLink)) + { + context.AddFailure("PreferredMetadataLink", "Must be a selected link"); + } + }); } } @@ -20,6 +48,8 @@ namespace NzbDrone.Core.Notifications.Gotify public GotifySettings() { Priority = 5; + MetadataLinks = Enumerable.Empty(); + PreferredMetadataLink = (int)MetadataLinkType.Tvdb; } [FieldDefinition(0, Label = "NotificationsGotifySettingsServer", HelpText = "NotificationsGotifySettingsServerHelpText")] @@ -34,6 +64,12 @@ namespace NzbDrone.Core.Notifications.Gotify [FieldDefinition(3, Label = "NotificationsGotifySettingIncludeSeriesPoster", Type = FieldType.Checkbox, HelpText = "NotificationsGotifySettingIncludeSeriesPosterHelpText")] public bool IncludeSeriesPoster { get; set; } + [FieldDefinition(4, Label = "NotificationsGotifySettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsGotifySettingsMetadataLinksHelpText")] + public IEnumerable MetadataLinks { get; set; } + + [FieldDefinition(5, Label = "NotificationsGotifySettingsPreferredMetadataLink", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsGotifySettingsPreferredMetadataLinkHelpText")] + public int PreferredMetadataLink { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Notifications/MetadataLinkType.cs b/src/NzbDrone.Core/Notifications/MetadataLinkType.cs new file mode 100644 index 000000000..0a6644bd7 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/MetadataLinkType.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Notifications +{ + public enum MetadataLinkType + { + [FieldOption(Label = "IMDb")] + Imdb = 0, + + [FieldOption(Label = "TVDb")] + Tvdb = 1, + + [FieldOption(Label = "TVMaze")] + Tvmaze = 2, + + [FieldOption(Label = "Trakt")] + Trakt = 3 + } +} diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index 6ec3d4d15..ac39a1b45 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -59,19 +59,4 @@ namespace NzbDrone.Core.Notifications.Telegram return new NzbDroneValidationResult(Validator.Validate(this)); } } - - public enum MetadataLinkType - { - [FieldOption(Label = "IMDb")] - Imdb, - - [FieldOption(Label = "TVDb")] - Tvdb, - - [FieldOption(Label = "TVMaze")] - Tvmaze, - - [FieldOption(Label = "Trakt")] - Trakt, - } } From 6cccacd4d7ae2ff54a30bc0ca635b0fdc0808f24 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Sun, 15 Sep 2024 19:22:28 +0200 Subject: [PATCH 535/762] Add workflow to close issue when labelled as support --- .github/workflows/support-requests.yml | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/support-requests.yml diff --git a/.github/workflows/support-requests.yml b/.github/workflows/support-requests.yml new file mode 100644 index 000000000..adf5a8c4a --- /dev/null +++ b/.github/workflows/support-requests.yml @@ -0,0 +1,29 @@ +name: 'Support Requests' + +on: + issues: + types: [labeled, unlabeled, reopened] + +permissions: + issues: write + +jobs: + action: + runs-on: ubuntu-latest + if: github.repository == 'Sonarr/Sonarr' + steps: + - uses: dessant/support-requests@v4 + with: + github-token: ${{ github.token }} + support-label: 'support' + issue-comment: > + :wave: @{issue-author}, we use the issue tracker exclusively + for bug reports and feature requests. However, this issue appears + to be a support request. Please use one of the support channels: + [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), + [discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr) + for support/questions. + close-issue: true + issue-close-reason: 'not planned' + lock-issue: false + issue-lock-reason: 'off-topic' From 31bf9e313e6a376f6ef3c46d53e2450088041033 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Sun, 15 Sep 2024 19:23:12 +0200 Subject: [PATCH 536/762] New: Add rating as option in sort dropdown on series overviews and posters views --- frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx index d3698e501..256ac0231 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx @@ -153,6 +153,15 @@ function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) { > {translate('Tags')} + + + {translate('Rating')} + ); From 97ebaf279650082c6baee9563ef179921c5ed25a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 14 Sep 2024 12:47:42 -0700 Subject: [PATCH 537/762] New: Use instance name in forms authentication cookie name Closes #7199 --- .../AuthenticationBuilderExtensions.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 21d5b7009..361a091f6 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -1,7 +1,10 @@ using System; +using System.Web; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; namespace Sonarr.Http.Authentication { @@ -29,19 +32,25 @@ namespace Sonarr.Http.Authentication public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) { - return services.AddAuthentication() - .AddNone(AuthenticationType.None.ToString()) - .AddExternal(AuthenticationType.External.ToString()) - .AddBasic(AuthenticationType.Basic.ToString()) - .AddCookie(AuthenticationType.Forms.ToString(), options => + services.AddOptions(AuthenticationType.Forms.ToString()) + .Configure((options, configFileProvider) => { - options.Cookie.Name = "SonarrAuth"; + // Url Encode the cookie name to account for spaces or other invalid characters in the configured instance name + var instanceName = HttpUtility.UrlEncode(configFileProvider.InstanceName); + + options.Cookie.Name = $"{instanceName}Auth"; options.AccessDeniedPath = "/login?loginFailed=true"; options.LoginPath = "/login"; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.SlidingExpiration = true; options.ReturnUrlParameter = "returnUrl"; - }) + }); + + return services.AddAuthentication() + .AddNone(AuthenticationType.None.ToString()) + .AddExternal(AuthenticationType.External.ToString()) + .AddBasic(AuthenticationType.Basic.ToString()) + .AddCookie(AuthenticationType.Forms.ToString()) .AddApiKey("API", options => { options.HeaderName = "X-Api-Key"; From d84c4500949a530fac92d73f7f2f8e8462b37244 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 14 Sep 2024 13:40:02 -0700 Subject: [PATCH 538/762] New: Add exception to SSL Certificate validation message Closes #7198 --- .../Config/CertificateValidator.cs | 52 +++++++++++++++++++ .../Config/HostConfigController.cs | 18 +------ 2 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 src/Sonarr.Api.V3/Config/CertificateValidator.cs diff --git a/src/Sonarr.Api.V3/Config/CertificateValidator.cs b/src/Sonarr.Api.V3/Config/CertificateValidator.cs new file mode 100644 index 000000000..ded119c18 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/CertificateValidator.cs @@ -0,0 +1,52 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using FluentValidation; +using FluentValidation.Validators; +using NLog; +using NzbDrone.Common.Instrumentation; + +namespace Sonarr.Api.V3.Config +{ + public static class CertificateValidation + { + public static IRuleBuilderOptions IsValidCertificate(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new CertificateValidator()); + } + } + + public class CertificateValidator : PropertyValidator + { + protected override string GetDefaultMessageTemplate() => "Invalid SSL certificate file or password. {message}"; + + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(CertificateValidator)); + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return false; + } + + if (context.InstanceToValidate is not HostConfigResource resource) + { + return true; + } + + try + { + new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + + return true; + } + catch (CryptographicException ex) + { + Logger.Debug(ex, "Invalid SSL certificate file or password. {0}", ex.Message); + + context.MessageFormatter.AppendArgument("message", ex.Message); + + return false; + } + } + } +} diff --git a/src/Sonarr.Api.V3/Config/HostConfigController.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs index 3aea78c45..a108e70c3 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigController.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -1,7 +1,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Security.Cryptography.X509Certificates; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; @@ -58,7 +57,7 @@ namespace Sonarr.Api.V3.Config .NotEmpty() .IsValidPath() .SetValidator(fileExistsValidator) - .Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password") + .IsValidCertificate() .When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.LogSizeLimit).InclusiveBetween(1, 10); @@ -71,21 +70,6 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90); } - private bool IsValidSslCertificate(HostConfigResource resource) - { - X509Certificate2 cert; - try - { - cert = new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); - } - catch - { - return false; - } - - return cert != null; - } - private bool IsMatchingPassword(HostConfigResource resource) { var user = _userService.FindUser(); From 9603f0b08632f5ddbb378e07157049db27b8660f Mon Sep 17 00:00:00 2001 From: Sonarr Date: Sun, 15 Sep 2024 17:22:49 +0000 Subject: [PATCH 539/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 399e3f6ed..5db55798e 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -5482,6 +5482,35 @@ } } }, + "/api/v3/qualitydefinition/limits": { + "get": { + "tags": [ + "QualityDefinition" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionLimitsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionLimitsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionLimitsResource" + } + } + } + } + } + } + }, "/api/v3/qualityprofile": { "post": { "tags": [ @@ -10565,6 +10594,20 @@ }, "additionalProperties": false }, + "QualityDefinitionLimitsResource": { + "type": "object", + "properties": { + "min": { + "type": "integer", + "format": "int32" + }, + "max": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "QualityDefinitionResource": { "type": "object", "properties": { From e6bd58453a532c137879c2b1a6a267dc4bf03828 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 15 Sep 2024 17:20:49 +0000 Subject: [PATCH 540/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Weblate Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 1 - src/NzbDrone.Core/Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 1 - src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 7 files changed, 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index b96baa9f3..4ab538d01 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1880,7 +1880,6 @@ "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.", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 2be390cfa..f7a39508a 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -691,7 +691,6 @@ "SslPort": "SSL-portti", "StopSelecting": "Lopeta valitseminen", "Standard": "Vakio", - "SkipFreeSpaceCheckWhenImportingHelpText": "Käytä kun juurikansion tallennusmedian vapaata tilaa ei tunnisteta tiedostotuonnin yhteydessä.", "Small": "Pieni", "DownloadWarning": "Latausvaroitus: {warningMessage}", "DownloadIgnored": "Lataus ohitettiin", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c91afde38..8da77fcfd 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -742,7 +742,6 @@ "ShowUnknownSeriesItems": "Afficher les éléments de série inconnus", "ShowUnknownSeriesItemsHelpText": "Afficher les éléments sans série dans la file d'attente. Cela peut inclure des séries, des films ou tout autre élément supprimé dans la catégorie de {appName}", "ShownClickToHide": "Affiché, cliquez pour masquer", - "SkipFreeSpaceCheckWhenImportingHelpText": "À utiliser lorsque {appName} ne parvient pas à détecter l'espace libre de votre dossier racine lors de l'importation de fichiers", "SkipRedownloadHelpText": "Empêche {appName} d'essayer de télécharger une version alternative pour cet élément", "Small": "Petit", "Socks5": "Socks5 (Support TOR)", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 23a5f0207..7bfb4163d 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -997,7 +997,6 @@ "UiSettingsLoadError": "Nem sikerült betölteni a felhasználói felület beállításait", "SelectDropdown": "Válassz...", "SelectEpisodes": "Epizód(ok) kiválasztása", - "SkipFreeSpaceCheckWhenImportingHelpText": "Akkor használja, ha a(z) {appName} nem tud szabad helyet észlelni a gyökérmappában a fájlimportálás során", "SkipRedownloadHelpText": "Megakadályozza, hogy {appName} megpróbáljon letölteni egy alternatív kiadást ehhez az elemhez", "Specials": "Különlegességek", "SslCertPasswordHelpText": "Jelszó a pfx fájlhoz", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 5eded8dff..3b717e244 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -834,7 +834,6 @@ "SingleEpisode": "Episódio Único", "SizeLimit": "Limite de Tamanho", "SkipFreeSpaceCheck": "Ignorar verificação de espaço livre", - "SkipFreeSpaceCheckWhenImportingHelpText": "Use quando {appName} não consegue detectar espaço livre em sua pasta raiz durante a importação do arquivo", "SmartReplace": "Substituição inteligente", "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome", "Socks4": "Socks4", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 88209a861..dd92b792f 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1593,7 +1593,6 @@ "SkipFreeSpaceCheck": "Пропустить проверку свободного места", "ShowSizeOnDisk": "Показать размер на диске", "ShowSearchHelpText": "Показать копку поиска при наведении", - "SkipFreeSpaceCheckWhenImportingHelpText": "Используйте, когда {appName} не может обнаружить свободное место в вашей корневой папке во время импорта файлов", "SpecialsFolderFormat": "Формат папки спец. эпизодов", "SmartReplace": "Умная замена", "SkipRedownloadHelpText": "Предотвращает попытку {appName} загрузить альтернативную версию для этого элемента", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 72eb8977c..2df0befc0 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1147,7 +1147,6 @@ "ShowSeriesTitleHelpText": "在海报下显示剧集标题", "ShowUnknownSeriesItems": "实现未知剧集项目", "ShowUnknownSeriesItemsHelpText": "显示队列中没有剧集的项目,这可能包括已删除的剧集、电影或 {appName} 分类中的任何其他内容", - "SkipFreeSpaceCheckWhenImportingHelpText": "在文件导入期间,当{appName}无法检测根文件夹的空闲空间时使用", "Small": "小", "SingleEpisodeInvalidFormat": "单集:非法格式", "SkipFreeSpaceCheck": "跳过剩余空间检查", From 99fc52039f44264c83d939e5f096d8e16d2f3355 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:09:55 +0200 Subject: [PATCH 541/762] Convert ClipboardButton to TypeScript --- .../src/Components/Form/FormInputButton.js | 4 +- .../src/Components/Link/ClipboardButton.js | 139 ------------------ .../src/Components/Link/ClipboardButton.tsx | 69 +++++++++ frontend/src/Utilities/getUniqueElementId.ts | 6 +- package.json | 1 - yarn.lock | 31 ---- 6 files changed, 76 insertions(+), 174 deletions(-) delete mode 100644 frontend/src/Components/Link/ClipboardButton.js create mode 100644 frontend/src/Components/Link/ClipboardButton.tsx diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js index a7145363a..2bacc3779 100644 --- a/frontend/src/Components/Form/FormInputButton.js +++ b/frontend/src/Components/Form/FormInputButton.js @@ -42,7 +42,9 @@ function FormInputButton(props) { FormInputButton.propTypes = { className: PropTypes.string.isRequired, isLastButton: PropTypes.bool.isRequired, - canSpin: PropTypes.bool.isRequired + canSpin: PropTypes.bool.isRequired, + children: PropTypes.element, + id: PropTypes.string }; FormInputButton.defaultProps = { diff --git a/frontend/src/Components/Link/ClipboardButton.js b/frontend/src/Components/Link/ClipboardButton.js deleted file mode 100644 index 55843f05f..000000000 --- a/frontend/src/Components/Link/ClipboardButton.js +++ /dev/null @@ -1,139 +0,0 @@ -import Clipboard from 'clipboard'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormInputButton from 'Components/Form/FormInputButton'; -import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; -import getUniqueElememtId from 'Utilities/getUniqueElementId'; -import styles from './ClipboardButton.css'; - -class ClipboardButton extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._id = getUniqueElememtId(); - this._successTimeout = null; - this._testResultTimeout = null; - - this.state = { - showSuccess: false, - showError: false - }; - } - - componentDidMount() { - this._clipboard = new Clipboard(`#${this._id}`, { - text: () => this.props.value, - container: document.getElementById(this._id) - }); - - this._clipboard.on('success', this.onSuccess); - } - - componentDidUpdate() { - const { - showSuccess, - showError - } = this.state; - - if (showSuccess || showError) { - this._testResultTimeout = setTimeout(this.resetState, 3000); - } - } - - componentWillUnmount() { - if (this._clipboard) { - this._clipboard.destroy(); - } - - if (this._testResultTimeout) { - clearTimeout(this._testResultTimeout); - } - } - - // - // Control - - resetState = () => { - this.setState({ - showSuccess: false, - showError: false - }); - }; - - // - // Listeners - - onSuccess = () => { - this.setState({ - showSuccess: true - }); - }; - - onError = () => { - this.setState({ - showError: true - }); - }; - - // - // Render - - render() { - const { - value, - className, - ...otherProps - } = this.props; - - const { - showSuccess, - showError - } = this.state; - - const showStateIcon = showSuccess || showError; - const iconName = showError ? icons.DANGER : icons.CHECK; - const iconKind = showError ? kinds.DANGER : kinds.SUCCESS; - - return ( - - - { - showSuccess && - - - - } - - { - - - - } - - - ); - } -} - -ClipboardButton.propTypes = { - className: PropTypes.string.isRequired, - value: PropTypes.string.isRequired -}; - -ClipboardButton.defaultProps = { - className: styles.button -}; - -export default ClipboardButton; diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx new file mode 100644 index 000000000..09095ae74 --- /dev/null +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import FormInputButton from 'Components/Form/FormInputButton'; +import Icon from 'Components/Icon'; +import { icons, kinds } from 'Helpers/Props'; +import { ButtonProps } from './Button'; +import styles from './ClipboardButton.css'; + +export interface ClipboardButtonProps extends Omit { + value: string; +} + +export type ClipboardState = 'success' | 'error' | null; + +export default function ClipboardButton({ + id, + value, + className = styles.button, + ...otherProps +}: ClipboardButtonProps) { + const [state, setState] = useState(null); + + useEffect(() => { + if (!state) { + return; + } + + const timeoutId = setTimeout(() => { + setState(null); + }, 3000); + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [state]); + + const handleClick = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + setState('success'); + } catch (_) { + setState('error'); + } + }, [value]); + + return ( + + + {state ? ( + + + + ) : null} + + + + + + + ); +} diff --git a/frontend/src/Utilities/getUniqueElementId.ts b/frontend/src/Utilities/getUniqueElementId.ts index dae5150b7..1b380851d 100644 --- a/frontend/src/Utilities/getUniqueElementId.ts +++ b/frontend/src/Utilities/getUniqueElementId.ts @@ -1,7 +1,9 @@ let i = 0; -// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - +/** + * @deprecated Use React's useId() instead + * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + */ export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/package.json b/package.json index 4d62f7d2a..e5e0bbb8f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "classnames": "2.3.2", - "clipboard": "2.0.11", "connected-react-router": "6.9.3", "element-class": "0.2.2", "filesize": "10.0.7", diff --git a/yarn.lock b/yarn.lock index 2fd9a7e55..f46048a70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2477,15 +2477,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2880,11 +2871,6 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -3811,13 +3797,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6271,11 +6250,6 @@ section-iterator@^2.0.0: resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" integrity sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ== -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -6756,11 +6730,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" From 89d730cdfdf44ad738ce77c90d3290cde67ba6a1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 15 Sep 2024 18:51:06 -0700 Subject: [PATCH 542/762] Fixed: Links for Trakt and TVMaze in Gotify notifications --- src/NzbDrone.Core/Notifications/Gotify/Gotify.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs index 83b3568bd..251c0da76 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs @@ -180,13 +180,13 @@ namespace NzbDrone.Core.Notifications.Gotify if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0) { - linkText = "TVMaze"; + linkText = "Trakt"; linkUrl = $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show"; } if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0) { - linkText = "Trakt"; + linkText = "TVMaze"; linkUrl = $"http://www.tvmaze.com/shows/{series.TvMazeId}/_"; } From a73a5cc85caa85e220d9cfe7ca00115d5340b9a5 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 19 Sep 2024 10:25:21 +0000 Subject: [PATCH 543/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: FloatStream <1213193613@qq.com> Co-authored-by: Havok Dan Co-authored-by: Weblate Co-authored-by: fordas Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 11 +++++++++-- src/NzbDrone.Core/Localization/Core/pt_BR.json | 7 ++++++- src/NzbDrone.Core/Localization/Core/zh_CN.json | 12 ++++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 4ab538d01..6f80ec6a6 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -78,7 +78,7 @@ "Torrents": "Torrents", "Ui": "Interfaz", "Underscore": "Guion bajo", - "UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script", + "UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script", "Warn": "Advertencia", "AutoTagging": "Etiquetado Automático", "AddAutoTag": "Añadir etiqueta automática", @@ -2112,5 +2112,12 @@ "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) seleccionado(s)", "LastSearched": "Último buscado", "CustomFormatsSpecificationExceptLanguageHelpText": "Coincide si cualquier idioma distinto del seleccionado está presente", - "CustomFormatsSpecificationExceptLanguage": "Excepto idioma" + "CustomFormatsSpecificationExceptLanguage": "Excepto idioma", + "MinimumCustomFormatScoreIncrement": "Incremento mínimo de puntuación de formato personalizado", + "MinimumCustomFormatScoreIncrementHelpText": "Mejora mínima requerida de la puntuación de formato personalizado entre los lanzamientos existentes y nuevos antes de que {appName} lo considere una actualización", + "NotificationsGotifySettingsMetadataLinks": "Enlaces de metadatos", + "NotificationsGotifySettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones", + "NotificationsGotifySettingsPreferredMetadataLink": "Enlace de metadatos preferido", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Enlace de metadatos para clientes que solo soportan un único enlace", + "SkipFreeSpaceCheckHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 3b717e244..982b8e822 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2110,5 +2110,10 @@ "ManageCustomFormats": "Gerenciar formatos personalizados", "NoCustomFormatsFound": "Nenhum formato personalizado encontrado", "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)", - "LastSearched": "Última Pesquisa" + "LastSearched": "Última Pesquisa", + "SkipFreeSpaceCheckHelpText": "Usar quando {appName} não consegue detectar espaço livre em sua pasta raiz", + "CustomFormatsSpecificationExceptLanguage": "Exceto Idioma", + "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponde se qualquer idioma diferente do idioma selecionado estiver presente", + "MinimumCustomFormatScoreIncrement": "Incremento Mínimo da Pontuação de Formato Personalizado", + "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que {appName} considere isso uma atualização" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 2df0befc0..cae7ea474 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1134,7 +1134,7 @@ "SeriesPremiere": "剧集首播", "ShortDateFormat": "短日期格式", "ShowEpisodes": "显示剧集", - "ShowMonitored": "显示已追踪项", + "ShowMonitored": "显示追踪状态", "ShowMonitoredHelpText": "在海报下显示追踪状态", "ShowNetwork": "显示网络", "ShowPreviousAiring": "显示上一次播出", @@ -1471,8 +1471,8 @@ "UrlBase": "基本URL", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端 {downloadClientName} 已被设置为删除已完成的下载。这可能导致在 {appName} 导入之前,已下载的文件会被您的客户端移除。", "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", - "AutoRedownloadFailed": "重新下载失败", - "AutoRedownloadFailedFromInteractiveSearch": "来自手动搜索的资源重新下载失败", + "AutoRedownloadFailed": "失败时重新下载", + "AutoRedownloadFailedFromInteractiveSearch": "失败时重新下载来自手动搜索的资源", "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从手动搜索中抓取的发布资源下载失败时,自动搜索并尝试下载不同的发布资源", "ImportListSearchForMissingEpisodes": "搜索缺失集", "QueueFilterHasNoItems": "所选的队列过滤器中无项目", @@ -1946,5 +1946,9 @@ "NotificationsTwitterSettingsMentionHelpText": "在发送的推文中提及此用户", "NotificationsTwitterSettingsMention": "提及", "ShowTags": "显示标签", - "ShowTagsHelpText": "在海报下显示标签" + "ShowTagsHelpText": "在海报下显示标签", + "SkipFreeSpaceCheckHelpText": "当 {appName} 无法检测到根目录的剩余空间时使用", + "MinimumCustomFormatScoreIncrement": "自定义格式分数最小增量", + "MinimumCustomFormatScoreIncrementHelpText": "{appName} 将新版本视为升级版本之前,新版本资源相较于现有版本在自定义格式分数上的最小提升", + "LastSearched": "最近搜索" } From 85f53e8cb153fc8a57dc5c2b8f162195ba27408d Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Sat, 21 Sep 2024 19:10:44 +0200 Subject: [PATCH 544/762] New: Parse KCRT as release group Closes #7214 --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 130ffdbb1..62f2abd81 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -88,6 +88,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] + [TestCase("Series.Title.S08E03.720p.WEB.DL.AAC2.0.H.264.KCRT", "KCRT")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3528cc080..584457a7e 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -556,7 +556,7 @@ namespace NzbDrone.Core.Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); // groups whose releases end with RlsGroup) or RlsGroup] private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?(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); From fca8c36156da1d9591483a08beddd115c3cb2c31 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 21 Sep 2024 20:12:01 +0300 Subject: [PATCH 545/762] Guard against using invalid sort keys --- .../ImportListExclusions.tsx | 2 +- frontend/src/Store/Actions/systemActions.js | 1 - .../Blocklist/BlocklistController.cs | 13 ++++++++++- .../History/HistoryController.cs | 9 +++++++- .../ImportListExclusionController.cs | 11 +++++++++- src/Sonarr.Api.V3/Logs/LogController.cs | 8 ++++++- src/Sonarr.Api.V3/Queue/QueueController.cs | 2 +- src/Sonarr.Api.V3/Wanted/CutoffController.cs | 18 +++++++++------ src/Sonarr.Api.V3/Wanted/MissingController.cs | 18 +++++++++------ src/Sonarr.Http/PagingResource.cs | 22 +++++++++++-------- 10 files changed, 74 insertions(+), 30 deletions(-) diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index a93ecda3c..e51ef316f 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -48,7 +48,7 @@ const COLUMNS: Column[] = [ isSortable: true, }, { - name: 'tvdbid', + name: 'tvdbId', label: () => translate('TvdbId'), isVisible: true, isSortable: true, diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 0f2410846..2c53bda7b 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -110,7 +110,6 @@ export const defaultState = { { name: 'actions', columnLabel: () => translate('Actions'), - isSortable: true, isVisible: true, isModifiable: false } diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs index c1f69974b..d29b1a86e 100644 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Blocklisting; @@ -28,7 +30,16 @@ namespace Sonarr.Api.V3.Blocklist public PagingResource GetBlocklist([FromQuery] PagingRequestResource paging, [FromQuery] int[] seriesIds = null, [FromQuery] DownloadProtocol[] protocols = null) { var pagingResource = new PagingResource(paging); - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + var pagingSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "series.sortTitle", + "sourceTitle", + "date", + "indexer" + }, + "date", + SortDirection.Descending); if (seriesIds?.Any() == true) { diff --git a/src/Sonarr.Api.V3/History/HistoryController.cs b/src/Sonarr.Api.V3/History/HistoryController.cs index 02eae491a..b0810113e 100644 --- a/src/Sonarr.Api.V3/History/HistoryController.cs +++ b/src/Sonarr.Api.V3/History/HistoryController.cs @@ -65,7 +65,14 @@ namespace Sonarr.Api.V3.History public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, [FromQuery(Name = "eventType")] int[] eventTypes, int? episodeId, string downloadId, [FromQuery] int[] seriesIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) { var pagingResource = new PagingResource(paging); - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + var pagingSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "series.sortTitle", + "date" + }, + "date", + SortDirection.Descending); if (eventTypes != null && eventTypes.Any()) { diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index c48604bc8..30bd035b4 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore; using NzbDrone.Core.ImportLists.Exclusions; using Sonarr.Http; using Sonarr.Http.Extensions; @@ -46,7 +47,15 @@ namespace Sonarr.Api.V3.ImportLists public PagingResource GetImportListExclusionsPaged([FromQuery] PagingRequestResource paging) { var pagingResource = new PagingResource(paging); - var pageSpec = pagingResource.MapToPagingSpec(); + var pageSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "id", + "tvdbId", + "title" + }, + "id", + SortDirection.Descending); return pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource); } diff --git a/src/Sonarr.Api.V3/Logs/LogController.cs b/src/Sonarr.Api.V3/Logs/LogController.cs index ba8021a42..ba4ff7dcf 100644 --- a/src/Sonarr.Api.V3/Logs/LogController.cs +++ b/src/Sonarr.Api.V3/Logs/LogController.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -29,7 +31,11 @@ namespace Sonarr.Api.V3.Logs } var pagingResource = new PagingResource(paging); - var pageSpec = pagingResource.MapToPagingSpec(); + var pageSpec = pagingResource.MapToPagingSpec(new HashSet(StringComparer.OrdinalIgnoreCase) + { + "id", + "time" + }); if (pageSpec.SortKey == "time") { diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 2e74dec94..34315de1e 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -139,7 +139,7 @@ namespace Sonarr.Api.V3.Queue public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, int? quality = null) { var pagingResource = new PagingResource(paging); - var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); + var pagingSpec = pagingResource.MapToPagingSpec(null, "timeleft", SortDirection.Ascending); return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } diff --git a/src/Sonarr.Api.V3/Wanted/CutoffController.cs b/src/Sonarr.Api.V3/Wanted/CutoffController.cs index 22be80366..8da4dc597 100644 --- a/src/Sonarr.Api.V3/Wanted/CutoffController.cs +++ b/src/Sonarr.Api.V3/Wanted/CutoffController.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -31,13 +33,15 @@ namespace Sonarr.Api.V3.Wanted public PagingResource GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false, bool monitored = true) { var pagingResource = new PagingResource(paging); - var pagingSpec = new PagingSpec - { - Page = pagingResource.Page, - PageSize = pagingResource.PageSize, - SortKey = pagingResource.SortKey, - SortDirection = pagingResource.SortDirection - }; + var pagingSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "series.sortTitle", + "episodes.airDateUtc", + "episodes.lastSearchTime" + }, + "episodes.airDateUtc", + SortDirection.Ascending); if (monitored) { diff --git a/src/Sonarr.Api.V3/Wanted/MissingController.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs index f7444f7a3..2c05025c6 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -27,13 +29,15 @@ namespace Sonarr.Api.V3.Wanted public PagingResource GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeImages = false, bool monitored = true) { var pagingResource = new PagingResource(paging); - var pagingSpec = new PagingSpec - { - Page = pagingResource.Page, - PageSize = pagingResource.PageSize, - SortKey = pagingResource.SortKey, - SortDirection = pagingResource.SortDirection - }; + var pagingSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "series.sortTitle", + "episodes.airDateUtc", + "episodes.lastSearchTime" + }, + "episodes.airDateUtc", + SortDirection.Ascending); if (monitored) { diff --git a/src/Sonarr.Http/PagingResource.cs b/src/Sonarr.Http/PagingResource.cs index 64123e66a..f29c21a3f 100644 --- a/src/Sonarr.Http/PagingResource.cs +++ b/src/Sonarr.Http/PagingResource.cs @@ -38,7 +38,11 @@ namespace Sonarr.Http public static class PagingResourceMapper { - public static PagingSpec MapToPagingSpec(this PagingResource pagingResource, string defaultSortKey = "Id", SortDirection defaultSortDirection = SortDirection.Ascending) + public static PagingSpec MapToPagingSpec( + this PagingResource pagingResource, + HashSet allowedSortKeys, + string defaultSortKey = "id", + SortDirection defaultSortDirection = SortDirection.Ascending) { var pagingSpec = new PagingSpec { @@ -48,15 +52,15 @@ namespace Sonarr.Http SortDirection = pagingResource.SortDirection, }; - if (pagingResource.SortKey == null) - { - pagingSpec.SortKey = defaultSortKey; + pagingSpec.SortKey = pagingResource.SortKey != null && + allowedSortKeys is { Count: > 0 } && + allowedSortKeys.Contains(pagingResource.SortKey) + ? pagingResource.SortKey + : defaultSortKey; - if (pagingResource.SortDirection == SortDirection.Default) - { - pagingSpec.SortDirection = defaultSortDirection; - } - } + pagingSpec.SortDirection = pagingResource.SortDirection == SortDirection.Default + ? defaultSortDirection + : pagingResource.SortDirection; return pagingSpec; } From 3976e5daf7a4b4c4dbfe36635c95b82477f191ca Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 16 Sep 2024 12:16:26 -0700 Subject: [PATCH 546/762] Fixed: Interactive searches causing multiple requests to indexers --- frontend/src/InteractiveSearch/InteractiveSearch.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx index 92fc06dbc..6dd3c2f1f 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -161,13 +161,12 @@ function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) { ); useEffect(() => { - // If search results are not yet isPopulated fetch them, - // otherwise re-show the existing props. + // Only fetch releases if they are not already being fetched and not yet populated. - if (!isPopulated) { + if (!isFetching && !isPopulated) { dispatch(fetchReleases(searchPayload)); } - }, [isPopulated, searchPayload, dispatch]); + }, [isFetching, isPopulated, searchPayload, dispatch]); const errorMessage = getErrorMessage(error); From 30c36fdc3baa686102ff124833c7963fc786f251 Mon Sep 17 00:00:00 2001 From: momo Date: Sat, 21 Sep 2024 19:15:51 +0200 Subject: [PATCH 547/762] Fix description for API key as query parameter --- src/NzbDrone.Host/Startup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index f26a8fcf9..ecf50e427 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -135,7 +135,7 @@ namespace NzbDrone.Host Name = "apikey", Type = SecuritySchemeType.ApiKey, Scheme = "apiKey", - Description = "Apikey passed as header", + Description = "Apikey passed as query parameter", In = ParameterLocation.Query, Reference = new OpenApiReference { From c9aa59340c945174dfad4c8ecf8c8d7d83dccd61 Mon Sep 17 00:00:00 2001 From: ManiMatter <124743318+ManiMatter@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:16:05 +0200 Subject: [PATCH 548/762] Add 'includeSeries' and 'includeEpisodeFile' to Episode API endpoint --- src/Sonarr.Api.V3/Episodes/EpisodeController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeController.cs b/src/Sonarr.Api.V3/Episodes/EpisodeController.cs index 7b53a1f54..326bc36a9 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeController.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeController.cs @@ -25,24 +25,24 @@ namespace Sonarr.Api.V3.Episodes [HttpGet] [Produces("application/json")] - public List GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List episodeIds, int? episodeFileId, bool includeImages = false) + public List GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List episodeIds, int? episodeFileId, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false) { if (seriesId.HasValue) { if (seasonNumber.HasValue) { - return MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), false, false, includeImages); + return MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), includeSeries, includeEpisodeFile, includeImages); } - return MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), false, false, includeImages); + return MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), includeSeries, includeEpisodeFile, includeImages); } else if (episodeIds.Any()) { - return MapToResource(_episodeService.GetEpisodes(episodeIds), false, false, includeImages); + return MapToResource(_episodeService.GetEpisodes(episodeIds), includeSeries, includeEpisodeFile, includeImages); } else if (episodeFileId.HasValue) { - return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), false, false, includeImages); + return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), includeSeries, includeEpisodeFile, includeImages); } throw new BadRequestException("seriesId or episodeIds must be provided"); From 9875e550a8df22347f09c6658f185d94c21ff45b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 16 Sep 2024 16:30:17 -0700 Subject: [PATCH 549/762] Fixed: Adding Bluray 576p to some profiles --- .../214_add_bluray576p_in_profileFixture.cs | 41 ----- ...rofiles_with_grouped_blurary480pFixture.cs | 141 ++++++++++++++++++ .../Migration/214_fake_bluray576p.cs | 14 ++ ...y_in_profiles_with_grouped_blurary480p.cs} | 44 +++--- 4 files changed, 180 insertions(+), 60 deletions(-) delete mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480pFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/214_fake_bluray576p.cs rename src/NzbDrone.Core/Datastore/Migration/{214_add_blurary576p_quality_in_profiles.cs => 215_add_blurary576p_quality_in_profiles_with_grouped_blurary480p.cs} (73%) diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs deleted file mode 100644 index 8fd403b91..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/214_add_bluray576p_in_profileFixture.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class add_bluray576p_in_profileFixture : MigrationTest - { - private string GenerateQualityJson(int quality, bool allowed) - { - return $"{{ \"quality\": {quality}, \"allowed\": {allowed.ToString().ToLowerInvariant()} }}"; - } - - [Test] - public void should_add_bluray576p_to_old_profile() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("QualityProfiles").Row(new - { - Id = 0, - Name = "Bluray", - Cutoff = 7, - Items = $"[{GenerateQualityJson((int)Quality.DVD, true)}, {GenerateQualityJson((int)Quality.Bluray480p, true)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" - }); - }); - - var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); - - var items = profiles.First().Items; - items.Should().HaveCount(4); - items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p, (int)Quality.Bluray720p); - items.Select(v => v.Allowed).Should().Equal(true, true, true, false); - items.Select(v => v.Name).Should().Equal(null, null, null, null); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480pFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480pFixture.cs new file mode 100644 index 000000000..1a72d8379 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480pFixture.cs @@ -0,0 +1,141 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_blurary576p_quality_in_profiles_with_grouped_blurary480pFixture : MigrationTest + { + private string GenerateQualityJson(int quality, bool allowed) + { + return $"{{ \"quality\": {quality}, \"allowed\": {allowed.ToString().ToLowerInvariant()} }}"; + } + + private string GenerateQualityGroupJson(int id, string name, int[] qualities, bool allowed) + { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"items\": [{string.Join(", ", qualities.Select(q => $"{{ \"quality\": {q} }}"))}], \"allowed\": {allowed.ToString().ToLowerInvariant()} }}"; + } + + [Test] + public void should_add_bluray576p_to_old_profile() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityJson((int)Quality.DVD, true)}, {GenerateQualityJson((int)Quality.Bluray480p, true)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(4); + items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p, (int)Quality.Bluray720p); + items.Select(v => v.Allowed).Should().Equal(true, true, true, false); + items.Select(v => v.Name).Should().Equal(null, null, null, null); + } + + [Test] + public void should_not_allow_bluray576p_if_blurary480p_not_allowed() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityJson((int)Quality.DVD, true)}, {GenerateQualityJson((int)Quality.Bluray480p, false)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(4); + items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p, (int)Quality.Bluray720p); + items.Select(v => v.Allowed).Should().Equal(true, false, false, false); + items.Select(v => v.Name).Should().Equal(null, null, null, null); + } + + [Test] + public void should_add_bluray576p_to_old_profile_with_grouped_bluray_480p() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityGroupJson(1000, "DVD", new[] { (int)Quality.DVD, (int)Quality.Bluray480p }, true)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(3); + items.Select(v => v.Quality).Should().Equal(null, (int)Quality.Bluray576p, (int)Quality.Bluray720p); + items.Select(v => v.Id).Should().Equal(1000, 0, 0); + items.Select(v => v.Allowed).Should().Equal(true, true, false); + items.Select(v => v.Name).Should().Equal("DVD", null, null); + } + + [Test] + public void should_not_add_bluray576p_to_profile_with_bluray_576p() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityJson((int)Quality.DVD, true)}, {GenerateQualityJson((int)Quality.Bluray480p, false)}, {GenerateQualityJson((int)Quality.Bluray576p, false)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(4); + items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p, (int)Quality.Bluray720p); + items.Select(v => v.Allowed).Should().Equal(true, false, false, false); + items.Select(v => v.Name).Should().Equal(null, null, null, null); + } + + [Test] + public void should_not_add_bluray576p_to_profile_with_grouped_bluray_576p() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("QualityProfiles").Row(new + { + Id = 0, + Name = "Bluray", + Cutoff = 7, + Items = $"[{GenerateQualityGroupJson(1000, "DVD", new[] { (int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p }, true)}, {GenerateQualityJson((int)Quality.Bluray720p, false)}]" + }); + }); + + var profiles = db.Query("SELECT \"Items\" FROM \"QualityProfiles\" LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(2); + items.Select(v => v.Quality).Should().Equal(null, (int)Quality.Bluray720p); + items.Select(v => v.Id).Should().Equal(1000, 0); + items.Select(v => v.Allowed).Should().Equal(true, false); + items.Select(v => v.Name).Should().Equal("DVD", null); + items.First().Items.Select(v => v.Quality).Should().Equal((int)Quality.DVD, (int)Quality.Bluray480p, (int)Quality.Bluray576p); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/214_fake_bluray576p.cs b/src/NzbDrone.Core/Datastore/Migration/214_fake_bluray576p.cs new file mode 100644 index 000000000..b5551ebce --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/214_fake_bluray576p.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(214)] + public class add_blurary576p_quality_in_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + // Replaced with 215 + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480p.cs similarity index 73% rename from src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs rename to src/NzbDrone.Core/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480p.cs index 8024313cd..3a8bd6019 100644 --- a/src/NzbDrone.Core/Datastore/Migration/214_add_blurary576p_quality_in_profiles.cs +++ b/src/NzbDrone.Core/Datastore/Migration/215_add_blurary576p_quality_in_profiles_with_grouped_blurary480p.cs @@ -9,8 +9,8 @@ using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration { - [Migration(214)] - public class add_blurary576p_quality_in_profiles : NzbDroneMigrationBase + [Migration(215)] + public class add_blurary576p_quality_in_profiles_with_grouped_blurary480p : NzbDroneMigrationBase { protected override void MainDbUpgrade() { @@ -19,46 +19,46 @@ namespace NzbDrone.Core.Datastore.Migration private void ConvertProfile(IDbConnection conn, IDbTransaction tran) { - var updater = new ProfileUpdater214(conn, tran); + var updater = new ProfileUpdater215(conn, tran); updater.InsertQualityAfter(13, 22); // Group Bluray576p with Bluray480p updater.Commit(); } } - public class Profile214 + public class Profile215 { public int Id { get; set; } public string Name { get; set; } public int Cutoff { get; set; } - public List Items { get; set; } + public List Items { get; set; } } - public class ProfileItem214 + public class ProfileItem215 { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public int Id { get; set; } public string Name { get; set; } public int? Quality { get; set; } - public List Items { get; set; } + public List Items { get; set; } public bool Allowed { get; set; } - public ProfileItem214() + public ProfileItem215() { - Items = new List(); + Items = new List(); } } - public class ProfileUpdater214 + public class ProfileUpdater215 { private readonly IDbConnection _connection; private readonly IDbTransaction _transaction; - private List _profiles; - private HashSet _changedProfiles = new HashSet(); + private List _profiles; + private HashSet _changedProfiles = new HashSet(); - public ProfileUpdater214(IDbConnection conn, IDbTransaction tran) + public ProfileUpdater215(IDbConnection conn, IDbTransaction tran) { _connection = conn; _transaction = tran; @@ -86,11 +86,17 @@ namespace NzbDrone.Core.Datastore.Migration { foreach (var profile in _profiles) { - var findIndex = profile.Items.FindIndex(v => v.Quality == find); + // Don't update if Bluray 576p was already added to the profile in 214 + if (profile.Items.FindIndex(v => v.Quality == quality || v.Items.Any(i => i.Quality == quality)) > -1) + { + continue; + } + + var findIndex = profile.Items.FindIndex(v => v.Quality == find || v.Items.Any(i => i.Quality == find)); if (findIndex > -1) { - profile.Items.Insert(findIndex + 1, new ProfileItem214 + profile.Items.Insert(findIndex + 1, new ProfileItem215 { Quality = quality, Allowed = profile.Items[findIndex].Allowed @@ -101,9 +107,9 @@ namespace NzbDrone.Core.Datastore.Migration } } - private List GetProfiles() + private List GetProfiles() { - var profiles = new List(); + var profiles = new List(); using (var getProfilesCmd = _connection.CreateCommand()) { @@ -114,12 +120,12 @@ namespace NzbDrone.Core.Datastore.Migration { while (profileReader.Read()) { - profiles.Add(new Profile214 + profiles.Add(new Profile215 { Id = profileReader.GetInt32(0), Name = profileReader.GetString(1), Cutoff = profileReader.GetInt32(2), - Items = Json.Deserialize>(profileReader.GetString(3)) + Items = Json.Deserialize>(profileReader.GetString(3)) }); } } From 4b72a0a4e8075a2555ff876cd14d40b1a250dbba Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 16 Sep 2024 16:30:59 -0700 Subject: [PATCH 550/762] Fixed: Rejections for Custom Format score increment --- .../Specifications/QueueSpecification.cs | 13 ++++++++----- .../Specifications/RssSync/HistorySpecification.cs | 6 +++++- .../Specifications/UpgradeDiskSpecification.cs | 5 +++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 63c4b51e9..97a1993e8 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -79,19 +79,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications switch (upgradeableRejectReason) { case UpgradeableRejectReason.BetterQuality: - return Decision.Reject("Release in queue on disk is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + return Decision.Reject("Release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); case UpgradeableRejectReason.BetterRevision: - return Decision.Reject("Release in queue on disk is of equal or higher revision: {0}", remoteEpisode.ParsedEpisodeInfo.Quality.Revision); + return Decision.Reject("Release in queue is of equal or higher revision: {0}", remoteEpisode.ParsedEpisodeInfo.Quality.Revision); case UpgradeableRejectReason.QualityCutoff: - return Decision.Reject("Release in queue on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + return Decision.Reject("Release in queue meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); case UpgradeableRejectReason.CustomFormatCutoff: - return Decision.Reject("Release in queue on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); + return Decision.Reject("Release in queue meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("Release in queue on disk has an equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(queuedItemCustomFormats)); + return Decision.Reject("Release in queue has an equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(queuedItemCustomFormats)); + + case UpgradeableRejectReason.MinCustomFormatScore: + return Decision.Reject("Release in queue has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); } _logger.Debug("Checking if profiles allow upgrading. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index b9c9429f0..34b2ace4b 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -93,6 +93,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { case UpgradeableRejectReason.None: continue; + case UpgradeableRejectReason.BetterQuality: return Decision.Reject("{0} grab event in history is of equal or higher preference: {1}", rejectionSubject, mostRecent.Quality); @@ -106,7 +107,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Reject("{0} grab event in history meets Custom Format cutoff: {1}", rejectionSubject, qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("{0} grab event in history has an equal or higher custom format score: {1}", rejectionSubject, qualityProfile.CalculateCustomFormatScore(customFormats)); + return Decision.Reject("{0} grab event in history has an equal or higher Custom Format score: {1}", rejectionSubject, qualityProfile.CalculateCustomFormatScore(customFormats)); + + case UpgradeableRejectReason.MinCustomFormatScore: + return Decision.Reject("{0} grab event in history has Custom Format score within Custom Format score increment: {1}", rejectionSubject, qualityProfile.MinUpgradeFormatScore); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index c316a2fb7..86d9edcb2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -63,6 +63,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { case UpgradeableRejectReason.None: continue; + case UpgradeableRejectReason.BetterQuality: return Decision.Reject("Existing file on disk is of equal or higher preference: {0}", file.Quality); @@ -76,10 +77,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Reject("Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("Existing file on disk has a equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); + return Decision.Reject("Existing file on disk has a equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); case UpgradeableRejectReason.MinCustomFormatScore: - return Decision.Reject("Existing file differential between new release does not meet minimum Custom Format score increment: {0}", qualityProfile.MinFormatScore); + return Decision.Reject("Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); } } From ca38a9b5774243c2e1c3cd1abfc8d218bca4409e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 21 Sep 2024 08:55:05 -0700 Subject: [PATCH 551/762] Fixed: Aggregating media files with 576p resolution --- .../Aggregators/AggregateQualityFixture.cs | 36 +++++++++++++++++++ .../AugmentQualityFromMediaInfoFixture.cs | 22 ++++++++++++ .../Quality/AugmentQualityFromMediaInfo.cs | 6 ++++ src/NzbDrone.Core/Qualities/QualityFinder.cs | 14 ++++++++ 4 files changed, 78 insertions(+) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs index b88b66ffb..d25181345 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs @@ -170,5 +170,41 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators result.Quality.Revision.Version.Should().Be(2); result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name); } + + [Test] + public void should_return_Bluray576p_when_Bluray_came_from_name_and_mediainfo_indicates_576p() + { + _nameAugmenter.Setup(s => s.AugmentQuality(It.IsAny(), It.IsAny())) + .Returns(new AugmentQualityResult(QualitySource.Bluray, Confidence.Default, 480, Confidence.Default, new Revision(0), Confidence.Tag)); + + _mediaInfoAugmenter.Setup(s => s.AugmentQuality(It.IsAny(), It.IsAny())) + .Returns(AugmentQualityResult.ResolutionOnly(576, Confidence.MediaInfo)); + + GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter); + + var result = Subject.Aggregate(new LocalEpisode(), null); + + result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name); + result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo); + result.Quality.Quality.Should().Be(Quality.Bluray576p); + } + + [Test] + public void should_return_SDTV_when_HDTV_came_from_name_and_mediainfo_indicates_576p() + { + _nameAugmenter.Setup(s => s.AugmentQuality(It.IsAny(), It.IsAny())) + .Returns(new AugmentQualityResult(QualitySource.Television, Confidence.Default, 480, Confidence.Default, new Revision(0), Confidence.Tag)); + + _mediaInfoAugmenter.Setup(s => s.AugmentQuality(It.IsAny(), It.IsAny())) + .Returns(AugmentQualityResult.ResolutionOnly(576, Confidence.MediaInfo)); + + GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter); + + var result = Subject.Aggregate(new LocalEpisode(), null); + + result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name); + result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo); + result.Quality.Quality.Should().Be(Quality.SDTV); + } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs index af72aa538..783364899 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs @@ -47,6 +47,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Au [TestCase(1490, 1, 720)] [TestCase(1280, 1, 720)] // HD [TestCase(1200, 1, 720)] + [TestCase(1000, 1, 576)] + [TestCase(720, 576, 576)] [TestCase(800, 1, 480)] [TestCase(720, 1, 480)] // SDTV [TestCase(600, 1, 480)] @@ -108,5 +110,25 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Au result.Resolution.Should().Be(1080); result.Source.Should().Be(QualitySource.Unknown); } + + [Test] + public void should_include_source_for_576_if_extracted_from_title() + { + var mediaInfo = Builder.CreateNew() + .With(m => m.Width = 1024) + .With(m => m.Height = 576) + .With(m => m.Title = "Series.Title.S01E05.Bluray.x264-Sonarr") + .Build(); + + var localEpisode = Builder.CreateNew() + .With(l => l.MediaInfo = mediaInfo) + .Build(); + + var result = Subject.AugmentQuality(localEpisode, null); + + result.Should().NotBe(null); + result.Resolution.Should().Be(576); + result.Source.Should().Be(QualitySource.Bluray); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs index d854bbfaa..e03748e98 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs @@ -63,6 +63,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augment return AugmentQualityResult.SourceAndResolutionOnly(source, sourceConfidence, 720, Confidence.MediaInfo); } + if (width >= 1000 || height >= 560) + { + _logger.Trace("Resolution {0}x{1} considered 576p", width, height); + return AugmentQualityResult.SourceAndResolutionOnly(source, sourceConfidence, 576, Confidence.MediaInfo); + } + if (width > 0 && height > 0) { _logger.Trace("Resolution {0}x{1} considered 480p", width, height); diff --git a/src/NzbDrone.Core/Qualities/QualityFinder.cs b/src/NzbDrone.Core/Qualities/QualityFinder.cs index 8c75468ee..969d19128 100644 --- a/src/NzbDrone.Core/Qualities/QualityFinder.cs +++ b/src/NzbDrone.Core/Qualities/QualityFinder.cs @@ -17,6 +17,20 @@ namespace NzbDrone.Core.Qualities return matchingQuality; } + // Handle 576p releases that have a Television or Web source, so they don't get rolled up to Bluray 576p + if (resolution < 720) + { + switch (source) + { + case QualitySource.Television: + return Quality.SDTV; + case QualitySource.Web: + return Quality.WEBDL480p; + case QualitySource.WebRip: + return Quality.WEBRip480p; + } + } + var matchingResolution = Quality.All.Where(q => q.Resolution == resolution) .OrderBy(q => q.Source) .ToList(); From 27da0413882dc87e1617a5d091ac5111589e61a6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 16 Sep 2024 21:45:09 -0700 Subject: [PATCH 552/762] Fixed: Reprocessing manual import items unable to detect sample Closes #7221 --- .../Manual/ManualImportService.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index c413172aa..0e5d3c732 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -155,10 +155,19 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (episodeIds.Any()) { var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem; + var episodes = _episodeService.GetEpisodes(episodeIds); + var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace() + ? Parser.Parser.ParseReleaseGroup(path) + : releaseGroup; + var finalQuality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; + var finalLanguges = + languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown + ? languageParse + : languages; var localEpisode = new LocalEpisode(); localEpisode.Series = series; - localEpisode.Episodes = _episodeService.GetEpisodes(episodeIds); + localEpisode.Episodes = episodes; localEpisode.FileEpisodeInfo = Parser.Parser.ParsePath(path); localEpisode.DownloadClientEpisodeInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title); localEpisode.DownloadItem = downloadClientItem; @@ -166,15 +175,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.SceneSource = SceneSource(series, rootFolder); localEpisode.ExistingFile = series.Path.IsParentPath(path); localEpisode.Size = _diskProvider.GetFileSize(path); - 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.ReleaseGroup = finalReleaseGroup; + localEpisode.Languages = finalLanguges; + localEpisode.Quality = finalQuality; localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; localEpisode.ReleaseType = releaseType; localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + // Augment episode file so imported files have all additional information an automatic import would + localEpisode = _aggregationService.Augment(localEpisode, downloadClientItem); + + // Reapply the user-chosen values. + localEpisode.Series = series; + localEpisode.Episodes = episodes; + localEpisode.ReleaseGroup = finalReleaseGroup; + localEpisode.Quality = finalQuality; + localEpisode.Languages = finalLanguges; + localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; + localEpisode.ReleaseType = releaseType; + return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null); } From 0fa8e24f4886ce39b2cf86a4901cdcbe89aa0b39 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 17 Sep 2024 23:19:27 +0300 Subject: [PATCH 553/762] New: Fetch up to 1000 series from Plex Watchlist --- .../ImportLists/Plex/PlexImport.cs | 18 +++++++----------- .../Plex/PlexListRequestGenerator.cs | 17 +++++++++-------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs index ee50b3540..d7ec4b6a3 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexImport.cs @@ -14,11 +14,15 @@ namespace NzbDrone.Core.ImportLists.Plex { public class PlexImport : HttpImportListBase { - public readonly IPlexTvService _plexTvService; - + public override string Name => _localizationService.GetLocalizedString("ImportListsPlexSettingsWatchlistName"); public override ImportListType ListType => ImportListType.Plex; public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6); + public override int PageSize => 100; + public override TimeSpan RateLimit => TimeSpan.FromSeconds(5); + + private readonly IPlexTvService _plexTvService; + public PlexImport(IPlexTvService plexTvService, IHttpClient httpClient, IImportListStatusService importListStatusService, @@ -31,15 +35,10 @@ namespace NzbDrone.Core.ImportLists.Plex _plexTvService = plexTvService; } - public override string Name => _localizationService.GetLocalizedString("ImportListsPlexSettingsWatchlistName"); - public override int PageSize => 50; - public override ImportListFetchResult Fetch() { Settings.Validate().Filter("AccessToken").ThrowOnError(); - // var generator = GetRequestGenerator(); - return FetchItems(g => g.GetListItems()); } @@ -50,10 +49,7 @@ namespace NzbDrone.Core.ImportLists.Plex public override IImportListRequestGenerator GetRequestGenerator() { - return new PlexListRequestGenerator(_plexTvService, PageSize) - { - Settings = Settings - }; + return new PlexListRequestGenerator(_plexTvService, Settings, PageSize); } public override object RequestAction(string action, IDictionary query) diff --git a/src/NzbDrone.Core/ImportLists/Plex/PlexListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Plex/PlexListRequestGenerator.cs index 30909dd61..d0add0ffb 100644 --- a/src/NzbDrone.Core/ImportLists/Plex/PlexListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Plex/PlexListRequestGenerator.cs @@ -5,13 +5,16 @@ namespace NzbDrone.Core.ImportLists.Plex { public class PlexListRequestGenerator : IImportListRequestGenerator { - private readonly IPlexTvService _plexTvService; - private readonly int _pageSize; - public PlexListSettings Settings { get; set; } + private const int MaxPages = 10; - public PlexListRequestGenerator(IPlexTvService plexTvService, int pageSize) + private readonly IPlexTvService _plexTvService; + private readonly PlexListSettings _settings; + private readonly int _pageSize; + + public PlexListRequestGenerator(IPlexTvService plexTvService, PlexListSettings settings, int pageSize) { _plexTvService = plexTvService; + _settings = settings; _pageSize = pageSize; } @@ -26,11 +29,9 @@ namespace NzbDrone.Core.ImportLists.Plex private IEnumerable GetSeriesRequest() { - var maxPages = 10; - - for (var page = 0; page < maxPages; page++) + for (var page = 0; page < MaxPages; page++) { - yield return new ImportListRequest(_plexTvService.GetWatchlist(Settings.AccessToken, _pageSize, page * _pageSize)); + yield return new ImportListRequest(_plexTvService.GetWatchlist(_settings.AccessToken, _pageSize, page * _pageSize)); } } } From faf9173b3b4a298e3afa9a186e66ba6764ac055e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 19 Sep 2024 19:28:07 -0700 Subject: [PATCH 554/762] Fixed: Unable to login when instance name contained brackets Closes #7229 --- .../Authentication/AuthenticationBuilderExtensions.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 361a091f6..0fa510e3e 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; -using System.Web; +using System.Text.RegularExpressions; +using Diacritical; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,8 @@ namespace Sonarr.Http.Authentication { public static class AuthenticationBuilderExtensions { + private static readonly Regex CookieNameRegex = new Regex(@"[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action options) { return authenticationBuilder.AddScheme(name, options); @@ -35,8 +38,10 @@ namespace Sonarr.Http.Authentication services.AddOptions(AuthenticationType.Forms.ToString()) .Configure((options, configFileProvider) => { - // Url Encode the cookie name to account for spaces or other invalid characters in the configured instance name - var instanceName = HttpUtility.UrlEncode(configFileProvider.InstanceName); + // Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names + var instanceName = configFileProvider.InstanceName; + instanceName = instanceName.RemoveDiacritics(); + instanceName = CookieNameRegex.Replace(instanceName, string.Empty); options.Cookie.Name = $"{instanceName}Auth"; options.AccessDeniedPath = "/login?loginFailed=true"; From 75fae9262c6ca003d24df9fcf035d75b1e90f994 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2024 09:31:52 -0400 Subject: [PATCH 555/762] Update src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs Co-authored-by: Bogdan --- .../Authentication/AuthenticationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 0fa510e3e..3e3f17220 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -11,7 +11,7 @@ namespace Sonarr.Http.Authentication { public static class AuthenticationBuilderExtensions { - private static readonly Regex CookieNameRegex = new Regex(@"[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex CookieNameRegex = new Regex(@"[^a-z0-9]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action options) { From c199fd05d30e08968e5b03df15af8e02148c8c2d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 19 Sep 2024 20:03:40 -0700 Subject: [PATCH 556/762] Fixed: Don't set last write time on episode files if difference is within the same second Closes #7228 --- src/NzbDrone.Common/Extensions/DateTimeExtensions.cs | 7 ++++++- .../MediaFiles/UpdateEpisodeFileService.cs | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs b/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs index 75be57cb6..38ff663ae 100644 --- a/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs +++ b/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace NzbDrone.Common.Extensions { @@ -38,5 +38,10 @@ namespace NzbDrone.Common.Extensions { return dateTime >= afterDateTime && dateTime <= beforeDateTime; } + + public static DateTime WithoutTicks(this DateTime dateTime) + { + return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond)); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/UpdateEpisodeFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateEpisodeFileService.cs index 172cb5aba..539cc1f64 100644 --- a/src/NzbDrone.Core/MediaFiles/UpdateEpisodeFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpdateEpisodeFileService.cs @@ -84,7 +84,7 @@ namespace NzbDrone.Core.MediaFiles if (DateTime.TryParse(fileDate + ' ' + fileTime, out var airDate)) { // avoiding false +ve checks and set date skewing by not using UTC (Windows) - var oldDateTime = _diskProvider.FileGetLastWrite(filePath); + var oldLastWrite = _diskProvider.FileGetLastWrite(filePath); if (OsInfo.IsNotWindows && airDate < EpochTime) { @@ -92,12 +92,12 @@ namespace NzbDrone.Core.MediaFiles airDate = EpochTime; } - if (!DateTime.Equals(airDate, oldDateTime)) + if (!DateTime.Equals(airDate.WithoutTicks(), oldLastWrite.WithoutTicks())) { try { _diskProvider.FileSetLastWriteTime(filePath, airDate); - _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldDateTime, airDate); + _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDate); return true; } @@ -125,11 +125,11 @@ namespace NzbDrone.Core.MediaFiles airDateUtc = EpochTime; } - if (!DateTime.Equals(airDateUtc, oldLastWrite)) + if (!DateTime.Equals(airDateUtc.WithoutTicks(), oldLastWrite.WithoutTicks())) { try { - _diskProvider.FileSetLastWriteTime(filePath, airDateUtc); + _diskProvider.FileSetLastWriteTime(filePath, airDateUtc.AddMilliseconds(oldLastWrite.Millisecond)); _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDateUtc); return true; From 106ffd410c11a902cbde6ad3b3d9ffe51fd90bd7 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 21 Sep 2024 09:38:54 -0700 Subject: [PATCH 557/762] New: Persist sort in Select Episodes modal Closes #7233 --- frontend/src/Store/Actions/episodeSelectionActions.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/episodeSelectionActions.js b/frontend/src/Store/Actions/episodeSelectionActions.js index 14c39aa07..5ab9a59c8 100644 --- a/frontend/src/Store/Actions/episodeSelectionActions.js +++ b/frontend/src/Store/Actions/episodeSelectionActions.js @@ -24,6 +24,11 @@ export const defaultState = { items: [] }; +export const persistState = [ + 'episodeSelection.sortKey', + 'episodeSelection.sortDirection' +]; + // // Actions Types @@ -54,7 +59,9 @@ export const reducers = createHandleActions({ [CLEAR_EPISODES]: (state) => { return updateSectionState(state, section, { - ...defaultState + ...defaultState, + sortKey: state.sortKey, + sortDirection: state.sortDirection }); } From e196c1be69593b26c49e9844efd56d4322e3b334 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Sat, 21 Sep 2024 17:19:35 +0000 Subject: [PATCH 558/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 5db55798e..1f835fbe7 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -1927,6 +1927,22 @@ "format": "int32" } }, + { + "name": "includeSeries", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeEpisodeFile", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, { "name": "includeImages", "in": "query", @@ -12364,7 +12380,7 @@ }, "apikey": { "type": "apiKey", - "description": "Apikey passed as header", + "description": "Apikey passed as query parameter", "name": "apikey", "in": "query" } From be4a9e9491e8a8ecb62989ad6befbcf0fb9bced0 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 27 Sep 2024 02:25:22 +0000 Subject: [PATCH 559/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ Co-authored-by: Lizandra Candido da Silva Co-authored-by: Weblate Co-authored-by: liuwqq <843384478@qq.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/tr.json | 14 +++++++++++++- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 982b8e822..fe447dae8 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -365,7 +365,7 @@ "RejectionCount": "Número de rejeição", "SubtitleLanguages": "Idiomas das Legendas", "UnmonitoredOnly": "Somente Não Monitorados", - "AddAutoTag": "Adicionar Tag Automática", + "AddAutoTag": "Adicionar tag automática", "AddCondition": "Adicionar Condição", "Conditions": "Condições", "CloneAutoTag": "Clonar Tag Automática", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index bb900c16b..bca885725 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -855,5 +855,17 @@ "LogSizeLimitHelpText": "Arşivlemeden önce MB cinsinden maksimum log dosya boyutu. Varsayılan 1 MB'tır.", "ProgressBarProgress": "İlerleme Çubuğu %{progress} seviyesinde", "CountVotes": "{votes} oy", - "UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}" + "UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}", + "MinimumCustomFormatScoreIncrement": "Minimum Özel Format Puanı Artışı", + "MinimumCustomFormatScoreIncrementHelpText": "{appName}'in bunu bir yükseltme olarak değerlendirmesi için mevcut ve yeni sürümler arasında özel biçim puanında gereken minimum iyileştirme", + "SkipFreeSpaceCheckHelpText": "{appName} kök klasörünüzde boş alan tespit edemediğinde bunu kullansın", + "DayOfWeekAt": "{day}, {time} saatinde", + "Logout": "Çıkış", + "TodayAt": "Bugün {time}'da", + "TomorrowAt": "Yarın {time}'da", + "NoBlocklistItems": "Engellenenler listesi öğesi yok", + "YesterdayAt": "Dün saat {time}'da", + "CustomFormatsSpecificationExceptLanguage": "Dil Dışında", + "CustomFormatsSpecificationExceptLanguageHelpText": "Seçilen dil dışında herhangi bir dil mevcutsa eşleşir", + "LastSearched": "Son Aranan" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index cae7ea474..9b5785b9c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -223,7 +223,7 @@ "EpisodeAirDate": "剧集播出日期", "IndexerSearchNoInteractiveHealthCheckMessage": "没有启用交互式搜索的索引器,{appName}将不提供任何交互式搜索结果", "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {url}", - "About": "关于", + "About": "关于关于", "Actions": "动作", "AppDataDirectory": "AppData 目录", "ApplyTagsHelpTextHowToApplySeries": "如何将标记应用于所选剧集", From 30a52d11aa4749b430c5cbb828bb95066da2260b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 22 Sep 2024 07:21:53 +0300 Subject: [PATCH 560/762] Fixed: Sorting queue by columns Sort allowed keys Co-authored-by: Mark McDowall --- .../Blocklist/BlocklistController.cs | 6 ++--- .../History/HistoryController.cs | 4 +-- .../ImportListExclusionController.cs | 4 +-- src/Sonarr.Api.V3/Queue/QueueController.cs | 26 ++++++++++++++++++- src/Sonarr.Api.V3/Wanted/CutoffController.cs | 4 +-- src/Sonarr.Api.V3/Wanted/MissingController.cs | 4 +-- 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs index d29b1a86e..87fdc9e02 100644 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs @@ -33,10 +33,10 @@ namespace Sonarr.Api.V3.Blocklist var pagingSpec = pagingResource.MapToPagingSpec( new HashSet(StringComparer.OrdinalIgnoreCase) { - "series.sortTitle", - "sourceTitle", "date", - "indexer" + "indexer", + "series.sortTitle", + "sourceTitle" }, "date", SortDirection.Descending); diff --git a/src/Sonarr.Api.V3/History/HistoryController.cs b/src/Sonarr.Api.V3/History/HistoryController.cs index b0810113e..517098f4e 100644 --- a/src/Sonarr.Api.V3/History/HistoryController.cs +++ b/src/Sonarr.Api.V3/History/HistoryController.cs @@ -68,8 +68,8 @@ namespace Sonarr.Api.V3.History var pagingSpec = pagingResource.MapToPagingSpec( new HashSet(StringComparer.OrdinalIgnoreCase) { - "series.sortTitle", - "date" + "date", + "series.sortTitle" }, "date", SortDirection.Descending); diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 30bd035b4..7102fca41 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -51,8 +51,8 @@ namespace Sonarr.Api.V3.ImportLists new HashSet(StringComparer.OrdinalIgnoreCase) { "id", - "tvdbId", - "title" + "title", + "tvdbId" }, "id", SortDirection.Descending); diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 34315de1e..96917878e 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -139,7 +139,31 @@ namespace Sonarr.Api.V3.Queue public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, int? quality = null) { var pagingResource = new PagingResource(paging); - var pagingSpec = pagingResource.MapToPagingSpec(null, "timeleft", SortDirection.Ascending); + var pagingSpec = pagingResource.MapToPagingSpec( + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "added", + "downloadClient", + "episode", + "episode.airDateUtc", + "episode.title", + "episodes.airDateUtc", + "episodes.title", + "estimatedCompletionTime", + "indexer", + "language", + "languages", + "progress", + "protocol", + "quality", + "series.sortTitle", + "size", + "status", + "timeleft", + "title" + }, + "timeleft", + SortDirection.Ascending); return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } diff --git a/src/Sonarr.Api.V3/Wanted/CutoffController.cs b/src/Sonarr.Api.V3/Wanted/CutoffController.cs index 8da4dc597..4df880dc7 100644 --- a/src/Sonarr.Api.V3/Wanted/CutoffController.cs +++ b/src/Sonarr.Api.V3/Wanted/CutoffController.cs @@ -36,9 +36,9 @@ namespace Sonarr.Api.V3.Wanted var pagingSpec = pagingResource.MapToPagingSpec( new HashSet(StringComparer.OrdinalIgnoreCase) { - "series.sortTitle", "episodes.airDateUtc", - "episodes.lastSearchTime" + "episodes.lastSearchTime", + "series.sortTitle" }, "episodes.airDateUtc", SortDirection.Ascending); diff --git a/src/Sonarr.Api.V3/Wanted/MissingController.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs index 2c05025c6..bbfde535f 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs @@ -32,9 +32,9 @@ namespace Sonarr.Api.V3.Wanted var pagingSpec = pagingResource.MapToPagingSpec( new HashSet(StringComparer.OrdinalIgnoreCase) { - "series.sortTitle", "episodes.airDateUtc", - "episodes.lastSearchTime" + "episodes.lastSearchTime", + "series.sortTitle" }, "episodes.airDateUtc", SortDirection.Ascending); From 4d7a3d0909437268b4ad0a0dbeb59d45b4435118 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2024 06:33:03 -0700 Subject: [PATCH 561/762] New: Errors sending Telegram notifications when links aren't available Closes #7240 --- .../Notifications/Telegram/Telegram.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 4d23c3ac9..ba0c2d6dc 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -69,28 +69,29 @@ namespace NzbDrone.Core.Notifications.Telegram { var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; - _proxy.SendNotification(title, healthCheck.Message, null, Settings); + _proxy.SendNotification(title, healthCheck.Message, new List(), Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; - _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", null, Settings); + _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", new List(), Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; - _proxy.SendNotification(title, updateMessage.Message, null, Settings); + _proxy.SendNotification(title, updateMessage.Message, new List(), Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE; + var links = GetLinks(message.Series); - _proxy.SendNotification(title, message.Message, null, Settings); + _proxy.SendNotification(title, message.Message, links, Settings); } public override ValidationResult Test() @@ -106,6 +107,11 @@ namespace NzbDrone.Core.Notifications.Telegram { var links = new List(); + if (series == null) + { + return links; + } + foreach (var link in Settings.MetadataLinks) { var linkType = (MetadataLinkType)link; From dc1524c64f414aaca5c70b09aaa4d0f7fbf5d654 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2024 17:44:30 -0700 Subject: [PATCH 562/762] Fixed: Loading series images after placeholder in Safari --- frontend/src/Series/SeriesImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Series/SeriesImage.tsx b/frontend/src/Series/SeriesImage.tsx index 99a6d961f..89771e182 100644 --- a/frontend/src/Series/SeriesImage.tsx +++ b/frontend/src/Series/SeriesImage.tsx @@ -43,7 +43,7 @@ function SeriesImage({ }: SeriesImageProps) { const [url, setUrl] = useState(null); const [hasError, setHasError] = useState(false); - const [isLoaded, setIsLoaded] = useState(false); + const [isLoaded, setIsLoaded] = useState(true); const image = useRef(null); const handleLoad = useCallback(() => { From 10302323af7f777b4128f832dbf43af9f3f6cfe1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Sep 2024 03:26:04 +0300 Subject: [PATCH 563/762] Fixed: Parsing of Hybrid-Remux as Remux --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 4 ++++ src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index b9f99a146..eb0f4b5ba 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -352,6 +352,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)] [TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)] [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.1080p.DD.2.0.AVC.REMUX-FraMeSToR", false)] + [TestCase("Series Title S01 2018 1080p BluRay Hybrid-REMUX AVC TRUEHD 5.1 Dual Audio-ZR-", false)] + [TestCase("Series.Title.S01.2018.1080p.BluRay.Hybrid-REMUX.AVC.TRUEHD.5.1.Dual.Audio-ZR-", false)] public void should_parse_bluray1080p_remux_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper); @@ -373,6 +375,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)] [TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)] [TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.2160p.DD.2.0.AVC.REMUX-FraMeSToR", false)] + [TestCase("Series Title S01 2018 2160p BluRay Hybrid-REMUX AVC TRUEHD 5.1 Dual Audio-ZR-", false)] + [TestCase("Series.Title.S01.2018.2160p.BluRay.Hybrid-REMUX.AVC.TRUEHD.5.1.Dual.Audio-ZR-", false)] public void should_parse_bluray2160p_remux_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index d922d86ce..5205c8a21 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex HighDefPdtvRegex = new (@"hr[-_. ]ws", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex RemuxRegex = new (@"(?:[_. ]|\d{4}p-)(?(?:(BD|UHD)[-_. ]?)?Remux)\b|(?(?:(BD|UHD)[-_. ]?)?Remux[_. ]\d{4}p)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex RemuxRegex = new (@"(?:[_. ]|\d{4}p-|\bHybrid-)(?(?:(BD|UHD)[-_. ]?)?Remux)\b|(?(?:(BD|UHD)[-_. ]?)?Remux[_. ]\d{4}p)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static QualityModel ParseQuality(string name) { From a7cb264cc8013d9a56aee7d5e41acfd76cde5f96 Mon Sep 17 00:00:00 2001 From: Robin Dadswell <19610103+RobinDadswell@users.noreply.github.com> Date: Sat, 28 Sep 2024 01:26:29 +0100 Subject: [PATCH 564/762] Fixed: Telegram log message including token --- .../InstrumentationTests/CleanseLogMessageFixture.cs | 4 ++++ src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index cd8ed3476..5ef892a84 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -90,6 +90,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"https://discord.com/api/webhooks/mySecret")] [TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")] + // Telegram + [TestCase(@"https://api.telegram.org/bot1234567890:mySecret/sendmessage: chat_id=123456&parse_mode=HTML&text=")] + [TestCase(@"https://api.telegram.org/bot1234567890:mySecret/")] + public void should_clean_message(string message) { var cleansedMessage = CleanseLogMessage.Cleanse(message); diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index c2b496302..95475694e 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -54,7 +54,10 @@ namespace NzbDrone.Common.Instrumentation new (@"api/v[0-9]/notification/sonarr/(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Discord - new (@"discord.com/api/webhooks/((?[\w-]+)/)?(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase) + new (@"discord.com/api/webhooks/((?[\w-]+)/)?(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + + // Telegram + new (@"api.telegram.org/bot(?[\d]+):(?[\w-]+)/", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(? Date: Wed, 25 Sep 2024 04:23:39 +0300 Subject: [PATCH 565/762] Fix translation for Custom Colon Replacement label --- frontend/src/Settings/MediaManagement/Naming/Naming.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index 8d188551f..6fcbdb30e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -266,7 +266,7 @@ class Naming extends Component { { replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ? - {translate('ColonReplacement')} + {translate('CustomColonReplacement')} Date: Wed, 25 Sep 2024 05:08:51 +0300 Subject: [PATCH 566/762] Display naming example errors when all fields are empty --- .../src/Settings/MediaManagement/Naming/NamingConnector.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js index 2b033f918..55c3bc597 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -15,11 +14,11 @@ function createMapStateToProps() { (state) => state.settings.advancedSettings, (state) => state.settings.namingExamples, createSettingsSectionSelector(SECTION), - (advancedSettings, examples, sectionSettings) => { + (advancedSettings, namingExamples, sectionSettings) => { return { advancedSettings, - examples: examples.item, - examplesPopulated: !_.isEmpty(examples.item), + examples: namingExamples.item, + examplesPopulated: namingExamples.isPopulated, ...sectionSettings }; } From 2f0ca423418eefb3058e041016288d517e58960a Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Sep 2024 03:27:17 +0300 Subject: [PATCH 567/762] New: Ignore '.DS_Store' and '.unmanic' files --- .../DiskScanServiceTests/ScanFixture.cs | 24 +++++++++++++++++++ .../MediaFiles/DiskScanService.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index da3bcef4b..dfb2a1881 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -9,6 +9,8 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -457,5 +459,27 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Mocker.GetMock() .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false), Times.Once()); } + + [Test] + public void should_not_scan_excluded_files() + { + GivenSeriesFolder(); + + GivenFiles(new List + { + Path.Combine(_series.Path, ".DS_Store").AsOsAgnostic(), + Path.Combine(_series.Path, ".unmanic").AsOsAgnostic(), + Path.Combine(_series.Path, ".unmanic.part").AsOsAgnostic(), + Path.Combine(_series.Path, "24 The Status Quo Combustion.mkv").AsOsAgnostic() + }); + + Subject.Scan(_series); + + Mocker.GetMock() + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.Is(c => c.Series != null && c.PossibleExtraFiles.Count == 0)), Times.Once()); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index f7d878f16..82237aad9 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.MediaFiles private static readonly Regex ExcludedExtrasSubFolderRegex = new Regex(@"(?:\\|\/|^)(?:extras|extrafanart|behind the scenes|deleted scenes|featurettes|interviews|other|scenes|samples|shorts|trailers)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:@eadir|\.@__thumb|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedExtraFilesRegex = new Regex(@"(-(trailer|other|behindthescenes|deleted|featurette|interview|scene|short)\.[^.]+$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ExcludedFilesRegex = new Regex(@"^\.(_|unmanic|DS_Store$)|^Thumbs\.db$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public void Scan(Series series) { From 4f0e1c54c167f5123a33d19b76653450401adb6d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Sep 2024 16:42:39 -0700 Subject: [PATCH 568/762] Fixed: Don't reject revision upgrades if profile doesn't allow upgrades --- .../UpgradeAllowedSpecificationFixture .cs | 14 ++++++++++++++ .../Specifications/UpgradableSpecification.cs | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture .cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture .cs index c958dc1a6..af7444da6 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture .cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture .cs @@ -206,5 +206,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List()) .Should().BeTrue(); } + + [Test] + public void should_returntrue_when_quality_is_revision_upgrade_for_same_quality() + { + _qualityProfile.UpgradeAllowed = false; + + Subject.IsUpgradeAllowed( + _qualityProfile, + new QualityModel(Quality.DVD, new Revision(1)), + new List { _customFormatOne }, + new QualityModel(Quality.DVD, new Revision(2)), + new List { _customFormatOne }) + .Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index d0eb1603a..916560f8c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -178,6 +178,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var isQualityUpgrade = new QualityModelComparer(qualityProfile).Compare(newQuality, currentQuality) > 0; var isCustomFormatUpgrade = qualityProfile.CalculateCustomFormatScore(newCustomFormats) > qualityProfile.CalculateCustomFormatScore(currentCustomFormats); + if (IsRevisionUpgrade(currentQuality, newQuality)) + { + _logger.Debug("New quality '{0}' is a revision upgrade for '{1}'", newQuality, currentQuality); + return true; + } + if ((isQualityUpgrade || isCustomFormatUpgrade) && qualityProfile.UpgradeAllowed) { _logger.Debug("Quality profile allows upgrading"); From 6d0f10b877912edef21232c64339cc6548d9690e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Sep 2024 16:45:38 -0700 Subject: [PATCH 569/762] Fixed: Ignore extra spaces in path when not running on Windows Closes #7251 --- src/NzbDrone.Common/Extensions/PathExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index c8737d661..be6cd4ee8 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -147,14 +147,14 @@ namespace NzbDrone.Common.Extensions return false; } - if (path.Trim() != path) - { - return false; - } - // Only check for leading or trailing spaces for path when running on Windows. if (OsInfo.IsWindows) { + if (path.Trim() != path) + { + return false; + } + var directoryInfo = new DirectoryInfo(path); while (directoryInfo != null) From da610a1f409c9c03cbed1c27ccaedc32f42e636c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Sep 2024 16:50:42 -0700 Subject: [PATCH 570/762] New: Parse 'BEN THE MAN' release group Closes #7255 --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 62f2abd81..6376b5d27 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -89,6 +89,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] [TestCase("Series.Title.S08E03.720p.WEB.DL.AAC2.0.H.264.KCRT", "KCRT")] + [TestCase("S02E05 2160p WEB-DL DV HDR ENG DDP5.1 Atmos H265 MP4-BEN THE MAN", "BEN THE MAN")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 584457a7e..d219fc884 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -556,7 +556,7 @@ namespace NzbDrone.Core.Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN THE MAN)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); // groups whose releases end with RlsGroup) or RlsGroup] private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?(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); From bc0fc623eee9228296ab40b4a6f651ef29358c7a Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 6 Oct 2024 08:25:27 +0000 Subject: [PATCH 571/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ardenet <1213193613@qq.com> Co-authored-by: Mathias Co-authored-by: Weblate Co-authored-by: angelsky11 Co-authored-by: jsain Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 7 ++- src/NzbDrone.Core/Localization/Core/hr.json | 43 ++++++++++++++++++- .../Localization/Core/zh_CN.json | 34 +++++++-------- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index b811fa1ce..1824ec487 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -38,5 +38,10 @@ "AddDownloadClient": "Tilføj downloadklient", "AddImportListExclusion": "Tilføj ekslusion til importeringslisten", "AddDelayProfileError": "Kan ikke tilføje en ny forsinkelsesprofil. Prøv venligst igen.", - "AddDownloadClientError": "Ikke muligt at tilføje en ny downloadklient. Prøv venligst igen." + "AddDownloadClientError": "Ikke muligt at tilføje en ny downloadklient. Prøv venligst igen.", + "AddListError": "Kan ikke tilføje en ny liste, prøv igen.", + "AddListExclusion": "Tilføj ekskludering af liste", + "AddIndexerImplementation": "Tilføj betingelse - {implementationName}", + "AddImportListExclusionError": "Kunne ikke tilføje en ny listeekskludering. Prøv igen.", + "AddList": "Tilføj Liste" } diff --git a/src/NzbDrone.Core/Localization/Core/hr.json b/src/NzbDrone.Core/Localization/Core/hr.json index 9f795361c..0e9e2c887 100644 --- a/src/NzbDrone.Core/Localization/Core/hr.json +++ b/src/NzbDrone.Core/Localization/Core/hr.json @@ -17,5 +17,46 @@ "System": "Sustav", "Source": "Izvor", "Status": "Status", - "Time": "Vrijeme" + "Time": "Vrijeme", + "AppDataDirectory": "AppData direktorij", + "AddAutoTag": "Dodaj AutoOznaku", + "AddAutoTagError": "Neuspješno dodavanje automatske oznake, molimo pokušaj ponovno.", + "AddCondition": "Dodaj Uvjet", + "AddConnection": "Dodaj vezu", + "AddDownloadClientError": "Nesupješno dodavanje klijenta za preuzimanje, molimo pokušaj ponovno.", + "AddImportList": "Dodaj Listu Za Uvoz", + "AddNotificationError": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno.", + "AddQualityProfileError": "Neuspješno dodavanje novog profila kvalitete, molimo pokušaj ponovno.", + "AnalyseVideoFilesHelpText": "Čitanje video informacija kao što su rezolucija, vrijeme izvođenja i kodek iz datoteka. Ovo zahtjeva da {appName} čita dijelove datoteke što može uzrokovati visoku aktivnost diska ili mreže tijekom skeniranja.", + "AudioLanguages": "Audio Jezici", + "AllTitles": "Svi Naslovi", + "AddRootFolder": "Dodaj Korijensku Mapu", + "AddNewRestriction": "Dodaj novo ograničenje", + "Any": "BIlo koji", + "ApiKeyValidationHealthCheckMessage": "Molimo ažuriraj svoj API ključ da ima barem {length} znakova. Ovo možeš uraditi u postavkama ili konfiguracijskoj datoteci", + "AppUpdated": "{appName} Ažuriran", + "ApplyChanges": "Primjeni Promjene", + "AuthenticationMethod": "Metoda Autentikacije", + "AuthenticationMethodHelpTextWarning": "Molimo odaberi ispravnu metodu autentikacije", + "AuthenticationRequiredPasswordHelpTextWarning": "Unesi novu lozinku", + "AddConditionImplementation": "Dodaj Uvjet - {implementationName}", + "AddConnectionImplementation": "Dodaj Vezu - {implementationName}", + "AddCustomFormatError": "Neuspješno dodavanje novog prilagođenog formata, molimo pokušaj ponovno.", + "AddDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}", + "AddImportListImplementation": "Dodaj Listu Za Uvoz - {implementationName}", + "AddListError": "Neuspješno dodavanje nove liste, molimo pokušaj ponovno.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrdi novu lozinku", + "AddRootFolderError": "Neuspješno dodavanje korijenske mape", + "AutoRedownloadFailed": "Ponovno preuzimanje neuspješno", + "AutoRedownloadFailedFromInteractiveSearch": "Ponovno preuzimanje iz Interaktivne Pretrage neuspješno", + "AddDelayProfileError": "Neuspješno dodavanje profila odgode, molimo pokušaj ponovno.", + "AddIndexerError": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.", + "AddReleaseProfile": "Dodaj profil verzije", + "AddRemotePathMapping": "Dodaj mapiranje mrežne putanje", + "AddRemotePathMappingError": "Neuspješno dodavanje novog mapiranja mrežne putanje, molimo pokušaj ponovno.", + "AuthenticationRequired": "Potrebna Autentikacija", + "AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime", + "AddConditionError": "Neuspješno dodavanje novog uvjeta, molimo pokušaj ponovno.", + "AddIndexerImplementation": "Dodaj Indexer - {implementationName}", + "AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa." } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 9b5785b9c..83aa2078c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -16,7 +16,7 @@ "Language": "语言", "RemoveCompletedDownloads": "移除已完成的下载记录", "RemoveFailedDownloads": "移除失败下载记录", - "ShowAdvanced": "显示高级", + "ShowAdvanced": "高级设置", "ShownClickToHide": "显示,点击隐藏", "DeleteSelectedDownloadClients": "删除下载客户端", "DeleteSelectedImportLists": "删除导入列表", @@ -121,7 +121,7 @@ "MatchedToSeason": "与季匹配", "MatchedToSeries": "与节目匹配", "MetadataSource": "元数据源", - "Missing": "缺失", + "Missing": "缺失中", "NoChange": "无修改", "NoSeasons": "没有季", "PartialSeason": "部分季", @@ -168,7 +168,7 @@ "DeleteSelectedIndexersMessageText": "您确定要删除选定的 {count} 个索引器吗?", "Deleted": "已删除", "Disabled": "已禁用", - "Discord": "分歧", + "Discord": "Discord", "DiskSpace": "硬盘空间", "Docker": "Docker", "DockerUpdater": "更新Docker容器以更新应用", @@ -191,7 +191,7 @@ "Failed": "失败", "Formats": "格式", "General": "通用", - "Genres": "风格", + "Genres": "类型", "Grabbed": "已抓取", "HasMissingSeason": "有缺失的季", "Ignored": "已忽略", @@ -223,7 +223,7 @@ "EpisodeAirDate": "剧集播出日期", "IndexerSearchNoInteractiveHealthCheckMessage": "没有启用交互式搜索的索引器,{appName}将不提供任何交互式搜索结果", "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {url}", - "About": "关于关于", + "About": "关于", "Actions": "动作", "AppDataDirectory": "AppData 目录", "ApplyTagsHelpTextHowToApplySeries": "如何将标记应用于所选剧集", @@ -342,7 +342,7 @@ "ResetQualityDefinitionsMessageText": "您确定要重置质量定义吗?", "ResetTitles": "重置标题", "RestoreBackup": "恢复备份", - "Source": "来源", + "Source": "代码", "Started": "已开始", "StartupDirectory": "启动目录", "Status": "状态", @@ -603,7 +603,7 @@ "CertificateValidationHelpText": "改变HTTPS证书验证的严格程度。不要更改除非您了解风险。", "CopyUsingHardlinksSeriesHelpText": "硬链接 (Hardlinks) 允许 {appName} 将还在做种中的剧集文件(夹)导入而不占用额外的存储空间或者复制文件(夹)的全部内容。硬链接 (Hardlinks) 仅能在源文件和目标文件在同一磁盘卷中使用", "CustomFormat": "自定义格式", - "CustomFormatHelpText": "{appName} 根据与所有自定义格式的匹配程度分数总和为每个资源打分。如果新的资源在相同或更高质量下获得更高分数,{appName} 将会抓取该资源。", + "CustomFormatHelpText": "{appName} 会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数且有相同或更高的质量,则 {appName} 会抓取该发布版本。", "DeleteAutoTagHelpText": "您确认要删除 “{name}” 自动标签吗?", "DownloadClientSeriesTagHelpText": "仅将此下载客户端用于至少具有一个匹配标签的剧集。留空可用于所有剧集。", "Absolute": "绝对", @@ -1138,8 +1138,8 @@ "ShowMonitoredHelpText": "在海报下显示追踪状态", "ShowNetwork": "显示网络", "ShowPreviousAiring": "显示上一次播出", - "ShowQualityProfileHelpText": "在海报下方显示媒体质量配置", - "ShowQualityProfile": "显示质量配置文件", + "ShowQualityProfileHelpText": "在海报下方显示质量配置信息", + "ShowQualityProfile": "显示质量配置", "ShowSearch": "显示搜索", "ShowSearchHelpText": "悬停时显示搜索按钮", "ShowSeasonCount": "显示季数", @@ -1190,7 +1190,7 @@ "UpdateAll": "全部更新", "UpdateAutomaticallyHelpText": "自动下载并安装更新。您还可以在 “系统:更新” 中安装", "UpdateSelected": "更新选择的内容", - "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到质量达标或高于标准", + "UpgradeUntilThisQualityIsMetOrExceeded": "升级资源直至质量达标或高于标准", "UpgradesAllowed": "允许升级", "UpgradesAllowedHelpText": "如关闭,则质量不做升级", "Uppercase": "大写字母", @@ -1224,7 +1224,7 @@ "SetPermissionsLinuxHelpText": "当文件被导入或重命名时要更改文件权限吗?", "SizeLimit": "尺寸限制", "ShowRelativeDates": "显示相对日期", - "ShowRelativeDatesHelpText": "显示相对日期(今天昨天等)或绝对日期", + "ShowRelativeDatesHelpText": "显示相对日期(今天/昨天等)或绝对日期", "TablePageSizeMaximum": "页面大小不得超过 {maximumValue}", "UiSettingsSummary": "日历、日期和色弱模式选项", "TagCannotBeDeletedWhileInUse": "无法删除使用中的标签", @@ -1280,7 +1280,7 @@ "ListRootFolderHelpText": "根目录列表中的项目将被添加", "Logout": "注销", "ManageEpisodes": "管理集", - "MaximumSizeHelpText": "抓取发布资源的最大大小(以 MB 为单位)。设置为零表示无限制", + "MaximumSizeHelpText": "抓取发布资源的最大大小(MB)。设置为零则不限制", "MetadataProvidedBy": "元数据由 {provider} 提供", "MidseasonFinale": "季中完结", "MinimumFreeSpaceHelpText": "如果导入的磁盘空间不足,则禁止导入", @@ -1355,7 +1355,7 @@ "UsenetDelay": "Usenet延时", "UsenetDelayHelpText": "延迟几分钟才能等待从Usenet获取发布", "OrganizeModalHeader": "整理并重命名", - "RssSyncIntervalHelpText": "间隔时间(以分钟为单位),设置为零则禁用(这会停止自动抓取发布资源)", + "RssSyncIntervalHelpText": "间隔时间(分钟),设置为零则禁用(这会停止自动抓取发布资源)", "RssSyncIntervalHelpTextWarning": "这将适用于所有索引器,请遵循他们所制定的规则", "SceneNumberNotVerified": "场景编号未确认", "RssSyncInterval": "RSS同步间隔", @@ -1398,9 +1398,9 @@ "TestAllIndexers": "测试全部索引器", "TestAllLists": "测试全部列表", "SearchSelected": "搜索已选", - "TorrentDelayHelpText": "延迟几分钟等待获取torrent", + "TorrentDelayHelpText": "抓取种子前需等待的延迟时间(分钟)", "Underscore": "下划线", - "SelectAll": "选择全部", + "SelectAll": "全选", "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载客户端", "WantMoreControlAddACustomFormat": "想要更好地控制首选下载吗?添加[自定义格式](/settings/customformats)", "WeekColumnHeader": "日期格式", @@ -1409,7 +1409,7 @@ "SendAnonymousUsageData": "发送匿名使用数据", "UnmonitorSelected": "取消追踪选中项", "UnsavedChanges": "未保存更改", - "UnselectAll": "取消选择全部", + "UnselectAll": "取消全选", "UpcomingSeriesDescription": "剧集已宣布,但尚未确定具体的播出日期", "UpdateFiltered": "更新已过滤的内容", "UpdateMonitoring": "更新监控的内容", @@ -1437,7 +1437,7 @@ "SmartReplace": "智能替换", "ShowBanners": "显示横幅", "ShowBannersHelpText": "显示横幅而不是标题", - "ShowDateAdded": "显示加入时间", + "ShowDateAdded": "显示添加日期", "ShowEpisodeInformation": "显示集信息", "ShowEpisodeInformationHelpText": "显示集号和标题", "ShowPath": "显示路径", From 6660db22ecf53d7747e3abc400529669ea779fa1 Mon Sep 17 00:00:00 2001 From: Jared Ledvina Date: Mon, 7 Oct 2024 18:25:52 -0400 Subject: [PATCH 572/762] Recompare file size after import file if necessary --- .../Disk/DiskTransferService.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index fb7d93f48..2da930a78 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -474,12 +474,7 @@ namespace NzbDrone.Common.Disk try { _diskProvider.CopyFile(sourcePath, targetPath); - - var targetSize = _diskProvider.GetFileSize(targetPath); - if (targetSize != originalSize) - { - throw new IOException(string.Format("File copy incomplete. [{0}] was {1} bytes long instead of {2} bytes.", targetPath, targetSize, originalSize)); - } + VerifyFile(sourcePath, targetPath, originalSize, "copy"); } catch { @@ -493,12 +488,7 @@ namespace NzbDrone.Common.Disk try { _diskProvider.MoveFile(sourcePath, targetPath); - - var targetSize = _diskProvider.GetFileSize(targetPath); - if (targetSize != originalSize) - { - throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize)); - } + VerifyFile(sourcePath, targetPath, originalSize, "move"); } catch (Exception ex) { @@ -511,6 +501,27 @@ namespace NzbDrone.Common.Disk } } + private void VerifyFile(string sourcePath, string targetPath, long originalSize, string action) + { + var targetSize = _diskProvider.GetFileSize(targetPath); + + if (targetSize == originalSize) + { + return; + } + + _logger.Debug("File {0} incomplete, waiting in case filesystem is not synchronized. [{1}] was {2} bytes long instead of the expected {3}.", action, targetPath, targetSize, originalSize); + WaitForIO(); + targetSize = _diskProvider.GetFileSize(targetPath); + + if (targetSize == originalSize) + { + return; + } + + throw new IOException(string.Format("File {0} incomplete, data loss may have occurred. [{1}] was {2} bytes long instead of the expected {3}.", action, targetPath, targetSize, originalSize)); + } + private bool ShouldIgnore(DirectoryInfo folder) { if (folder.Name.StartsWith(".nfs")) From e6e1078c1511f7e6262be3c782981fc6a36f4248 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Sep 2024 08:52:32 +0300 Subject: [PATCH 573/762] Convert Release Profiles to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 8 + .../src/Components/Form/FormInputGroup.js | 4 + .../EditImportListExclusionModal.tsx | 6 +- .../EditImportListExclusionModalContent.tsx | 42 ++-- frontend/src/Settings/Profiles/Profiles.js | 4 +- .../Release/EditReleaseProfileModal.js | 27 --- .../Release/EditReleaseProfileModal.tsx | 41 ++++ .../EditReleaseProfileModalConnector.js | 39 ---- ....js => EditReleaseProfileModalContent.tsx} | 164 +++++++++----- ...EditReleaseProfileModalContentConnector.js | 112 ---------- .../Profiles/Release/ReleaseProfile.js | 206 ------------------ ...leaseProfile.css => ReleaseProfileRow.css} | 0 ...le.css.d.ts => ReleaseProfileRow.css.d.ts} | 0 .../Profiles/Release/ReleaseProfileRow.tsx | 132 +++++++++++ .../Profiles/Release/ReleaseProfiles.css | 2 +- .../Profiles/Release/ReleaseProfiles.js | 102 --------- .../Profiles/Release/ReleaseProfiles.tsx | 81 +++++++ .../Release/ReleaseProfilesConnector.js | 70 ------ .../src/typings/Settings/ReleaseProfile.ts | 12 + src/NzbDrone.Core/Localization/Core/en.json | 2 +- 20 files changed, 412 insertions(+), 642 deletions(-) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js rename frontend/src/Settings/Profiles/Release/{EditReleaseProfileModalContent.js => EditReleaseProfileModalContent.tsx} (51%) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfile.js rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css => ReleaseProfileRow.css} (100%) rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css.d.ts => ReleaseProfileRow.css.d.ts} (100%) create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.js create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js create mode 100644 frontend/src/typings/Settings/ReleaseProfile.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index a3704d10e..cbf9d8de2 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -16,6 +16,7 @@ import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import General from 'typings/Settings/General'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState @@ -49,6 +50,12 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionItemSchemaState {} +export interface ReleaseProfilesAppState + extends AppSectionState, + AppSectionSaveState { + pendingChanges: Partial; +} + export interface CustomFormatAppState extends AppSectionState, AppSectionDeleteState, @@ -83,6 +90,7 @@ interface SettingsAppState { languages: LanguageSettingsAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; + releaseProfiles: ReleaseProfilesAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 7a3191cdc..e3bccaf7c 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -272,6 +272,8 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), + placeholder: PropTypes.string, + delimiters: PropTypes.arrayOf(PropTypes.string), isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), @@ -284,8 +286,10 @@ FormInputGroup.propTypes = { helpTextWarning: PropTypes.string, helpLink: PropTypes.string, autoFocus: PropTypes.bool, + canEdit: PropTypes.bool, includeNoChange: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool, + includeAny: PropTypes.bool, selectedValueOptions: PropTypes.object, indexerFlags: PropTypes.number, pending: PropTypes.bool, diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx index b889a8105..7f5feafab 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -19,7 +19,7 @@ function EditImportListExclusionModal( const dispatch = useDispatch(); - const onModalClosePress = useCallback(() => { + const handleModalClose = useCallback(() => { dispatch( clearPendingChanges({ section: 'settings.importListExclusions', @@ -29,10 +29,10 @@ function EditImportListExclusionModal( }, [dispatch, onModalClose]); return ( - + ); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index 8570d1acf..2fb7da1b7 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -31,12 +31,6 @@ const newImportListExclusion = { tvdbId: 0, }; -interface EditImportListExclusionModalContentProps { - id?: number; - onModalClose: () => void; - onDeleteImportListExclusionPress?: () => void; -} - function createImportListExclusionSelector(id?: number) { return createSelector( (state: AppState) => state.settings.importListExclusions, @@ -62,12 +56,24 @@ function createImportListExclusionSelector(id?: number) { ); } -function EditImportListExclusionModalContent( - props: EditImportListExclusionModalContentProps -) { - const { id, onModalClose, onDeleteImportListExclusionPress } = props; +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModalContent({ + id, + onModalClose, + onDeleteImportListExclusionPress, +}: EditImportListExclusionModalContentProps) { + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(id)); + + const { title, tvdbId } = item; const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); const dispatchSetImportListExclusionValue = (payload: { name: string; @@ -77,20 +83,10 @@ function EditImportListExclusionModalContent( 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], - }); + Object.entries(newImportListExclusion).forEach(([name, value]) => { + dispatchSetImportListExclusionValue({ name, value }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -100,7 +96,7 @@ function EditImportListExclusionModalContent( if (previousIsSaving && !isSaving && !saveError) { onModalClose(); } - }); + }, [previousIsSaving, isSaving, saveError, onModalClose]); const onSavePress = useCallback(() => { dispatch(saveImportListExclusion({ id })); diff --git a/frontend/src/Settings/Profiles/Profiles.js b/frontend/src/Settings/Profiles/Profiles.js index 330591ed6..e54c6fdbd 100644 --- a/frontend/src/Settings/Profiles/Profiles.js +++ b/frontend/src/Settings/Profiles/Profiles.js @@ -7,7 +7,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import DelayProfilesConnector from './Delay/DelayProfilesConnector'; import QualityProfilesConnector from './Quality/QualityProfilesConnector'; -import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector'; +import ReleaseProfiles from './Release/ReleaseProfiles'; // Only a single DragDrop Context can exist so it's done here to allow editing // quality profiles and reordering delay profiles to work. @@ -26,7 +26,7 @@ class Profiles extends Component { - + diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js deleted file mode 100644 index a948ab123..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector'; - -function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditReleaseProfileModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx new file mode 100644 index 000000000..cb7c2cef1 --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; + +interface EditReleaseProfileModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModal({ + isOpen, + onModalClose, + ...otherProps +}: EditReleaseProfileModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.releaseProfiles', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js deleted file mode 100644 index e846ff6ff..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditReleaseProfileModal from './EditReleaseProfileModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditReleaseProfileModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.releaseProfiles' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx similarity index 51% rename from frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js rename to frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 99442839c..930064974 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -1,5 +1,7 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -10,33 +12,97 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import usePrevious from 'Helpers/Hooks/usePrevious'; import { inputTypes, kinds } from 'Helpers/Props'; +import { + saveReleaseProfile, + setReleaseProfileValue, +} from 'Store/Actions/Settings/releaseProfiles'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import translate from 'Utilities/String/translate'; import styles from './EditReleaseProfileModalContent.css'; const tagInputDelimiters = ['Tab', 'Enter']; -function EditReleaseProfileModalContent(props) { - const { - isSaving, - saveError, - item, - onInputChange, - onModalClose, - onSavePress, - onDeleteReleaseProfilePress, - ...otherProps - } = props; +const newReleaseProfile = { + enabled: true, + required: [], + ignored: [], + tags: [], + indexerId: 0, +}; - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId - } = item; +function createReleaseProfileSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.releaseProfiles, + (releaseProfiles) => { + const { items, isFetching, error, isSaving, saveError, pendingChanges } = + releaseProfiles; + + const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +interface EditReleaseProfileModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModalContent({ + id, + onModalClose, + onDeleteReleaseProfilePress, +}: EditReleaseProfileModalContentProps) { + const { item, isFetching, isSaving, error, saveError, ...otherProps } = + useSelector(createReleaseProfileSelector(id)); + + const { name, enabled, required, ignored, tags, indexerId } = item; + + const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); + + useEffect(() => { + if (!id) { + Object.entries(newReleaseProfile).forEach(([name, value]) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue({ name, value })); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }, [previousIsSaving, isSaving, saveError, onModalClose]); + + const handleSavePress = useCallback(() => { + dispatch(saveReleaseProfile({ id })); + }, [dispatch, id]); + + const handleInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue(payload)); + }, + [dispatch] + ); return ( @@ -46,7 +112,6 @@ function EditReleaseProfileModalContent(props) {
- {translate('Name')} @@ -56,7 +121,7 @@ function EditReleaseProfileModalContent(props) { {...name} placeholder={translate('OptionalName')} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -68,7 +133,7 @@ function EditReleaseProfileModalContent(props) { name="enabled" helpText={translate('EnableProfileHelpText')} {...enabled} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -85,7 +150,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -102,7 +167,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -113,10 +178,12 @@ function EditReleaseProfileModalContent(props) { type={inputTypes.INDEXER_SELECT} name="indexerId" helpText={translate('ReleaseProfileIndexerHelpText')} - helpTextWarning={translate('ReleaseProfileIndexerHelpTextWarning')} + helpTextWarning={translate( + 'ReleaseProfileIndexerHelpTextWarning' + )} {...indexerId} includeAny={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -128,33 +195,28 @@ function EditReleaseProfileModalContent(props) { name="tags" helpText={translate('ReleaseProfileTagSeriesHelpText')} {...tags} - onChange={onInputChange} + onChange={handleInputChange} />
- { - id && - - } + {id ? ( + + ) : null} - + {translate('Save')} @@ -163,14 +225,4 @@ function EditReleaseProfileModalContent(props) { ); } -EditReleaseProfileModalContent.propTypes = { - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onDeleteReleaseProfilePress: PropTypes.func -}; - export default EditReleaseProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js deleted file mode 100644 index 0371a1a7a..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; - -const newReleaseProfile = { - enabled: true, - required: [], - ignored: [], - tags: [], - indexerId: 0 -}; - -function createMapStateToProps() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.releaseProfiles, - (id, releaseProfiles) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = releaseProfiles; - - const profile = id ? items.find((i) => i.id === id) : newReleaseProfile; - const settings = selectSettings(profile, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - setReleaseProfileValue, - saveReleaseProfile -}; - -class EditReleaseProfileModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newReleaseProfile).forEach((name) => { - this.props.setReleaseProfileValue({ - name, - value: newReleaseProfile[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setReleaseProfileValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveReleaseProfile({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalContentConnector.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setReleaseProfileValue: PropTypes.func.isRequired, - saveReleaseProfile: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js deleted file mode 100644 index 7ec97bc80..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js +++ /dev/null @@ -1,206 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import MiddleTruncate from 'react-middle-truncate'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import styles from './ReleaseProfile.css'; - -class ReleaseProfile extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onEditReleaseProfilePress = () => { - this.setState({ isEditReleaseProfileModalOpen: true }); - }; - - onEditReleaseProfileModalClose = () => { - this.setState({ isEditReleaseProfileModalOpen: false }); - }; - - onDeleteReleaseProfilePress = () => { - this.setState({ - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: true - }); - }; - - onDeleteReleaseProfileModalClose = () => { - this.setState({ isDeleteReleaseProfileModalOpen: false }); - }; - - onConfirmDeleteReleaseProfile = () => { - this.props.onConfirmDeleteReleaseProfile(this.props.id); - }; - - // - // Render - - render() { - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId, - tagList, - indexerList - } = this.props; - - const { - isEditReleaseProfileModalOpen, - isDeleteReleaseProfileModalOpen - } = this.state; - - const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId); - - return ( - - { - name ? -
- {name} -
: - null - } - -
- { - required.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- -
- { - ignored.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- - - -
- { - !enabled && - - } - - { - indexer && - - } -
- - - - -
- ); - } -} - -ReleaseProfile.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string, - enabled: PropTypes.bool.isRequired, - required: PropTypes.arrayOf(PropTypes.string).isRequired, - ignored: PropTypes.arrayOf(PropTypes.string).isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - indexerId: PropTypes.number.isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -ReleaseProfile.defaultProps = { - enabled: true, - required: [], - ignored: [], - indexerId: 0 -}; - -export default ReleaseProfile; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx new file mode 100644 index 000000000..9ff1eb9aa --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from 'react'; +// @ts-expect-error 'MiddleTruncate' isn't typed +import MiddleTruncate from 'react-middle-truncate'; +import { useDispatch } from 'react-redux'; +import { Tag } from 'App/State/TagsAppState'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { kinds } from 'Helpers/Props'; +import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles'; +import Indexer from 'typings/Indexer'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import styles from './ReleaseProfileRow.css'; + +interface ReleaseProfileProps extends ReleaseProfile { + tagList: Tag[]; + indexerList: Indexer[]; +} + +function ReleaseProfileRow(props: ReleaseProfileProps) { + const { + id, + name, + enabled = true, + required = [], + ignored = [], + tags, + indexerId = 0, + tagList, + indexerList, + } = props; + + const dispatch = useDispatch(); + + const [ + isEditReleaseProfileModalOpen, + setEditReleaseProfileModalOpen, + setEditReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const handleDeletePress = useCallback(() => { + dispatch(deleteReleaseProfile({ id })); + }, [id, dispatch]); + + const indexer = + indexerId !== 0 && indexerList.find((i) => i.id === indexerId); + + return ( + + {name ?
{name}
: null} + +
+ {required.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ +
+ {ignored.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ + + +
+ {enabled ? null : ( + + )} + + {indexer ? ( + + ) : null} +
+ + + + +
+ ); +} + +export default ReleaseProfileRow; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css index 9e9715e77..43f17b9dc 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css @@ -4,7 +4,7 @@ } .addReleaseProfile { - composes: releaseProfile from '~./ReleaseProfile.css'; + composes: releaseProfile from '~./ReleaseProfileRow.css'; background-color: var(--cardAlternateBackgroundColor); color: var(--gray); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js deleted file mode 100644 index 51aa57b73..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js +++ /dev/null @@ -1,102 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import ReleaseProfile from './ReleaseProfile'; -import styles from './ReleaseProfiles.css'; - -class ReleaseProfiles extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onAddReleaseProfilePress = () => { - this.setState({ isAddReleaseProfileModalOpen: true }); - }; - - onAddReleaseProfileModalClose = () => { - this.setState({ isAddReleaseProfileModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - tagList, - indexerList, - onConfirmDeleteReleaseProfile, - ...otherProps - } = this.props; - - return ( -
- -
- -
- -
-
- - { - items.map((item) => { - return ( - - ); - }) - } -
- - -
-
- ); - } -} - -ReleaseProfiles.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx new file mode 100644 index 000000000..98300b1af --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { ReleaseProfilesAppState } from 'App/State/SettingsAppState'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import { fetchIndexers } from 'Store/Actions/Settings/indexers'; +import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import ReleaseProfileRow from './ReleaseProfileRow'; +import styles from './ReleaseProfiles.css'; + +function ReleaseProfiles() { + const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState = + useSelector(createClientSideCollectionSelector('settings.releaseProfiles')); + + const tagList = useSelector(createTagsSelector()); + const indexerList = useSelector( + (state: AppState) => state.settings.indexers.items + ); + + const dispatch = useDispatch(); + + const [ + isAddReleaseProfileModalOpen, + setAddReleaseProfileModalOpen, + setAddReleaseProfileModalClosed, + ] = useModalOpenState(false); + + useEffect(() => { + dispatch(fetchReleaseProfiles()); + dispatch(fetchIndexers()); + }, [dispatch]); + + return ( +
+ +
+ +
+ +
+
+ + {items.map((item) => { + return ( + + ); + })} +
+ + +
+
+ ); +} + +export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js b/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js deleted file mode 100644 index 0c0d81c77..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import ReleaseProfiles from './ReleaseProfiles'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.releaseProfiles, - (state) => state.settings.indexers, - createTagsSelector(), - (releaseProfiles, indexers, tagList) => { - return { - ...releaseProfiles, - tagList, - isIndexersPopulated: indexers.isPopulated, - indexerList: indexers.items - }; - } - ); -} - -const mapDispatchToProps = { - fetchIndexers, - fetchReleaseProfiles, - deleteReleaseProfile -}; - -class ReleaseProfilesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchReleaseProfiles(); - if (!this.props.isIndexersPopulated) { - this.props.fetchIndexers(); - } - } - - // - // Listeners - - onConfirmDeleteReleaseProfile = (id) => { - this.props.deleteReleaseProfile({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ReleaseProfilesConnector.propTypes = { - isIndexersPopulated: PropTypes.bool.isRequired, - fetchReleaseProfiles: PropTypes.func.isRequired, - deleteReleaseProfile: PropTypes.func.isRequired, - fetchIndexers: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector); diff --git a/frontend/src/typings/Settings/ReleaseProfile.ts b/frontend/src/typings/Settings/ReleaseProfile.ts new file mode 100644 index 000000000..847e7d54e --- /dev/null +++ b/frontend/src/typings/Settings/ReleaseProfile.ts @@ -0,0 +1,12 @@ +import ModelBase from 'App/ModelBase'; + +interface ReleaseProfile extends ModelBase { + name: string; + enabled: boolean; + required: string[]; + ignored: string[]; + indexerId: number; + tags: number[]; +} + +export default ReleaseProfile; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 364ffe5c2..abf08cbc3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -361,7 +361,7 @@ "DeleteQualityProfile": "Delete Quality Profile", "DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?", "DeleteReleaseProfile": "Delete Release Profile", - "DeleteReleaseProfileMessageText": "Are you sure you want to delete this release profile '{name}'?", + "DeleteReleaseProfileMessageText": "Are you sure you want to delete the release profile '{name}'?", "DeleteRemotePathMapping": "Delete Remote Path Mapping", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRootFolder": "Delete Root Folder", From 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Sep 2024 10:26:47 +0300 Subject: [PATCH 574/762] Fixed: Copy to clipboard in non-secure contexts --- frontend/src/Components/Link/ClipboardButton.tsx | 11 +++++++++-- package.json | 1 + yarn.lock | 12 ++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx index 09095ae74..dfce115ac 100644 --- a/frontend/src/Components/Link/ClipboardButton.tsx +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -1,3 +1,4 @@ +import copy from 'copy-to-clipboard'; import React, { useCallback, useEffect, useState } from 'react'; import FormInputButton from 'Components/Form/FormInputButton'; import Icon from 'Components/Icon'; @@ -37,10 +38,16 @@ export default function ClipboardButton({ const handleClick = useCallback(async () => { try { - await navigator.clipboard.writeText(value); + if ('clipboard' in navigator) { + await navigator.clipboard.writeText(value); + } else { + copy(value); + } + setState('success'); - } catch (_) { + } catch (e) { setState('error'); + console.error(`Failed to copy to clipboard`, e); } }, [value]); diff --git a/package.json b/package.json index e5e0bbb8f..d3af32a44 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/react-dom": "18.2.25", "classnames": "2.3.2", "connected-react-router": "6.9.3", + "copy-to-clipboard": "3.3.3", "element-class": "0.2.2", "filesize": "10.0.7", "fuse.js": "6.6.2", diff --git a/yarn.lock b/yarn.lock index f46048a70..4a387bed4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2609,6 +2609,13 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" +copy-to-clipboard@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.36.1: version "3.37.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73" @@ -6783,6 +6790,11 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" From c435fcd685cc97e98d14f747227eefd39e4d1164 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 8 Oct 2024 01:27:22 +0300 Subject: [PATCH 575/762] Fixed: Error updating providers with ID missing from JSON --- src/Sonarr.Api.V3/ProviderControllerBase.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index e49e16bdc..04b3c6b73 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -86,9 +86,16 @@ namespace Sonarr.Api.V3 [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult UpdateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) + public ActionResult UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) { - var existingDefinition = _providerFactory.Find(providerResource.Id); + // TODO: Remove fallback to Id from body in next API version bump + var existingDefinition = _providerFactory.Find(id) ?? _providerFactory.Find(providerResource.Id); + + if (existingDefinition == null) + { + return NotFound(); + } + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false); // Compare settings separately because they are not serialized with the definition. @@ -105,7 +112,7 @@ namespace Sonarr.Api.V3 _providerFactory.Update(providerDefinition); } - return Accepted(providerResource.Id); + return Accepted(existingDefinition.Id); } [HttpPut("bulk")] From 620220b2698953bd055b067a08bc1d929a230015 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Sep 2024 17:19:32 +0300 Subject: [PATCH 576/762] Add new category for FL --- .../Indexers/FileList/FileListSettings.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 1c8c7477d..9cff94744 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -75,17 +75,19 @@ namespace NzbDrone.Core.Indexers.FileList public enum FileListCategories { - [FieldOption] + [FieldOption(Label = "Anime")] Anime = 24, - [FieldOption] + [FieldOption(Label = "Animation")] Animation = 15, - [FieldOption] + [FieldOption(Label = "TV 4K")] TV_4K = 27, - [FieldOption] + [FieldOption(Label = "TV HD")] TV_HD = 21, - [FieldOption] + [FieldOption(Label = "TV SD")] TV_SD = 23, - [FieldOption] - Sport = 13 + [FieldOption(Label = "Sport")] + Sport = 13, + [FieldOption(Label = "RO Dubbed")] + RoDubbed = 28 } } From ea0bfed70088c91580b84886bdfa4a91ba84fc82 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 2 Oct 2024 18:20:04 +0300 Subject: [PATCH 577/762] Fixed: Validate path on series update --- src/Sonarr.Api.V3/Series/SeriesController.cs | 42 ++++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 48d377e40..72bf0ee41 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -72,26 +72,34 @@ namespace Sonarr.Api.V3.Series _commandQueueManager = commandQueueManager; _rootFolderService = rootFolderService; - Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); - - SharedValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.Stop) + SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(seriesPathValidator) - .SetValidator(seriesAncestorValidator) - .SetValidator(systemFolderValidator) - .When(s => !s.Path.IsNullOrWhiteSpace()); + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(seriesPathValidator) + .SetValidator(seriesAncestorValidator) + .SetValidator(systemFolderValidator) + .When(s => s.Path.IsNotNullOrWhiteSpace()); - SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator); + PostValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .SetValidator(rootFolderExistsValidator) + .SetValidator(seriesFolderAsRootFolderValidator) + .When(s => s.Path.IsNullOrWhiteSpace()); + + PutValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath(); + + SharedValidator.RuleFor(s => s.QualityProfileId).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(qualityProfileExistsValidator); - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath) - .IsValidPath() - .SetValidator(rootFolderExistsValidator) - .SetValidator(seriesFolderAsRootFolderValidator) - .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); } From a6735e7a3fc9f45ece7af8dcc68e9fa65da8f319 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 3 Oct 2024 15:04:00 +0300 Subject: [PATCH 578/762] Fixed: Manual importing to nested series folders --- .../MoveEpisodeFileFixture.cs | 6 ++++++ .../MediaFiles/EpisodeFileMovingService.cs | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs index f8575915d..41ed18709 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -51,6 +52,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests .Returns(@"C:\Test\TV\Series\Season 01".AsOsAgnostic()); var rootFolder = @"C:\Test\TV\".AsOsAgnostic(); + + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(It.IsAny())) + .Returns(rootFolder); + Mocker.GetMock() .Setup(s => s.FolderExists(rootFolder)) .Returns(true); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 2518df234..6bf474d94 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles @@ -32,6 +33,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IDiskProvider _diskProvider; private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IImportScript _scriptImportDecider; + private readonly IRootFolderService _rootFolderService; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; @@ -43,6 +45,7 @@ namespace NzbDrone.Core.MediaFiles IDiskProvider diskProvider, IMediaFileAttributeService mediaFileAttributeService, IImportScript scriptImportDecider, + IRootFolderService rootFolderService, IEventAggregator eventAggregator, IConfigService configService, Logger logger) @@ -54,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles _diskProvider = diskProvider; _mediaFileAttributeService = mediaFileAttributeService; _scriptImportDecider = scriptImportDecider; + _rootFolderService = rootFolderService; _eventAggregator = eventAggregator; _configService = configService; _logger = logger; @@ -180,11 +184,16 @@ namespace NzbDrone.Core.MediaFiles var episodeFolder = Path.GetDirectoryName(filePath); var seasonFolder = _buildFileNames.BuildSeasonPath(series, seasonNumber); var seriesFolder = series.Path; - var rootFolder = new OsPath(seriesFolder).Directory.FullPath; + var rootFolder = _rootFolderService.GetBestRootFolderPath(seriesFolder); + + if (rootFolder.IsNullOrWhiteSpace()) + { + throw new RootFolderNotFoundException($"Root folder was not found, '{seriesFolder}' is not a subdirectory of a defined root folder."); + } if (!_diskProvider.FolderExists(rootFolder)) { - throw new RootFolderNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + throw new RootFolderNotFoundException($"Root folder '{rootFolder}' was not found."); } var changed = false; From a00121695715feb2cf8f04da246dc18262ab3237 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 8 Oct 2024 01:29:30 +0300 Subject: [PATCH 579/762] Fixed: Cleaning paths for top level root folders --- .../PathExtensionFixture.cs | 21 +++++++++++++++++++ src/NzbDrone.Common/Disk/OsPath.cs | 14 +++++++++++-- .../Extensions/PathExtensions.cs | 8 ++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 2daf8b7bd..f0c5c3f98 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -393,5 +393,26 @@ namespace NzbDrone.Common.Test PosixOnly(); path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); } + + [TestCase(@"C:\", @"C:\")] + [TestCase(@"C:\\", @"C:\")] + [TestCase(@"C:\Test", @"C:\Test")] + [TestCase(@"C:\Test\", @"C:\Test")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\", @"\\server\share")] + public void windows_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } + + [TestCase("/", "/")] + [TestCase("//", "/")] + [TestCase("/test", "/test")] + [TestCase("/test/", "/test")] + [TestCase("/test//", "/test")] + public void unix_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } } } diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index 45e520761..42fdaf567 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -104,9 +104,19 @@ namespace NzbDrone.Common.Disk switch (kind) { case OsPathKind.Windows when !path.EndsWith(":\\"): - return path.TrimEnd('\\'); + while (!path.EndsWith(":\\") && path.EndsWith('\\')) + { + path = path[..^1]; + } + + return path; case OsPathKind.Unix when path != "/": - return path.TrimEnd('/'); + while (path != "/" && path.EndsWith('/')) + { + path = path[..^1]; + } + + return path; } return path; diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index be6cd4ee8..1dd7952d3 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -25,8 +25,6 @@ namespace NzbDrone.Common.Extensions private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Sonarr.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; - private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(? Date: Fri, 4 Oct 2024 19:50:49 +0300 Subject: [PATCH 580/762] Use the first allowed quality for cutoff met rejection message with disabled upgrades --- .../Specifications/UpgradeDiskSpecification.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 86d9edcb2..c7afa3351 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -36,8 +36,6 @@ namespace NzbDrone.Core.DecisionEngine.Specifications continue; } - var customFormats = _formatService.ParseCustomFormat(file); - _logger.Debug("Comparing file quality with report. Existing file is {0}.", file.Quality); if (!_upgradableSpecification.CutoffNotMet(qualityProfile, @@ -47,12 +45,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Debug("Cutoff already met, rejecting."); - var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); - var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; + var cutoff = qualityProfile.UpgradeAllowed ? qualityProfile.Cutoff : qualityProfile.FirststAllowedQuality().Id; + var qualityCutoff = qualityProfile.Items[qualityProfile.GetIndex(cutoff).Index]; return Decision.Reject("Existing file meets cutoff: {0}", qualityCutoff); } + var customFormats = _formatService.ParseCustomFormat(file); + var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile, file.Quality, customFormats, From 354ed9657259d8cb7fa914a4a9e26368d97766a7 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 4 Oct 2024 20:07:22 -0700 Subject: [PATCH 581/762] Fixed: Ignore free space check before grabbing if directory is missing Closes #7273 --- .../FreeSpaceSpecificationFixture.cs | 12 ++++++++++++ .../Specifications/FreeSpaceSpecification.cs | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs index b691c7544..957a323f3 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs @@ -1,3 +1,4 @@ +using System.IO; using FluentAssertions; using Moq; using NUnit.Framework; @@ -90,5 +91,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); } + + [Test] + public void should_return_true_if_root_folder_is_not_available() + { + WithMinimumFreeSpace(150); + WithSize(100); + + Mocker.GetMock().Setup(s => s.GetAvailableSpace(It.IsAny())).Throws(); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs index c01c94601..2d3d3b082 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs @@ -1,3 +1,4 @@ +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -32,11 +33,21 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } var size = subject.Release.Size; - var freeSpace = _diskProvider.GetAvailableSpace(subject.Series.Path); + var path = subject.Series.Path; + long? freeSpace = null; + + try + { + freeSpace = _diskProvider.GetAvailableSpace(path); + } + catch (DirectoryNotFoundException) + { + // Ignore so it'll be skipped in the following checks + } if (!freeSpace.HasValue) { - _logger.Debug("Unable to get available space for {0}. Skipping", subject.Series.Path); + _logger.Debug("Unable to get available space for {0}. Skipping", path); return Decision.Accept(); } From 39074b0b1d040969f86d787c2346d5ed5a9f72dc Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 4 Oct 2024 19:19:12 -0700 Subject: [PATCH 582/762] New: Use 307 redirect for requests missing URL Base Closes #7262 --- src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs b/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs index 9f85731d2..3b17c10d2 100644 --- a/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs +++ b/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs @@ -20,6 +20,8 @@ namespace Sonarr.Http.Middleware if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace()) { context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}"); + context.Response.StatusCode = 307; + return; } From ebfa0003753fc2a7e5bb6ddaeafc6aab46ef2876 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 7 Oct 2024 22:26:19 +0000 Subject: [PATCH 583/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: fordas Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 6f80ec6a6..198e607b5 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -455,7 +455,7 @@ "LogFilesLocation": "Los archivos de registro se encuentran en: {location}", "DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido", "InfoUrl": "Información de la URL", - "HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación.", + "HealthMessagesInfoBox": "Puedes encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte en los enlaces que aparecen a continuación.", "ManualGrab": "Captura manual", "FullColorEvents": "Eventos a todo color", "FullColorEventsHelpText": "Estilo alterado para colorear todo el evento con el color de estado, en lugar de sólo el borde izquierdo. No se aplica a la Agenda", @@ -1064,7 +1064,7 @@ "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexadores con capacidad de RSS no están disponibles temporalmente debido a errores recientes con el indexador", "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hay indexadores disponibles con la sincronización RSS activada, {appName} no capturará nuevos estrenos automáticamente", "IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas", - "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexador", "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", From 32fa63d24d08d8d8877386a8d2e7065ab5d0ad39 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 4 Oct 2024 12:59:41 +0300 Subject: [PATCH 584/762] Convert FormInputButton to TypeScript --- .../src/Components/Form/FormInputButton.js | 56 ------------------- .../src/Components/Form/FormInputButton.tsx | 38 +++++++++++++ 2 files changed, 38 insertions(+), 56 deletions(-) delete mode 100644 frontend/src/Components/Form/FormInputButton.js create mode 100644 frontend/src/Components/Form/FormInputButton.tsx diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js deleted file mode 100644 index 2bacc3779..000000000 --- a/frontend/src/Components/Form/FormInputButton.js +++ /dev/null @@ -1,56 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import { kinds } from 'Helpers/Props'; -import styles from './FormInputButton.css'; - -function FormInputButton(props) { - const { - className, - canSpin, - isLastButton, - ...otherProps - } = props; - - if (canSpin) { - return ( - - ); - } - - return ( - -
-
-
- ); - } -} - -NamingModal.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isOpen: PropTypes.bool.isRequired, - advancedSettings: PropTypes.bool.isRequired, - season: PropTypes.bool.isRequired, - episode: PropTypes.bool.isRequired, - daily: PropTypes.bool.isRequired, - anime: PropTypes.bool.isRequired, - additional: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -NamingModal.defaultProps = { - season: false, - episode: false, - daily: false, - anime: false, - additional: false -}; - -export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx new file mode 100644 index 000000000..aec03a87c --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx @@ -0,0 +1,591 @@ +import React, { useCallback, useState } from 'react'; +import FieldSet from 'Components/FieldSet'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { icons, sizes } from 'Helpers/Props'; +import NamingConfig from 'typings/Settings/NamingConfig'; +import translate from 'Utilities/String/translate'; +import NamingOption from './NamingOption'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingModal.css'; + +const separatorOptions: { key: TokenSeparator; value: string }[] = [ + { + key: ' ', + get value() { + return `${translate('Space')} ( )`; + }, + }, + { + key: '.', + get value() { + return `${translate('Period')} (.)`; + }, + }, + { + key: '_', + get value() { + return `${translate('Underscore')} (_)`; + }, + }, + { + key: '-', + get value() { + return `${translate('Dash')} (-)`; + }, + }, +]; + +const caseOptions: { key: TokenCase; value: string }[] = [ + { + key: 'title', + get value() { + return translate('DefaultCase'); + }, + }, + { + key: 'lower', + get value() { + return translate('Lowercase'); + }, + }, + { + key: 'upper', + get value() { + return translate('Uppercase'); + }, + }, +]; + +const fileNameTokens = [ + { + token: + '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}', + example: + "The Series Title's! (2010) - S01E01 - Episode Title HDTV-720p Proper", + }, + { + token: + '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}', + example: + "The Series Title's! (2010) - 1x01 - Episode Title HDTV-720p Proper", + }, + { + token: + '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}', + example: "The.Series.Title's!.(2010).S01E01.Episode.Title.HDTV-720p", + }, +]; + +const seriesTokens = [ + { token: '{Series Title}', example: "The Series Title's!", footNote: true }, + { + token: '{Series CleanTitle}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series TitleYear}', + example: "The Series Title's! (2010)", + footNote: true, + }, + { + token: '{Series CleanTitleYear}', + example: "The Series Title's! 2010", + footNote: true, + }, + { + token: '{Series TitleWithoutYear}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series CleanTitleWithoutYear}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series TitleThe}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series CleanTitleThe}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series TitleTheYear}', + example: "Series Title's!, The (2010)", + footNote: true, + }, + { + token: '{Series CleanTitleTheYear}', + example: "Series Title's!, The 2010", + footNote: true, + }, + { + token: '{Series TitleTheWithoutYear}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series CleanTitleTheWithoutYear}', + example: "Series Title's!, The", + footNote: true, + }, + { token: '{Series TitleFirstCharacter}', example: 'S', footNote: true }, + { token: '{Series Year}', example: '2010' }, +]; + +const seriesIdTokens = [ + { token: '{ImdbId}', example: 'tt12345' }, + { token: '{TvdbId}', example: '12345' }, + { token: '{TmdbId}', example: '11223' }, + { token: '{TvMazeId}', example: '54321' }, +]; + +const seasonTokens = [ + { token: '{season:0}', example: '1' }, + { token: '{season:00}', example: '01' }, +]; + +const episodeTokens = [ + { token: '{episode:0}', example: '1' }, + { token: '{episode:00}', example: '01' }, +]; + +const airDateTokens = [ + { token: '{Air-Date}', example: '2016-03-20' }, + { token: '{Air Date}', example: '2016 03 20' }, +]; + +const absoluteTokens = [ + { token: '{absolute:0}', example: '1' }, + { token: '{absolute:00}', example: '01' }, + { token: '{absolute:000}', example: '001' }, +]; + +const episodeTitleTokens = [ + { token: '{Episode Title}', example: "Episode's Title", footNote: true }, + { token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: true }, +]; + +const qualityTokens = [ + { token: '{Quality Full}', example: 'WEBDL-1080p Proper' }, + { token: '{Quality Title}', example: 'WEBDL-1080p' }, +]; + +const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true }, + + { token: '{MediaInfo AudioCodec}', example: 'DTS' }, + { token: '{MediaInfo AudioChannels}', example: '5.1' }, + { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true }, + { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true }, + + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo VideoBitDepth}', example: '10' }, + { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, + { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' }, +]; + +const otherTokens = [ + { token: '{Release Group}', example: 'Rls Grp', footNote: true }, + { token: '{Custom Formats}', example: 'iNTERNAL' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' }, +]; + +const otherAnimeTokens = [{ token: '{Release Hash}', example: 'ABCDEFGH' }]; + +const originalTokens = [ + { + token: '{Original Title}', + example: "The.Series.Title's!.S01E01.WEBDL.1080p.x264-EVOLVE", + }, + { + token: '{Original Filename}', + example: "the.series.title's!.s01e01.webdl.1080p.x264-EVOLVE", + }, +]; + +interface NamingModalProps { + isOpen: boolean; + name: keyof Pick< + NamingConfig, + | 'standardEpisodeFormat' + | 'dailyEpisodeFormat' + | 'animeEpisodeFormat' + | 'seriesFolderFormat' + | 'seasonFolderFormat' + | 'specialsFolderFormat' + >; + value: string; + advancedSettings: boolean; + season?: boolean; + episode?: boolean; + daily?: boolean; + anime?: boolean; + additional?: boolean; + onInputChange: ({ name, value }: { name: string; value: string }) => void; + onModalClose: () => void; +} + +function NamingModal(props: NamingModalProps) { + const { + isOpen, + name, + value, + advancedSettings, + season = false, + episode = false, + anime = false, + additional = false, + onInputChange, + onModalClose, + } = props; + + const [tokenSeparator, setTokenSeparator] = useState(' '); + const [tokenCase, setTokenCase] = useState('title'); + const [selectionStart, setSelectionStart] = useState(null); + const [selectionEnd, setSelectionEnd] = useState(null); + + const handleTokenSeparatorChange = useCallback( + ({ value }: { value: TokenSeparator }) => { + setTokenSeparator(value); + }, + [setTokenSeparator] + ); + + const handleTokenCaseChange = useCallback( + ({ value }: { value: TokenCase }) => { + setTokenCase(value); + }, + [setTokenCase] + ); + + const handleInputSelectionChange = useCallback( + (selectionStart: number, selectionEnd: number) => { + setSelectionStart(selectionStart); + setSelectionEnd(selectionEnd); + }, + [setSelectionStart, setSelectionEnd] + ); + + const handleOptionPress = useCallback( + ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => { + if (isFullFilename) { + onInputChange({ name, value: tokenValue }); + } else if (selectionStart == null || selectionEnd == null) { + onInputChange({ + name, + value: `${value}${tokenValue}`, + }); + } else { + const start = value.substring(0, selectionStart); + const end = value.substring(selectionEnd); + const newValue = `${start}${tokenValue}${end}`; + + onInputChange({ name, value: newValue }); + + setSelectionStart(newValue.length - 1); + setSelectionEnd(newValue.length - 1); + } + }, + [name, value, selectionEnd, selectionStart, onInputChange] + ); + + return ( + + + + {episode + ? translate('FileNameTokens') + : translate('FolderNameTokens')} + + + +
+ + + +
+ + {advancedSettings ? null : ( +
+
+ {fileNameTokens.map(({ token, example }) => ( + + ))} +
+
+ )} + +
+
+ {seriesTokens.map(({ token, example, footNote }) => ( + + ))} +
+ +
+ + +
+
+ +
+
+ {seriesIdTokens.map(({ token, example }) => ( + + ))} +
+
+ + {season ? ( +
+
+ {seasonTokens.map(({ token, example }) => ( + + ))} +
+
+ ) : null} + + {episode ? ( +
+
+
+ {episodeTokens.map(({ token, example }) => ( + + ))} +
+
+ +
+
+ {airDateTokens.map(({ token, example }) => ( + + ))} +
+
+ + {anime ? ( +
+
+ {absoluteTokens.map(({ token, example }) => ( + + ))} +
+
+ ) : null} +
+ ) : null} + + {additional ? ( +
+
+
+ {episodeTitleTokens.map(({ token, example, footNote }) => ( + + ))} +
+
+ + +
+
+ +
+
+ {qualityTokens.map(({ token, example }) => ( + + ))} +
+
+ +
+
+ {mediaInfoTokens.map(({ token, example, footNote }) => ( + + ))} +
+ +
+ + +
+
+ +
+
+ {otherTokens.map(({ token, example, footNote }) => ( + + ))} + + {anime + ? otherAnimeTokens.map(({ token, example }) => ( + + )) + : null} +
+ +
+ + +
+
+ +
+
+ {originalTokens.map(({ token, example }) => ( + + ))} +
+
+
+ ) : null} +
+ + + + + + +
+
+ ); +} + +export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 204c93d0e..1fb8a05eb 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -45,6 +45,10 @@ } } +.title { + text-transform: none; +} + .lower { text-transform: lowercase; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts index a060f6218..5c50bfab2 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'lower': string; 'option': string; 'small': string; + 'title': string; 'token': string; 'upper': string; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js deleted file mode 100644 index 6373c11e3..000000000 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js +++ /dev/null @@ -1,93 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons, sizes } from 'Helpers/Props'; -import styles from './NamingOption.css'; - -class NamingOption extends Component { - - // - // Listeners - - onPress = () => { - const { - token, - tokenSeparator, - tokenCase, - isFullFilename, - onPress - } = this.props; - - let tokenValue = token; - - tokenValue = tokenValue.replace(/ /g, tokenSeparator); - - if (tokenCase === 'lower') { - tokenValue = token.toLowerCase(); - } else if (tokenCase === 'upper') { - tokenValue = token.toUpperCase(); - } - - onPress({ isFullFilename, tokenValue }); - }; - - // - // Render - render() { - const { - token, - tokenSeparator, - example, - footNote, - tokenCase, - isFullFilename, - size - } = this.props; - - return ( - -
- {token.replace(/ /g, tokenSeparator)} -
- -
- {example.replace(/ /g, tokenSeparator)} - - { - footNote !== 0 && - - } -
- - ); - } -} - -NamingOption.propTypes = { - token: PropTypes.string.isRequired, - example: PropTypes.string.isRequired, - footNote: PropTypes.number.isRequired, - tokenSeparator: PropTypes.string.isRequired, - tokenCase: PropTypes.string.isRequired, - isFullFilename: PropTypes.bool.isRequired, - size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]), - onPress: PropTypes.func.isRequired -}; - -NamingOption.defaultProps = { - footNote: 0, - size: sizes.SMALL, - isFullFilename: false -}; - -export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx new file mode 100644 index 000000000..e9bcf11ff --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import React, { useCallback } from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import { Size } from 'Helpers/Props/sizes'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingOption.css'; + +interface NamingOptionProps { + token: string; + tokenSeparator: TokenSeparator; + example: string; + tokenCase: TokenCase; + isFullFilename?: boolean; + footNote?: boolean; + size?: Extract; + onPress: ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => void; +} + +function NamingOption(props: NamingOptionProps) { + const { + token, + tokenSeparator, + example, + tokenCase, + isFullFilename = false, + footNote = false, + size = 'small', + onPress, + } = props; + + const handlePress = useCallback(() => { + let tokenValue = token; + + tokenValue = tokenValue.replace(/ /g, tokenSeparator); + + if (tokenCase === 'lower') { + tokenValue = token.toLowerCase(); + } else if (tokenCase === 'upper') { + tokenValue = token.toUpperCase(); + } + + onPress({ isFullFilename, tokenValue }); + }, [token, tokenCase, tokenSeparator, isFullFilename, onPress]); + + return ( + +
{token.replace(/ /g, tokenSeparator)}
+ +
+ {example.replace(/ /g, tokenSeparator)} + + {footNote ? ( + + ) : null} +
+ + ); +} + +export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts new file mode 100644 index 000000000..280ef307d --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts @@ -0,0 +1,3 @@ +type TokenCase = 'title' | 'lower' | 'upper'; + +export default TokenCase; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts new file mode 100644 index 000000000..5ef86a6a1 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts @@ -0,0 +1,3 @@ +type TokenSeparator = ' ' | '.' | '_' | '-'; + +export default TokenSeparator; diff --git a/frontend/src/typings/Settings/NamingConfig.ts b/frontend/src/typings/Settings/NamingConfig.ts new file mode 100644 index 000000000..e013b89cc --- /dev/null +++ b/frontend/src/typings/Settings/NamingConfig.ts @@ -0,0 +1,13 @@ +export default interface NamingConfig { + renameEpisodes: boolean; + replaceIllegalCharacters: boolean; + colonReplacementFormat: number; + customColonReplacementFormat: string; + multiEpisodeStyle: number; + standardEpisodeFormat: string; + dailyEpisodeFormat: string; + animeEpisodeFormat: string; + seriesFolderFormat: string; + seasonFolderFormat: string; + specialsFolderFormat: string; +} diff --git a/frontend/src/typings/Settings/NamingExample.ts b/frontend/src/typings/Settings/NamingExample.ts new file mode 100644 index 000000000..52ffc4d27 --- /dev/null +++ b/frontend/src/typings/Settings/NamingExample.ts @@ -0,0 +1,10 @@ +export default interface NamingExample { + singleEpisodeExample: string; + multiEpisodeExample: string; + dailyEpisodeExample: string; + animeEpisodeExample: string; + animeMultiEpisodeExample: string; + seriesFolderExample: string; + seasonFolderExample: string; + specialsFolderExample: string; +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index abf08cbc3..3ade2c46b 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -727,6 +727,7 @@ "FirstDayOfWeek": "First Day of Week", "Fixed": "Fixed", "Folder": "Folder", + "FolderNameTokens": "Folder Name Tokens", "Folders": "Folders", "Forecast": "Forecast", "FormatAgeDay": "day", From 2f1793d87ae4b473e0ecb8d94125154bd3492477 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Sep 2024 16:25:59 +0300 Subject: [PATCH 586/762] Filename examples specific for daily and anime naming --- .../MediaManagement/Naming/Naming.tsx | 1 - .../MediaManagement/Naming/NamingModal.tsx | 85 ++++++++++++++++--- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.tsx b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx index 08dc86910..10fb42518 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.tsx +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx @@ -509,7 +509,6 @@ function Naming() { {namingModalOptions ? ( ; value: string; - advancedSettings: boolean; season?: boolean; episode?: boolean; daily?: boolean; @@ -246,9 +281,9 @@ function NamingModal(props: NamingModalProps) { isOpen, name, value, - advancedSettings, season = false, episode = false, + daily = false, anime = false, additional = false, onInputChange, @@ -339,9 +374,39 @@ function NamingModal(props: NamingModalProps) { />
- {advancedSettings ? null : ( + {episode ? (
+ {daily + ? fileNameDailyTokens.map(({ token, example }) => ( + + )) + : null} + + {anime + ? fileNameAnimeTokens.map(({ token, example }) => ( + + )) + : null} + {fileNameTokens.map(({ token, example }) => (
- )} + ) : null}
From 86446a768627b98909389eac230720fcd00cb127 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Mon, 7 Oct 2024 22:32:50 +0000 Subject: [PATCH 587/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 85 ++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 1f835fbe7..be98294a0 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -1559,6 +1559,15 @@ "DownloadClient" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -1566,14 +1575,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -2828,6 +2829,15 @@ "ImportList" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -2835,14 +2845,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -3459,6 +3461,15 @@ "Indexer" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -3466,14 +3477,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -4499,6 +4502,15 @@ "Metadata" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -4506,14 +4518,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -5078,6 +5082,15 @@ "Notification" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -5085,14 +5098,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { From 28599f87af9ca2e05a5697254e1080adc458dc86 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 24 Oct 2024 11:25:27 +0000 Subject: [PATCH 588/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: DNArjen Co-authored-by: Fixer Co-authored-by: Havok Dan Co-authored-by: JoseFilipeFerreira Co-authored-by: Kuzmich Co-authored-by: Lars Co-authored-by: Weblate Co-authored-by: anne Co-authored-by: fordas Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 5 ++-- src/NzbDrone.Core/Localization/Core/fr.json | 6 +++-- src/NzbDrone.Core/Localization/Core/nl.json | 3 ++- src/NzbDrone.Core/Localization/Core/pt.json | 11 +++++++-- .../Localization/Core/pt_BR.json | 9 +++++-- src/NzbDrone.Core/Localization/Core/ro.json | 6 ++++- src/NzbDrone.Core/Localization/Core/ru.json | 24 +++++++++---------- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 198e607b5..5b26e7e54 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -495,7 +495,7 @@ "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", "DeleteImportListExclusionMessageText": "¿Está seguro de que desea eliminar esta exclusión de la lista de importación?", "DeleteQualityProfile": "Borrar perfil de calidad", - "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar este perfil de lanzamiento '{name}'?", + "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar el perfil de lanzamiento '{name}'?", "DeleteRemotePathMapping": "Borrar mapeo de ruta remota", "DeleteSelectedEpisodeFiles": "Borrar los archivos de episodios seleccionados", "DeleteSelectedEpisodeFilesHelpText": "Esta seguro que desea borrar los archivos de episodios seleccionados?", @@ -2119,5 +2119,6 @@ "NotificationsGotifySettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones", "NotificationsGotifySettingsPreferredMetadataLink": "Enlace de metadatos preferido", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Enlace de metadatos para clientes que solo soportan un único enlace", - "SkipFreeSpaceCheckHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz" + "SkipFreeSpaceCheckHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz", + "FolderNameTokens": "Tokens de nombre de carpeta" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 8da77fcfd..0e8a86efd 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1873,7 +1873,7 @@ "AutoTaggingSpecificationStatus": "État", "CustomFormatsSpecificationLanguage": "Langue", "CustomFormatsSpecificationMaximumSize": "Taille maximum", - "CustomFormatsSpecificationMinimumSize": "Taille maximum", + "CustomFormatsSpecificationMinimumSize": "Taille minimum", "CustomFormatsSpecificationRegularExpression": "Expression régulière", "CustomFormatsSpecificationReleaseGroup": "Groupe de versions", "CustomFormatsSpecificationResolution": "Résolution", @@ -2103,5 +2103,7 @@ "LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo.", "DeleteSelected": "Supprimer la sélection", "LogSizeLimit": "Limite de taille du journal", - "DeleteSelectedImportListExclusionsMessageText": "Êtes-vous sûr de vouloir supprimer les exclusions de la liste d'importation sélectionnée ?" + "DeleteSelectedImportListExclusionsMessageText": "Êtes-vous sûr de vouloir supprimer les exclusions de la liste d'importation sélectionnée ?", + "CustomFormatsSpecificationExceptLanguage": "Excepté Langue", + "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponf si l'autre langue que celle sélectionné est présente" } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 1da86873e..ded6f3d54 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -232,5 +232,6 @@ "CustomFormatJson": "Aangepast formaat JSON", "CustomFormatUnknownCondition": "Onbekende aangepaste formaatvoorwaarde '{implementation}'.", "ClickToChangeIndexerFlags": "Klik om indexeringsvlaggen te wijzigen", - "CustomFormatsSettingsTriggerInfo": "Een Aangepast Formaat wordt toegepast op een uitgave of bestand als het overeenkomt met ten minste één van de verschillende condities die zijn gekozen." + "CustomFormatsSettingsTriggerInfo": "Een Aangepast Formaat wordt toegepast op een uitgave of bestand als het overeenkomt met ten minste één van de verschillende condities die zijn gekozen.", + "CountVotes": "{votes} Stemmen" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index bf795d86d..4cf02ea11 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -184,7 +184,7 @@ "BackupFolderHelpText": "Caminhos relativos estarão no diretório AppData do {appName}", "Automatic": "Automático", "AutomaticSearch": "Pesquisa automática", - "AutoTaggingRequiredHelpText": "Esta condição de {implementationName} deve corresponder para que a regra de marcação automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} é suficiente.", + "AutoTaggingRequiredHelpText": "Esta condição {implementationName} tem de corresponder para que a regra de marcação automática seja aplicada. Caso contrário, uma única correspondência {implementationName} é suficiente.", "Backup": "Backup", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmar nova senha", "Conditions": "Condições", @@ -209,5 +209,12 @@ "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", "ChownGroup": "Fazer chown em grupo", "Clone": "Clonar", - "ContinuingOnly": "Continuando apenas" + "ContinuingOnly": "Continuando apenas", + "CountCustomFormatsSelected": "{count} formatos selecionados", + "CountVotes": "{votes} votos", + "CustomFormatUnknownCondition": "Condição de formato personalizado \"{implementation}\" desconhecida", + "BlocklistReleaseHelpText": "Impede o {appName} de capturar automaticamente estes ficheiros novamente", + "BlackholeFolderHelpText": "Pasta em que {appName} guardará o ficheiro {extension}.", + "BlackholeWatchFolderHelpText": "Pasta de onde {appName} deve importar os downloads completos", + "CustomFormatUnknownConditionOption": "Opção \"{key}\" desconhecida para a condição \"{implementation}\"" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index fe447dae8..dea94c4c1 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -744,7 +744,7 @@ "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DeleteDelayProfileMessageText": "Tem certeza de que deseja excluir este perfil de atraso?", "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista '{name}'?", - "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir este perfil de lançamento '{name}'?", + "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir o perfil de lançamento '{name}'?", "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", "EpisodeTitleRequiredHelpText": "Impeça a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for TBA", "External": "Externo", @@ -2115,5 +2115,10 @@ "CustomFormatsSpecificationExceptLanguage": "Exceto Idioma", "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponde se qualquer idioma diferente do idioma selecionado estiver presente", "MinimumCustomFormatScoreIncrement": "Incremento Mínimo da Pontuação de Formato Personalizado", - "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que {appName} considere isso uma atualização" + "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que {appName} considere isso uma atualização", + "NotificationsGotifySettingsMetadataLinks": "Links de metadados", + "NotificationsGotifySettingsMetadataLinksHelpText": "Adicionar links aos metadados da série ao enviar notificações", + "NotificationsGotifySettingsPreferredMetadataLink": "Link de Metadados Preferido", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Link de metadados para clientes que suportam apenas um único link", + "FolderNameTokens": "Tokens de Nome de Pasta" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index bd8b3b7b6..a77c947a0 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -201,5 +201,9 @@ "TimeFormat": "Format ora", "CustomFilter": "Filtru personalizat", "CustomFilters": "Filtre personalizate", - "UpdateAvailableHealthCheckMessage": "O nouă versiune este disponibilă: {version}" + "UpdateAvailableHealthCheckMessage": "O nouă versiune este disponibilă: {version}", + "OnFileImport": "La import fișier", + "FileNameTokens": "Jetoane pentru nume de fișier", + "FolderNameTokens": "Jetoane pentru nume de folder", + "OnFileUpgrade": "La actualizare fișier" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index dd92b792f..afde0c4c4 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -86,7 +86,7 @@ "TestParsing": "Тест сбора данных", "True": "Правильно", "SkipRedownload": "Пропустить повторное скачивание", - "WouldYouLikeToRestoreBackup": "Желаете восстановить резервную копию {name} ?", + "WouldYouLikeToRestoreBackup": "Хотите восстановить резервную копию '{name}'?", "RemoveTagsAutomatically": "Автоматическое удаление тегов", "RemoveTagsAutomaticallyHelpText": "Автоматически удалять теги, если условия не выполняются", "DeleteSelectedIndexers": "Удалить индексатор(ы)", @@ -240,7 +240,7 @@ "Calendar": "Календарь", "CloneProfile": "Клонировать профиль", "Ended": "Завершён", - "Download": "Скачать", + "Download": "Загрузить", "DownloadClient": "Клиент загрузки", "Donate": "Пожертвовать", "AnalyseVideoFiles": "Анализировать видео файлы", @@ -260,7 +260,7 @@ "ClickToChangeSeries": "Нажмите, чтобы изменить сериал", "CloneIndexer": "Клонировать индексер", "BackupNow": "Создать резервную копию", - "UpdaterLogFiles": "Фалы журналов обновления", + "UpdaterLogFiles": "Файлы журнала обновления", "Updates": "Обновления", "UpgradeUntil": "Обновить до качества", "UrlBaseHelpText": "Для поддержки обратного прокси, значение по умолчанию - пустая строка", @@ -385,7 +385,7 @@ "Details": "Подробности", "DiskSpace": "Дисковое пространство", "DoNotBlocklist": "Не вносить в черный список", - "DockerUpdater": "Обновить контейнер, чтобы получить обновление", + "DockerUpdater": "Обновите контейнер Docker, чтобы получить обновление", "DownloadClientDelugeTorrentStateError": "Deluge сообщает об ошибке", "DownloadClientDelugeValidationLabelPluginInactive": "Плагин меток не активирован", "DownloadClientDownloadStationProviderMessage": "Приложение {appName} не может подключиться к Download Station, если в вашей учетной записи DSM включена двухфакторная аутентификация", @@ -657,7 +657,7 @@ "AllTitles": "Все заголовки", "AnimeEpisodeTypeDescription": "Эпизоды выпущены с использованием абсолютного номера эпизода", "ApiKey": "API ключ", - "AptUpdater": "Используйте apt для установки обновления", + "AptUpdater": "Использовать apt для установки обновления", "AuthenticationMethodHelpText": "Необходимо ввести имя пользователя и пароль для доступа к {appName}", "AutoAdd": "Автоматическое добавление", "AutoTaggingSpecificationOriginalLanguage": "Язык", @@ -709,7 +709,7 @@ "DownloadClientValidationSslConnectFailureDetail": "{appName} не может подключиться к {clientName} с помощью SSL. Эта проблема может быть связана с компьютером. Попробуйте настроить {appName} и {clientName} так, чтобы они не использовали SSL.", "AnEpisodeIsDownloading": "Эпизод загружается", "ConnectionLostToBackend": "{appName} потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", - "CustomFormatHelpText": "{appName} оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то Sonarr запишет его.", + "CustomFormatHelpText": "{appName} оценивает каждый релиз используя сумму баллов по пользовательским форматам. Если новый релиз улучшит оценку, того же или лучшего качества, то {appName} запишет его.", "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите допустимый метод авторизации", "CustomFormatUnknownCondition": "Неизвестное условие пользовательского формата '{implementation}'", "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это предотвращает конфликты с несвязанными загрузками.", @@ -828,7 +828,7 @@ "Monitor": "Монитор", "MoreDetails": "Ещё подробности", "EpisodesLoadError": "Невозможно загрузить эпизоды", - "ErrorRestoringBackup": "Ошибка при восстановлении данных", + "ErrorRestoringBackup": "Восстановить резервную копию", "Exception": "Исключение", "ExistingSeries": "Существующие сериалы", "ExternalUpdater": "{appName} настроен на использование внешнего механизма обновления", @@ -1342,7 +1342,7 @@ "LibraryImportTips": "Несколько советов, чтобы импорт прошел без проблем:", "ImportListsTraktSettingsWatchedListTypeAll": "Все", "FailedToLoadSystemStatusFromApi": "Не удалось загрузить статус системы из API", - "FailedToFetchUpdates": "Не удалось получить обновления", + "FailedToFetchUpdates": "Не удалось загрузить обновления", "LibraryImportTipsDontUseDownloadsFolder": "Не используйте для импорта загрузки из вашего клиента загрузки, это предназначено только для существующих организованных библиотек, а не для неотсортированных файлов.", "IncludeCustomFormatWhenRenaming": "Включить пользовательский формат при переименовании", "IndexerHDBitsSettingsCategories": "Категории", @@ -1402,11 +1402,11 @@ "ListWillRefreshEveryInterval": "Список будет обновляться каждые {refreshInterval}", "LocalPath": "Локальный путь", "LocalStorageIsNotSupported": "Локальное хранилище не поддерживается или отключено. Плагин или приватный просмотр могли отключить его.", - "LogFilesLocation": "Файлы журнала расположены по адресу: {location}", + "LogFilesLocation": "Файлы журнала расположены в: {location}", "LogLevel": "Уровень журналирования", "LogOnly": "Только журнал", "Logging": "Журналирование", - "Logout": "Выйти", + "Logout": "Завершить сеанс", "Lowercase": "Нижний регистр", "ManageEpisodes": "Управление эпизодами", "ManageEpisodesSeason": "Управление файлами эпизодов в этом сезоне", @@ -2022,7 +2022,7 @@ "RatingVotes": "Рейтинг голосов", "NotificationsPlexSettingsServer": "Сервер", "Search": "Поиск", - "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит пользовательский интерфейс во время процесса восстановления.", + "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит интерфейс пользователя во время процесса восстановления.", "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.", "Space": "Пробел", @@ -2075,7 +2075,7 @@ "Retention": "Удержание", "UnmonitorDeletedEpisodesHelpText": "Эпизоды, удаленные с диска, автоматически не отслеживаются в приложении {appName}", "Umask775Description": "{octal} - Владелец и группа - запись, другое - чтение", - "TheLogLevelDefault": "По умолчанию уровень журнала равен «Информация», и его можно изменить в [Общие настройки](/settings/general)", + "TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Информация' и может быть изменён в [Общих настройках](/settings/general)", "SystemTimeHealthCheckMessage": "Расхождение системного времени более чем на 1 день. Запланированные задачи могут работать некорректно, пока не будет исправлено время", "UiSettingsSummary": "Параметры календаря, даты и опции для слабовидящих", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Вместо перемещения файлов это даст указание {appName} скопировать или установить жесткую ссылку (в зависимости от настроек/конфигурации системы)", From 562e0dd7c0a69460e24c3b4f7519b0a432029859 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 25 Oct 2024 17:10:46 -0700 Subject: [PATCH 589/762] Bump version to 4.0.10 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6339833c8..67fd6f9e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.9 + VERSION: 4.0.10 jobs: backend: From 0784f56b9ac71d36800209d3d73b50d78e72650f Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 26 Oct 2024 17:25:33 +0000 Subject: [PATCH 590/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ardenet <1213193613@qq.com> Co-authored-by: fordas Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 130 +++++++++--------- .../Localization/Core/zh_CN.json | 92 +++++++------ 2 files changed, 113 insertions(+), 109 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 5b26e7e54..28799aaaa 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -24,7 +24,7 @@ "AddCustomFormat": "Añadir Formato Personalizado", "AddDelayProfile": "Añadir Perfil de Retraso", "AddDownloadClient": "Añadir Cliente de Descarga", - "AddDownloadClientError": "No se pudo añadir un nuevo cliente de descargas, inténtelo de nuevo.", + "AddDownloadClientError": "No se ha podido añadir un nuevo cliente de descarga, prueba otra vez.", "AddExclusion": "Añadir Exclusión", "AddImportList": "Añadir Lista de Importación", "AddImportListExclusion": "Añadir Exclusión de Lista de Importación", @@ -59,7 +59,7 @@ "Custom": "Personalizado", "Cutoff": "Límite", "Dates": "Fechas", - "Debug": "Debug", + "Debug": "Depuración", "Date": "Fecha", "DeleteTag": "Eliminar Etiqueta", "Duplicate": "Duplicar", @@ -109,8 +109,8 @@ "Tags": "Etiquetas", "Unmonitored": "Sin monitorizar", "Yes": "Sí", - "ApplyTagsHelpTextHowToApplyDownloadClients": "Cómo añadir etiquetas a los clientes de descargas seleccionados", - "ApplyTagsHelpTextHowToApplyImportLists": "Cómo añadir etiquetas a las listas de importación seleccionadas", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Cómo aplicar etiquetas a los clientes de descargas seleccionados", + "ApplyTagsHelpTextHowToApplyImportLists": "Cómo aplicar etiquetas a las listas de importación seleccionadas", "ApplyTagsHelpTextHowToApplyIndexers": "Cómo aplicar etiquetas a los indexadores seleccionados", "ApplyTagsHelpTextHowToApplySeries": "Cómo añadir etiquetas a las series seleccionadas", "Series": "Series", @@ -132,7 +132,7 @@ "ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente", "ApplyTagsHelpTextRemove": "Eliminar: Elimina las etiquetas introducidas", "Blocklist": "Lista de bloqueos", - "Grabbed": "Añadido", + "Grabbed": "Capturado", "Genres": "Géneros", "Indexer": "Indexador", "Imported": "Importado", @@ -149,7 +149,7 @@ "Title": "Título", "Type": "Tipo", "AppDataDirectory": "Directorio AppData", - "AptUpdater": "Use apt para instalar la actualización", + "AptUpdater": "Usa apt para instalar la actualización", "BackupNow": "Hacer copia de seguridad ahora", "Backups": "Copias de seguridad", "BeforeUpdate": "Antes de actualizar", @@ -159,7 +159,7 @@ "Queued": "Encolado", "Source": "Fuente", "Reset": "Reiniciar", - "AddCustomFormatError": "No se pudo añadir un nuevo formato personalizado, inténtelo de nuevo.", + "AddCustomFormatError": "No se ha podido añadir un nuevo formato personalizado, prueba otra vez.", "AddIndexer": "Añadir Indexador", "AddListError": "No se pudo añadir una nueva lista, inténtelo de nuevo.", "AddRemotePathMappingError": "No se pudo añadir una nueva asignación de ruta remota, inténtelo de nuevo.", @@ -197,20 +197,20 @@ "CloneCondition": "Clonar Condición", "EditSelectedImportLists": "Editar Listas de Importación Seleccionadas", "EditSelectedDownloadClients": "Editar Clientes de Descarga Seleccionados", - "DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?", + "DeleteRemotePathMappingMessageText": "¿Estás seguro que quieres eliminar esta asignación de ruta remota?", "Implementation": "Implementación", "ImportUsingScript": "Importar usando un script", "CloneAutoTag": "Clonar Etiquetado Automático", - "ManageIndexers": "Gestionar Indexadores", + "ManageIndexers": "Administrar Indexadores", "DeleteAutoTag": "Eliminar Etiquetado Automático", "DeleteRootFolder": "Eliminar Carpeta Raíz", "EditAutoTag": "Editar Etiquetado Automático", - "ManageClients": "Gestionar Clientes", - "ManageImportLists": "Gestionar Listas de Importación", - "ManageDownloadClients": "Gestionar Clientes de Descarga", + "ManageClients": "Administrar Clientes", + "ManageImportLists": "Administrar Listas de Importación", + "ManageDownloadClients": "Administrar Clientes de Descarga", "MoveAutomatically": "Mover Automáticamente", "IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {indexerNames}.", - "ManageLists": "Gestionar Listas", + "ManageLists": "Administrar Listas", "DeleteSelectedImportLists": "Eliminar Lista(s) de Importación", "EditSelectedIndexers": "Editar Indexadores Seleccionados", "ImportScriptPath": "Importar Ruta de Script", @@ -237,21 +237,21 @@ "DeleteIndexerMessageText": "¿Estás seguro que quieres eliminar el indexador '{name}'?", "BlocklistLoadError": "No se ha podido cargar la lista de bloqueos", "BypassDelayIfAboveCustomFormatScore": "Omitir si está por encima de la puntuación del formato personalizado", - "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para evitar el retraso del protocolo preferido", - "DeleteDownloadClientMessageText": "Seguro que quieres eliminar el gestor de descargas '{name}'?", - "DeleteNotificationMessageText": "¿Seguro que quieres eliminar la notificación '{name}'?", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para omitir el retraso del protocolo preferido", + "DeleteDownloadClientMessageText": "¿Estás seguro que quieres eliminar el cliente de descarga '{name}'?", + "DeleteNotificationMessageText": "¿Estás seguro que quieres eliminar la notificación '{name}'?", "DefaultNameCopiedSpecification": "{name} - Copia", "DefaultNameCopiedProfile": "{name} - Copia", - "DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{name}'?", - "DeleteQualityProfileMessageText": "¿Seguro que quieres eliminar el perfil de calidad {name}?", - "DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{path}'?", + "DeleteConditionMessageText": "¿Estás seguro que quieres eliminar la etiqueta '{name}'?", + "DeleteQualityProfileMessageText": "¿Estás seguro que quieres eliminar el perfil de calidad {name}?", + "DeleteRootFolderMessageText": "¿Estás seguro que quieres eliminar la carpeta raíz '{path}'?", "DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?", "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.", "CalendarOptions": "Opciones de Calendario", - "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilita la omisión cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", "Default": "Predeterminado", - "DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{name}'?", - "DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?", + "DeleteBackupMessageText": "¿Estás seguro que quieres eliminar la copia de seguridad '{name}'?", + "DeleteAutoTagHelpText": "¿Estás seguro que quieres eliminar el etiquetado automático '{name}'?", "AddImportListImplementation": "Añadir lista de importación - {implementationName}", "AddIndexerImplementation": "Agregar Indexador - {implementationName}", "AutoRedownloadFailed": "Descarga fallida", @@ -259,13 +259,13 @@ "CustomFormatJson": "Formato JSON personalizado", "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", "DeleteImportList": "Eliminar lista de importación", - "DeleteImportListMessageText": "Seguro que quieres eliminar la lista '{name}'?", + "DeleteImportListMessageText": "¿Estás seguro que quieres eliminar la lista '{name}'?", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Búsqueda automática e intento de descarga de una versión diferente cuando se obtiene una versión fallida de la búsqueda interactiva", "AutoRedownloadFailedFromInteractiveSearch": "Fallo al volver a descargar desde la búsqueda interactiva", "DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?", "DeleteSelectedImportListsMessageText": "¿Estás seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", "DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada", - "DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?", + "DeleteTagMessageText": "¿Estás seguro que quieres eliminar la etiqueta '{label}'?", "DisabledForLocalAddresses": "Deshabilitada para direcciones locales", "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", @@ -311,13 +311,13 @@ "ChooseAnotherFolder": "Elige otra Carpeta", "ClientPriority": "Prioridad del Cliente", "CloneIndexer": "Clonar indexador", - "BranchUpdateMechanism": "La rama se uso por un mecanisco de actualizacion externo", - "BrowserReloadRequired": "Se requiere recargar el explorador", + "BranchUpdateMechanism": "Rama usada por un mecanismo de actualización externo", + "BrowserReloadRequired": "Recarga de Navegador Requerida", "CalendarLoadError": "Incapaz de cargar el calendario", - "CertificateValidation": "Validacion de certificado", + "CertificateValidation": "Validación de certificado", "BypassProxyForLocalAddresses": "Omitir Proxy para Direcciones Locales", "ChangeFileDateHelpText": "Cambia la fecha del archivo al importar/volver a escanear", - "ChownGroupHelpText": "Nombre del grupo o gid. Utilice gid para sistemas de archivos remotos.", + "ChownGroupHelpText": "Nombre del grupo o gid. Utiliza gid para sistemas de archivos remotos.", "CloneProfile": "Clonar Perfil", "CollectionsLoadError": "No se han podido cargar las colecciones", "ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados", @@ -348,7 +348,7 @@ "CustomFormatUnknownConditionOption": "Opción Desconocida '{key}' para condición '{implementation}'", "CustomFormatScore": "Puntuación de formato personalizado", "Connection": "Conexiones", - "CancelPendingTask": "Estas seguro de que deseas cancelar esta tarea pendiente?", + "CancelPendingTask": "¿Estás seguro que quieres cancelar esta tarea pendiente?", "Clear": "Borrar", "AnEpisodeIsDownloading": "Un episodio se esta descargando", "Agenda": "Agenda", @@ -378,20 +378,20 @@ "CalendarLegendEpisodeUnairedTooltip": "El episodio no ha sido emitido todavia", "CalendarLegendEpisodeUnmonitoredTooltip": "El episodio esta sin monitorizar", "AnimeEpisodeTypeFormat": "Numero de episodio absoluto ({format})", - "BypassDelayIfHighestQualityHelpText": "Evitar el retardo cuando el lanzamiento tiene habilitada la máxima calidad en el perfil de calidad con el protocolo preferido", + "BypassDelayIfHighestQualityHelpText": "Omite el retraso cuando el lanzamiento tiene habilitada la máxima calidad en el perfil de calidad con el protocolo preferido", "CalendarFeed": "Canal de calendario de {appName}", "ChooseImportMode": "Elegir Modo de Importación", - "ClickToChangeLanguage": "Clic para cambiar el idioma", - "ClickToChangeQuality": "Clic para cambiar la calidad", + "ClickToChangeLanguage": "Haz clic para cambiar el idioma", + "ClickToChangeQuality": "Haz clic para cambiar la calidad", "ClickToChangeSeason": "Click para cambiar la temporada", "ChmodFolderHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga establezca los permisos correctamente.", "BlackholeWatchFolderHelpText": "Carpeta desde la que {appName} debería importar las descargas completadas", - "BlackholeFolderHelpText": "La carpeta en donde {appName} se almacenaran los {extension} file", - "CancelProcessing": "Procesando cancelacion", + "BlackholeFolderHelpText": "La carpeta donde {appName} almacenará los archivos {extension}", + "CancelProcessing": "Procesando cancelación", "Category": "Categoría", "WhatsNew": "¿Qué hay nuevo?", "BlocklistReleases": "Lista de bloqueos de lanzamientos", - "BypassDelayIfHighestQuality": "Pasar sí es la calidad más alta", + "BypassDelayIfHighestQuality": "Omitir si es la calidad más alta", "ChownGroupHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga use el mismo grupo que {appName}.", "ClearBlocklistMessageText": "¿Estás seguro de que quieres borrar todos los elementos de la lista de bloqueos?", "FormatAgeDay": "día", @@ -400,7 +400,7 @@ "FormatAgeHours": "horas", "FormatDateTime": "{formattedDate} {formattedTime}", "DownloadIgnored": "Descarga ignorada", - "EditImportListImplementation": "Añadir lista de importación - {implementationName}", + "EditImportListImplementation": "Editar lista de importación - {implementationName}", "EditIndexerImplementation": "Editar Indexador - {implementationName}", "EnableProfile": "Habilitar perfil", "False": "Falso", @@ -408,7 +408,7 @@ "FormatAgeMinutes": "minutos", "UnknownEventTooltip": "Evento desconocído", "DownloadWarning": "Alerta de descarga: {warningMessage}", - "DownloadClientsLoadError": "No se puden cargar los gestores de descargas", + "DownloadClientsLoadError": "No se pudieron cargar los clientes de descargas", "FormatAgeMinute": "minuto", "FormatRuntimeHours": "{hours}h", "FormatRuntimeMinutes": "{minutes}m", @@ -420,7 +420,7 @@ "EditConditionImplementation": "Editar Condición - {implementationName}", "FailedToFetchUpdates": "Fallo al buscar las actualizaciones", "FailedToUpdateSettings": "Fallo al actualizar los ajustes", - "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver historial de commits de Github para mas detalle", + "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver el historial de commits de Github para más detalles", "CreateEmptySeriesFoldersHelpText": "Crea carpetas de series faltantes durante el análisis del disco", "DefaultCase": "Caso predeterminado", "Daily": "Diario", @@ -441,7 +441,7 @@ "DelayMinutes": "{delay} minutos", "Continuing": "Continua", "CustomFormats": "Formatos personalizados", - "AddRootFolderError": "No se pudoagregar la carpeta raíz", + "AddRootFolderError": "No se pudo añadir la carpeta raíz", "CollapseAll": "Desplegar todo", "DailyEpisodeTypeDescription": "Episodios publicados diariamente o con menos frecuencia que utilizan año, mes y día (2023-08-04)", "DefaultNotFoundMessage": "Debes estar perdido, no hay nada que ver aquí.", @@ -455,26 +455,26 @@ "LogFilesLocation": "Los archivos de registro se encuentran en: {location}", "DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido", "InfoUrl": "Información de la URL", - "HealthMessagesInfoBox": "Puedes encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte en los enlaces que aparecen a continuación.", + "HealthMessagesInfoBox": "Puedes encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte en los enlaces que aparecen abajo.", "ManualGrab": "Captura manual", "FullColorEvents": "Eventos a todo color", "FullColorEventsHelpText": "Estilo alterado para colorear todo el evento con el color de estado, en lugar de sólo el borde izquierdo. No se aplica a la Agenda", "InteractiveImportLoadError": "No se pueden cargar elementos de la importación manual", "InteractiveImportNoFilesFound": "No se han encontrado archivos de vídeo en la carpeta seleccionada", - "InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado", + "InteractiveImportNoQuality": "Debe elegirse la calidad para cada archivo seleccionado", "InteractiveSearchModalHeader": "Búsqueda interactiva", - "InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración", + "InvalidUILanguage": "Tu interfaz de usuario está configurada en un idioma inválido, corrígelo y guarda la configuración", "ChownGroup": "chown del grupo", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo", "ContinuingSeriesDescription": "Se esperan más episodios u otra temporada", "CutoffUnmetLoadError": "Error cargando elementos con límites no alcanzados", "CutoffUnmetNoItems": "Ningún elemento con límites no alcanzados", - "DelayProfile": "Perfil de retardo", + "DelayProfile": "Perfil de retraso", "Delete": "Eliminar", - "DeleteDelayProfile": "Eliminar Perfil de Retardo", - "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)", - "DelayProfiles": "Perfiles de retardo", + "DeleteDelayProfile": "Eliminar Perfil de Retraso", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usa el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crea una subcarpeta (qBittorrent 4.3.2+)", + "DelayProfiles": "Perfiles de retraso", "DeleteCustomFormatMessageText": "¿Estás seguro que quieres eliminar el formato personalizado '{name}'?", "DeleteBackup": "Eliminar copia de seguridad", "CopyUsingHardlinksSeriesHelpText": "Los hardlinks permiten a {appName} a importar los torrents que se estén compartiendo a la carpeta de la serie sin usar espacio adicional en el disco o sin copiar el contenido completo del archivo. Los hardlinks solo funcionarán si el origen y el destino están en el mismo volumen", @@ -483,7 +483,7 @@ "DeleteCustomFormat": "Eliminar formato personalizado", "BlackholeWatchFolder": "Monitorizar carpeta", "DeleteEmptyFolders": "Eliminar directorios vacíos", - "DeleteNotification": "Borrar Notificacion", + "DeleteNotification": "Eliminar Notificación", "DeleteReleaseProfile": "Eliminar perfil de lanzamiento", "Details": "Detalles", "DeleteDownloadClient": "Borrar cliente de descarga", @@ -491,12 +491,12 @@ "DestinationPath": "Ruta de destino", "DeleteImportListExclusion": "Eliminar exclusión de listas de importación", "DeleteSeriesFolderConfirmation": "El directorio de series '{path}' y todos sus contenidos seran eliminados.", - "DeleteDelayProfileMessageText": "¿Está seguro de que desea borrar este perfil de retraso?", + "DeleteDelayProfileMessageText": "¿Estás seguro que quieres borrar este perfil de retraso?", "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", - "DeleteImportListExclusionMessageText": "¿Está seguro de que desea eliminar esta exclusión de la lista de importación?", - "DeleteQualityProfile": "Borrar perfil de calidad", + "DeleteImportListExclusionMessageText": "¿Estás seguro que quieres eliminar esta exclusión de la lista de importación?", + "DeleteQualityProfile": "Eliminar perfil de calidad", "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar el perfil de lanzamiento '{name}'?", - "DeleteRemotePathMapping": "Borrar mapeo de ruta remota", + "DeleteRemotePathMapping": "Eliminar asignación de ruta remota", "DeleteSelectedEpisodeFiles": "Borrar los archivos de episodios seleccionados", "DeleteSelectedEpisodeFilesHelpText": "Esta seguro que desea borrar los archivos de episodios seleccionados?", "DeleteSeriesFolder": "Eliminar directorio de series", @@ -520,7 +520,7 @@ "Destination": "Destino", "DestinationRelativePath": "Ruta Relativa de Destino", "DetailedProgressBar": "Barra de Progreso Detallada", - "DetailedProgressBarHelpText": "Mostrar tecto en la barra de progreso", + "DetailedProgressBarHelpText": "Mostrar texto en la barra de progreso", "DeletedReasonEpisodeMissingFromDisk": "{appName} no pudo encontrar el archivo en disco entonces el archivo fue desvinculado del episodio en la base de datos", "DeleteEmptySeriesFoldersHelpText": "Eliminar carpetas vacías de series y temporadas durante el escaneo del disco y cuando se eliminen archivos correspondientes a episodios", "DeleteEpisodeFile": "Eliminar archivo de episodio", @@ -541,7 +541,7 @@ "DownloadClientFreeboxUnableToReachFreebox": "No se pudo alcanzar la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene múltiples 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", + "DownloadClientOptionsLoadError": "No se han podido cargar las opciones del cliente de descarga", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC", "DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb", "Docker": "Docker", @@ -585,7 +585,7 @@ "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La configuración KeepHistory de NzbGet está establecida demasiado alta.", "UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", - "DownloadClientDownloadStationProviderMessage": "{appName} no pudo conectarse a la Estación de Descarga si la Autenticación de 2 factores está habilitada en su cuenta de DSM", + "DownloadClientDownloadStationProviderMessage": "{appName} no pudo conectarse a Download Station si la Autenticación de 2 factores está habilitada en tu cuenta de DSM", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", "DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Por defecto: 1. Se usa Round-Robin para clientes con la misma prioridad.", "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de etiqueta no activado", @@ -626,7 +626,7 @@ "EditSelectedSeries": "Editar series seleccionadas", "EnableAutomaticSearchHelpTextWarning": "Será usado cuando se use la búsqueda interactiva", "EnableColorImpairedMode": "Habilitar Modo de dificultad con los colores", - "EnableMetadataHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato", + "EnableMetadataHelpText": "Habilita la creación de un archivo de metadatos para este tipo de metadato", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas por debajo de la versión 3.3.0 de qBittorrent. Por favor actualiza o inténtalo de nuevo con una categoría vacía.", "DownloadClientValidationCategoryMissing": "La categoría no existe", "DownloadClientValidationGroupMissingDetail": "El grupo que introdujiste no existe en {clientName}. Créalo en {clientName} primero.", @@ -829,7 +829,7 @@ "EpisodeMissingAbsoluteNumber": "El episodio no tiene un número de episodio absoluto", "EpisodeTitleRequired": "Título del episodio requerido", "ExternalUpdater": "{appName} está configurado para usar un mecanismo de actualización externo", - "ExtraFileExtensionsHelpText": "Lista de archivos adicionales separados por coma para importar (.nfo será importado como .nfo-orig)", + "ExtraFileExtensionsHelpText": "Lista separada con comas de los archivos adicionales a importar (.nfo será importado como .nfo-orig)", "FailedToLoadSystemStatusFromApi": "Error al cargar el estado del sistema desde la API", "FailedToLoadUiSettingsFromApi": "Error al cargar opciones de la interfaz de usuario desde la API", "FileManagement": "Gestión de archivos", @@ -873,7 +873,7 @@ "Group": "Grupo", "ImportListSearchForMissingEpisodes": "Buscar episodios faltantes", "EnableProfileHelpText": "Marcar para habilitar el perfil de lanzamiento", - "EnableRssHelpText": "Se usará cuando {appName} busque periódicamente lanzamientos vía Sincronización RSS", + "EnableRssHelpText": "Se utilizará cuando {appName} busque periódicamente publicaciones a través de la sincronización por RSS", "EndedSeriesDescription": "No se esperan episodios o temporadas adicionales", "EpisodeFileDeleted": "Archivo de episodio eliminado", "EpisodeFileDeletedTooltip": "Archivo de episodio eliminado", @@ -886,7 +886,7 @@ "ImportExistingSeries": "Importar series existentes", "ImportErrors": "Importar errores", "ImportList": "Importar lista", - "ImportListSettings": "Importar ajustes de lista", + "ImportListSettings": "Ajustes de Listas de Importación", "ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o listas de Trakt y gestionar exclusiones de lista", "IncludeCustomFormatWhenRenamingHelpText": "Incluir en formato de renombrado {Custom Formats}", "QualityCutoffNotMet": "No se ha alcanzado el límite de calidad", @@ -909,7 +909,7 @@ "ImportCustomFormat": "Importar formato personalizado", "ImportExtraFiles": "Importar archivos adicionales", "ImportExtraFilesEpisodeHelpText": "Importa archivos adicionales (subtítulos, nfo, etc) tras importar un archivo de episodio", - "ImportListExclusionsLoadError": "No se pudo cargar Importar lista de exclusiones", + "ImportListExclusionsLoadError": "No se pueden cargar las exclusiones de listas de importación", "ImportListStatusUnavailableHealthCheckMessage": "Listas no disponibles debido a fallos: {importListNames}", "ImportListsAniListSettingsAuthenticateWithAniList": "Autenticar con AniList", "ImportListsAniListSettingsImportCompletedHelpText": "Lista: Vistos por completo", @@ -932,7 +932,7 @@ "ImportListsCustomListValidationConnectionError": "No se pudo hacer la petición a esa URL. Código de estado: {exceptionStatusCode}", "ImportListsImdbSettingsListId": "ID de lista", "ImportListsImdbSettingsListIdHelpText": "ID de lista de IMDb (p. ej. ls12345678)", - "ImportListsLoadError": "No se pudo cargas Importar listas", + "ImportListsLoadError": "No se pueden cargar las Listas de Importación", "ImportListsPlexSettingsAuthenticateWithPlex": "Autenticar con Plex.tv", "ImportListsPlexSettingsWatchlistName": "Lista de seguimiento de Plex", "ImportListsSettingsExpires": "Expira", @@ -1015,7 +1015,7 @@ "Images": "Imágenes", "ImportCountSeries": "Importar {selectedCount} series", "ImportListStatusAllUnavailableHealthCheckMessage": "Ninguna lista está disponible debido a fallos", - "ImportLists": "Importar listas", + "ImportLists": "Listas de Importación", "ImportListsAniListSettingsImportHiatusHelpText": "Medios: Series en hiatus", "ImportListsAniListSettingsImportCompleted": "Importación Completados", "ImportListsAniListSettingsImportCancelledHelpText": "Medios: Series que están canceladas", @@ -1042,7 +1042,7 @@ "CleanLibraryLevel": "Limpiar nivel de biblioteca", "SearchForCutoffUnmetEpisodes": "Buscar todos los episodios con límites no alcanzados", "IconForSpecials": "Icono para Especiales", - "ImportListExclusions": "Importar lista de exclusiones", + "ImportListExclusions": "Exclusiones de listas de importación", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas las listas requieren interacción manual debido a posibles búsquedas parciales", "ImportListsAniListSettingsImportCancelled": "Importación Cancelados", "ImportListsAniListSettingsImportPlanning": "Importar planeados", @@ -1166,7 +1166,7 @@ "ListRootFolderHelpText": "Los elementos de la lista de carpetas raíz se añadirán a", "Local": "Local", "LocalStorageIsNotSupported": "El Almacenamiento Local no es compatible o está deshabilitado. Es posible que un plugin o la navegación privada lo hayan deshabilitado.", - "ListOptionsLoadError": "No se pueden cargar opciones de lista", + "ListOptionsLoadError": "No se pueden cargar las opciones de lista", "ListsLoadError": "No se pueden cargar las Listas", "Logout": "Cerrar Sesión", "Links": "Enlaces", @@ -1183,7 +1183,7 @@ "ManageEpisodesSeason": "Gestionar los archivos de Episodios de esta temporada", "LibraryImportTipsDontUseDownloadsFolder": "No lo utilices para importar descargas desde tu cliente de descarga, esto es sólo para bibliotecas organizadas existentes, no para archivos sin clasificar.", "LocalAirDate": "Fecha de emisión local", - "NotificationsEmailSettingsUseEncryptionHelpText": "Si prefiere utilizar el cifrado si está configurado en el servidor, utilizar siempre el cifrado mediante SSL (sólo puerto 465) o StartTLS (cualquier otro puerto) o no utilizar nunca el cifrado", + "NotificationsEmailSettingsUseEncryptionHelpText": "Si prefiere utilizar el cifrado si está configurado en el servidor, utilizar siempre el cifrado mediante SSL (sólo puerto 465) o StartTLS (cualquier otro puerto), o no utilizar nunca el cifrado", "LibraryImportTipsSeriesUseRootFolder": "Dirije {appName} a la carpeta que contenga todas tus series de TV, no a una en concreto. Por ejemplo, \"`{goodFolderExample}`\" y no \"`{badFolderExample}`\". Además, cada serie debe estar en su propia carpeta dentro de la carpeta raíz/biblioteca.", "ListSyncTag": "Etiqueta de Sincronización de Lista", "ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)", @@ -2083,7 +2083,7 @@ "CustomColonReplacementFormatHint": "Caracteres válidos del sistema de archivos como dos puntos (letra)", "OnFileImport": "Al importar un archivo", "OnImportComplete": "Al completar la importación", - "OnFileUpgrade": "Al actualizar archivo", + "OnFileUpgrade": "Al actualizar un archivo", "RatingVotes": "Calificaciones", "ShowTagsHelpText": "Muestra etiquetas debajo del póster", "ShowTags": "Mostrar etiquetas", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 83aa2078c..c99686e2e 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1,8 +1,8 @@ { "CloneCondition": "克隆条件", "DeleteCondition": "删除条件", - "DeleteConditionMessageText": "您确认要删除条件 “{name}” 吗?", - "DeleteCustomFormatMessageText": "是否确实要删除条件“{name}”?", + "DeleteConditionMessageText": "您确定要删除条件 “{name}” 吗?", + "DeleteCustomFormatMessageText": "您确定要删除自定义格式 “{name}” 吗?", "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少 {length} 个字符长。您可以通过设置或配置文件完成此操作", "RemoveSelectedItemQueueMessageText": "您确认要从队列中移除 1 项吗?", "RemoveSelectedItemsQueueMessageText": "您确认要从队列中移除 {selectedCount} 项吗?", @@ -30,7 +30,7 @@ "NoDownloadClientsFound": "找不到下载客户端", "NoImportListsFound": "未找到导入列表", "NoIndexersFound": "未找到索引器", - "Added": "已添加", + "Added": "添加日期", "Add": "添加", "AddingTag": "添加标签", "Apply": "应用", @@ -169,7 +169,7 @@ "Deleted": "已删除", "Disabled": "已禁用", "Discord": "Discord", - "DiskSpace": "硬盘空间", + "DiskSpace": "磁盘空间", "Docker": "Docker", "DockerUpdater": "更新Docker容器以更新应用", "Donations": "赞助", @@ -201,7 +201,7 @@ "Info": "信息", "LatestSeason": "最新季", "MissingEpisodes": "缺失集", - "MonitoredOnly": "仅追踪", + "MonitoredOnly": "已追踪项", "NotSeasonPack": "非季包", "OutputPath": "输出路径", "PackageVersion": "Package版本", @@ -218,7 +218,7 @@ "RelativePath": "相对路径", "ReleaseGroups": "发布组", "Reload": "重新加载", - "DeleteBackupMessageText": "您确定要删除备份“{name}”吗?", + "DeleteBackupMessageText": "您确定要删除备份 “{name}” 吗?", "EnableAutomaticSearch": "启用自动搜索", "EpisodeAirDate": "剧集播出日期", "IndexerSearchNoInteractiveHealthCheckMessage": "没有启用交互式搜索的索引器,{appName}将不提供任何交互式搜索结果", @@ -362,7 +362,7 @@ "UpdaterLogFiles": "更新器日志文件", "Uptime": "运行时间", "Wiki": "Wiki", - "WouldYouLikeToRestoreBackup": "是否要还原备份“{name}”?", + "WouldYouLikeToRestoreBackup": "是否要还原备份 “{name}”?", "YesCancel": "确定,取消", "UpdateAvailableHealthCheckMessage": "有新的更新可用:{version}", "AddAutoTag": "添加自动标签", @@ -415,7 +415,7 @@ "BrowserReloadRequired": "浏览器需重新加载", "BuiltIn": "内置的", "BypassDelayIfAboveCustomFormatScore": "若高于自定义格式分数则跳过", - "BypassDelayIfHighestQualityHelpText": "当发布资源的质量为 “质量配置” 中最高启用质量且使用首选协议时,忽略延迟", + "BypassDelayIfHighestQualityHelpText": "当发布资源的质量为 “质量配置” 中最高启用质量且为首选协议时,忽略延迟", "BypassProxyForLocalAddresses": "对局域网地址不使用代理", "CalendarFeed": "{appName} 日历订阅", "CalendarLegendEpisodeDownloadedTooltip": "集已下载完成并完成排序", @@ -457,7 +457,7 @@ "CreateGroup": "创建组", "CustomFilters": "自定义过滤器", "CustomFormatUnknownCondition": "未知自定义格式条件'{implementation}'", - "CustomFormatUnknownConditionOption": "条件“{implementation}”的未知选项“{key}”", + "CustomFormatUnknownConditionOption": "条件 “{implementation}” 的未知选项 “{key}”", "CustomFormatsLoadError": "无法加载自定义格式", "CustomFormatsSettings": "自定义格式设置", "CustomFormatsSettingsSummary": "自定义格式和设置", @@ -475,7 +475,7 @@ "DeleteAutoTag": "删除自动标签", "DeleteDelayProfileMessageText": "您确认要删除此延迟配置吗?", "DeleteDownloadClient": "删除下载客户端", - "DeleteDownloadClientMessageText": "您确认要删除下载客户端 “{name}” 吗?", + "DeleteDownloadClientMessageText": "您确定要删除下载客户端 “{name}” 吗?", "DeleteEmptyFolders": "删除空目录", "DeleteEmptySeriesFoldersHelpText": "如果集文件被删除,当磁盘扫描时删除剧集或季的空目录", "DeleteEpisodeFile": "删除集文件", @@ -483,12 +483,12 @@ "DeleteEpisodeFromDisk": "从磁盘中删除剧集", "DeleteImportList": "删除导入的列表", "DeleteQualityProfile": "删除质量配置", - "DeleteQualityProfileMessageText": "您确定要删除质量配置“{name}”吗?", + "DeleteQualityProfileMessageText": "您确定要删除质量配置 “{name}” 吗?", "DeleteReleaseProfile": "删除发布资源配置", "DeleteReleaseProfileMessageText": "您确定要删除发布资源配置 “{name}” 吗?", "DeleteRemotePathMapping": "删除远程路径映射", "DeleteRemotePathMappingMessageText": "您确认要删除此远程路径映射吗?", - "DeleteRootFolderMessageText": "您确认要删除根目录 “{path}” 吗?", + "DeleteRootFolderMessageText": "您确定要删除根目录 “{path}” 吗?", "DeleteSelectedEpisodeFilesHelpText": "您确定要删除选定的剧集文件吗?", "DeleteTag": "删除标签", "DownloadClientOptionsLoadError": "无法加载下载客户端选项", @@ -536,8 +536,8 @@ "AllSeriesInRootFolderHaveBeenImported": "{path} 中的所有剧集都已导入", "Analytics": "分析", "Anime": "动漫", - "AnalyseVideoFilesHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要{appName}读取文件,可能导致扫描期间磁盘或网络出现高负载。", - "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到{appName}的服务器。这包括有关您的浏览器的信息、您使用的{appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", + "AnalyseVideoFilesHelpText": "从文件中提取视频信息,如分辨率、时长和编解码器信息。这需要 {appName} 在扫描期间读取文件并可能导致高磁盘或网络占用。", + "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到 {appName} 的服务器。这包括有关您的浏览器信息、您使用的 {appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", "AnalyseVideoFiles": "分析视频文件", "ApplicationURL": "应用程序 URL", "AnimeEpisodeTypeDescription": "使用绝对集数发布的集数", @@ -575,7 +575,7 @@ "DeleteImportListExclusionMessageText": "您确认要删除此导入排除列表吗?", "DeleteImportListMessageText": "您确定要删除列表 “{name}” 吗?", "DeleteNotification": "删除消息推送", - "DeleteNotificationMessageText": "您确定要删除通知“{name}”吗?", + "DeleteNotificationMessageText": "您确定要删除通知 “{name}” 吗?", "DeleteIndexer": "删除索引器", "DeleteTagMessageText": "您确定要删除标签 '{label}' 吗?", "DeletedReasonUpgrade": "升级时删除原文件", @@ -604,14 +604,14 @@ "CopyUsingHardlinksSeriesHelpText": "硬链接 (Hardlinks) 允许 {appName} 将还在做种中的剧集文件(夹)导入而不占用额外的存储空间或者复制文件(夹)的全部内容。硬链接 (Hardlinks) 仅能在源文件和目标文件在同一磁盘卷中使用", "CustomFormat": "自定义格式", "CustomFormatHelpText": "{appName} 会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数且有相同或更高的质量,则 {appName} 会抓取该发布版本。", - "DeleteAutoTagHelpText": "您确认要删除 “{name}” 自动标签吗?", + "DeleteAutoTagHelpText": "您确定要删除 “{name}” 自动标签吗?", "DownloadClientSeriesTagHelpText": "仅将此下载客户端用于至少具有一个匹配标签的剧集。留空可用于所有剧集。", "Absolute": "绝对", "AddANewPath": "添加一个新的目录", "AbsoluteEpisodeNumber": "准确的集数", "DeleteSelectedEpisodeFiles": "删除选定的剧集文件", "Donate": "捐赠", - "DownloadPropersAndRepacksHelpTextCustomFormat": "使用“不要首选”按 优化版(Propers) / 重制版(Repacks) 中的自定义格式分数排序", + "DownloadPropersAndRepacksHelpTextCustomFormat": "使用 “不喜欢” 按自定义格式分数对 Propers/Repacks 排序", "DownloadPropersAndRepacksHelpTextWarning": "使用自定义格式自动升级到优化版/重制版", "EditConditionImplementation": "编辑条件- {implementationName}", "EditConnectionImplementation": "编辑连接- {implementationName}", @@ -796,7 +796,7 @@ "ReplaceIllegalCharactersHelpText": "替换非法字符。如未勾选,则字符会被 {appName} 移除", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们]({url})。", "DeleteSelectedSeries": "删除选中的剧集", - "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", + "ProxyBypassFilterHelpText": "使用 “ , ” 作为分隔符,并使用 “ *. ” 作为二级域名的通配符", "DeleteSeriesFolderCountConfirmation": "你确定要删除选中的 {count} 个剧集吗?", "PublishedDate": "发布日期", "FormatAgeDay": "天", @@ -1162,7 +1162,7 @@ "TagsSettingsSummary": "显示全部标签和标签使用情况,可删除未使用的标签", "Tba": "待公布", "Test": "测试", - "ThemeHelpText": "改变应用界面主题,选择“自动”主题会通过操作系统主题来自适应白天黑夜模式。(受Theme.Park启发)", + "ThemeHelpText": "更改程序界面主题,“自动” 主题将根据您的操作系统主题来设置浅色或深色模式。灵感来自 Theme.Park", "TimeFormat": "时间格式", "ToggleMonitoredSeriesUnmonitored": "当系列不受监控时,无法切换监控状态", "ToggleMonitoredToUnmonitored": "已监视,单击可取消监视", @@ -1188,7 +1188,7 @@ "UnmonitorDeletedEpisodesHelpText": "从磁盘删除的集将在 {appName} 中自动取消监控", "UnmonitorSpecialEpisodes": "取消监控特别节目", "UpdateAll": "全部更新", - "UpdateAutomaticallyHelpText": "自动下载并安装更新。您还可以在 “系统:更新” 中安装", + "UpdateAutomaticallyHelpText": "自动下载并安装更新。您还可以在「“系统”->“更新”」中安装", "UpdateSelected": "更新选择的内容", "UpgradeUntilThisQualityIsMetOrExceeded": "升级资源直至质量达标或高于标准", "UpgradesAllowed": "允许升级", @@ -1274,7 +1274,7 @@ "InteractiveSearch": "手动搜索", "InteractiveSearchModalHeader": "手动搜索", "InvalidFormat": "格式不合法", - "InvalidUILanguage": "您的UI设置为无效语言,请纠正并保存设置", + "InvalidUILanguage": "您的 UI 设置为无效语言,请纠正并保存设置", "KeyboardShortcuts": "快捷键", "KeyboardShortcutsSaveSettings": "保存设置", "ListRootFolderHelpText": "根目录列表中的项目将被添加", @@ -1338,7 +1338,7 @@ "SslCertPath": "SSL证书路径", "SslCertPathHelpText": "pfx文件路径", "SupportedAutoTaggingProperties": "{appName}支持自动标记规则的以下属性", - "SupportedDownloadClientsMoreInfo": "若需要查看有关下载客户端的详细信息,请点击“更多信息”按钮。", + "SupportedDownloadClientsMoreInfo": "若需要查看有关下载客户端的详细信息,请点击 “更多信息” 按钮。", "SupportedIndexers": "{appName} 支持任何使用 Newznab 标准的索引器,以及以下列出的其他索引器。", "SupportedListsSeries": "{appName}支持将多个列表中的剧集导入数据库。", "TableOptionsButton": "表格选项按钮", @@ -1372,7 +1372,7 @@ "OneMinute": "1分钟", "RemotePathMappings": "远程路径映射", "RemotePathMappingsLoadError": "无法加载远程路径映射", - "RemoveQueueItemConfirmation": "您确认要从队列中移除 “{sourceTitle}” 吗?", + "RemoveQueueItemConfirmation": "您确定要从队列中移除 “{sourceTitle}” 吗?", "RequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自定义格式。 否则,单个 {implementationName} 匹配就足够了。", "RescanSeriesFolderAfterRefresh": "刷新后重新扫描剧集文件夹", "RestartRequiredToApplyChanges": "{appName}需要重新启动才能应用更改,您想现在重新启动吗?", @@ -1490,27 +1490,27 @@ "DownloadClientDownloadStationValidationSharedFolderMissing": "共享文件夹不存在", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation 上没有名为 “{sharedFolder}” 的共享文件夹,您确定输入正确吗?", "DownloadClientFloodSettingsAdditionalTagsHelpText": "添加媒体属性作为标签。 提示是示例。", - "DownloadClientFloodSettingsRemovalInfo": "{appName} 将根据“设置”->“索引器”中的当前做种规则自动删除种子", + "DownloadClientFloodSettingsRemovalInfo": "{appName} 将根据「“设置”->“索引器”」中的当前做种规则自动删除种子", "DownloadClientFloodSettingsUrlBaseHelpText": "为 Flood API 添加前缀,例如 {url}", "DownloadClientFreeboxApiError": "Freebox API 返回错误:{errorDescription}", "DownloadClientFreeboxNotLoggedIn": "未登录", - "DownloadClientFreeboxSettingsApiUrlHelpText": "使用 API 版本定义 Freebox API 基本 URL,例如“{url}”,默认为“{defaultApiUrl}”", - "DownloadClientFreeboxSettingsAppIdHelpText": "创建访问 Freebox API 所需的 App ID(即“app_id”)", + "DownloadClientFreeboxSettingsApiUrlHelpText": "使用 API 版本定义 Freebox API 基本 URL,例如 “{url}”,默认为 “{defaultApiUrl}”", + "DownloadClientFreeboxSettingsAppIdHelpText": "创建访问 Freebox API 所需的 App ID(即 “app_id”)", "DownloadClientFreeboxSettingsAppId": "App ID", - "DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即“ app_token”)", + "DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)", "DownloadClientFreeboxSettingsPortHelpText": "用于访问 Freebox 接口的端口,默认为 '{port}'", - "DownloadClientFreeboxSettingsHostHelpText": "Freebox 的主机名或主机 IP 地址,默认为“{url}”(仅在同一网络上有效)", + "DownloadClientFreeboxSettingsHostHelpText": "Freebox 的主机名或主机 IP 地址,默认为 “{url}”(仅在同一网络上有效)", "DownloadClientNzbVortexMultipleFilesMessage": "下载包含多个文件且不在作业文件夹中:{outputPath}", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet 已将 KeepHistory 设置为 0。这会阻止 {appName} 查看已完成的下载。", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "按顺序下载文件(qBittorrent 4.1.0+)", "DownloadClientQbittorrentSettingsUseSslHelpText": "使用安全连接。 请参阅 qBittorrent 中的「选项 -> Web UI -> “使用 HTTPS 而不是 HTTP”」。", "DownloadClientQbittorrentTorrentStateError": "qBittorrent 报告错误", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent 正在下载元数据", - "DownloadClientQbittorrentTorrentStatePathError": "无法导入。路径与客户端的基础下载目录匹配,可能是该种子的 “保留顶级文件夹” 功能已禁用,或 “种子内容布局” 未设置为 “原始” 或 “创建子文件夹”?", + "DownloadClientQbittorrentTorrentStatePathError": "无法导入。路径与客户端的基础下载目录匹配,可能是该种子的 “保留顶级文件夹” 功能被禁用,或 “种子内容布局” 未设置为 “原始” 或 “创建子文件夹”?", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} 无法将标签添加到 qBittorrent。", "DownloadClientQbittorrentValidationCategoryRecommended": "推荐分类", "DownloadClientQbittorrentValidationCategoryUnsupported": "不支持分类", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "您的 qBittorrent 设置中未启用 Torrent 队列。 在 qBittorrent 中启用它或选择“最后”作为优先级。", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "您的 qBittorrent 设置中未启用 Torrent 队列。 在 qBittorrent 中启用它或选择 “最后” 作为优先级。", "DownloadClientQbittorrentValidationQueueingNotEnabled": "未启用队列", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent 配置为在达到共享比率限制时删除种子", "DownloadClientRTorrentSettingsAddStopped": "添加后暂停", @@ -1564,7 +1564,7 @@ "IndexerValidationRequestLimitReached": "达到请求限制:{exceptionMessage}", "MonitorNoNewSeasonsDescription": "不自动监控任何新的季", "PasswordConfirmation": "确认密码", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "用于磁力链接的扩展名,默认为“.magnet”", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "用于磁力链接的扩展名,默认为 “.magnet”", "UnknownDownloadState": "未知下载状态:{state}", "DownloadClientSettingsInitialState": "初始状态", "DownloadClientSettingsInitialStateHelpText": "添加到 {clientName} 的种子初始状态", @@ -1598,7 +1598,7 @@ "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因:{errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "API 地址", "DownloadClientFreeboxSettingsAppToken": "App Token", - "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查“主机名”、“端口”或“使用 SSL”的设置(错误: {exceptionMessage})", + "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查 “主机名”、“端口” 或 “使用 SSL” 的设置(错误: {exceptionMessage})", "DownloadClientNzbgetSettingsAddPausedHelpText": "此选项至少需要 NzbGet 版本 16.0", "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGet 设置 KeepHistory 应小于 25000", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGet 设置 KeepHistory 设置得太高。", @@ -1614,10 +1614,10 @@ "DownloadClientQbittorrentTorrentStateUnknown": "未知下载状态:{state}", "DownloadClientQbittorrentValidationCategoryAddFailure": "分类配置失败", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "qBittorrent 版本 3.3.0 之前不支持分类。 请升级后重试或者留空。", - "DownloadClientRTorrentProviderMessage": "当种子满足做种规则时,rTorrent 不会暂停做种。 仅当启用 “删除已完成项” 时,{appName} 才会根据 “设置 -> 索引器” 中的当前做种规则自动删除种子。 导入后,它还会将 {importedView} 设置为 rTorrent 视图,可以使用 rTorrent 脚本来自定义行为。", + "DownloadClientRTorrentProviderMessage": "当种子满足做种规则时,rTorrent 不会暂停做种。 仅当启用 “删除已完成项” 时,{appName} 才会根据「“设置 -> 索引器”」中的当前做种规则自动删除种子。 导入后,它还会将 {importedView} 设置为 rTorrent 视图,可以使用 rTorrent 脚本来自定义行为。", "DownloadClientRTorrentSettingsAddStoppedHelpText": "启用将在停止状态下向 rTorrent 添加 torrent 和磁力链接。 这可能会破坏磁力文件。", "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC 端点的路径,请参阅 {url}。 使用 ruTorrent 时,这通常是 RPC2 或 [ruTorrent 路径]{url2}。", - "DownloadClientSabnzbdValidationCheckBeforeDownload": "禁用 Sabnbzd 中的“下载前检查”选项", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "禁用 Sabnbzd 中的 “下载前检查” 选项", "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "您必须对 {appName} 使用的类别禁用电视节目排序,以防止出现导入问题。 去 Sabnzbd 修复它。", "DownloadClientSabnzbdValidationEnableJobFolders": "启用工作文件夹", "DownloadClientSettingsAddPaused": "添加并暂停", @@ -1671,7 +1671,7 @@ "BlackholeWatchFolder": "监视文件夹", "BlackholeWatchFolderHelpText": "{appName} 用来导入已完成下载的文件夹", "BlackholeFolderHelpText": "{appName} 将在其中存储 {extension} 文件的文件夹", - "DownloadClientFreeboxUnableToReachFreeboxApi": "无法访问 Freebox API。 请检查“API 地址”的基础地址和版本。", + "DownloadClientFreeboxUnableToReachFreeboxApi": "无法访问 Freebox API。 请检查 “API 地址” 的基础地址和版本。", "IndexerSettingsAllowZeroSize": "允许零大小", "IndexerValidationNoRssFeedQueryAvailable": "没有查询到可用的 RSS 订阅源。 这可能是索引器或索引器分类设置的问题。", "DownloadClientSabnzbdValidationDevelopVersionDetail": "运行开发版本时,{appName} 可能无法支持 SABnzbd 添加的新功能。", @@ -1686,7 +1686,7 @@ "ClearBlocklistMessageText": "您确认要将黑名单中的所有项目清空吗?", "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} 不会尝试导入未分类的已完成下载。", "DownloadClientSettingsPostImportCategoryHelpText": "导入下载后要设置的 {appName} 的分类。 即使做种完成,{appName} 也不会删除该分类中的种子。 留空以保留同一分类。", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} 将无法按照 “已完成下载处理” 配置执行。 您可以在 qBittorrent 中修复此问题(菜单中的 “工具 -> 选项...”),通过将 “选项 -> BitTorrent -> 分享率限制” 从 “删除种子” 更改为 “暂停种子”", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} 将无法按照 “已完成下载处理” 配置执行。 您可以在 qBittorrent 中修复此问题(菜单中的「 “工具 -> 选项...”」),通过将「“选项 -> BitTorrent -> 分享率限制”」从 “删除种子” 更改为 “暂停种子”", "DownloadClientDelugeTorrentStateError": "Deluge 正在报告错误", "TorrentBlackholeSaveMagnetFilesExtension": "保存磁力链接文件扩展名", "DownloadClientDelugeValidationLabelPluginFailure": "标签配置失败", @@ -1763,7 +1763,7 @@ "NotificationsPushcutSettingsApiKeyHelpText": "API Key 可在 Pushcut app 的账号视图中管理", "NotificationsPushcutSettingsNotificationName": "通知名称", "NotificationsPushcutSettingsNotificationNameHelpText": "Pushcut app 通知页中的通知名称", - "NotificationsJoinSettingsDeviceIdsHelpText": "弃用,请改用“设备名称”。通知要发给的设备的 ID,以括号分隔。留空则发给所有设备。", + "NotificationsJoinSettingsDeviceIdsHelpText": "弃用,请改用 “设备名称”。通知要发给的设备的 ID,以括号分隔。留空则发给所有设备。", "NotificationsAppriseSettingsConfigurationKeyHelpText": "持久化存储方案的配置键名,若使用无状态 URL 则留空。", "NotificationsAppriseSettingsStatelessUrls": "Apprise 的无状态 URL", "NotificationsAppriseSettingsTags": "Apprise 标签", @@ -1793,17 +1793,17 @@ "BlocklistMultipleOnlyHint": "无需搜索替换的黑名单", "BlocklistOnly": "仅限黑名单", "BlocklistOnlyHint": "无需一次搜索替换的黑名单", - "ChangeCategoryMultipleHint": "将下载从下载客户端更改为“导入后类别”", + "ChangeCategoryMultipleHint": "将下载客户端中的下载内容更改为 “导入后类别”", "CustomFormatsSpecificationRegularExpressionHelpText": "自定义格式正则表达式不区分大小写", "CustomFormatsSpecificationRegularExpression": "正则表达式", "RemoveFromDownloadClientHint": "从下载客户端中移除下载记录和文件", "RemoveMultipleFromDownloadClientHint": "从下载客户端删除下载和文件", "RemoveQueueItemsRemovalMethodHelpTextWarning": "“从下载客户端中移除” 将从下载客户端中移除下载记录和文件。", "BlocklistAndSearchHint": "列入黑名单后开始一次搜索替换", - "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", + "ChangeCategoryHint": "将下载客户端中的下载内容更改为 “导入后类别”", "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "即使种子的散列值被列入黑名单,某些索引器在 RSS 同步或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取种子之后但在将其发送到下载客户端之前拒绝它。", - "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端中移除”将从下载客户端中移除下载记录和文件。", + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端中移除” 将从下载客户端中移除下载记录和文件。", "AutoTaggingSpecificationOriginalLanguage": "语言", "CustomFormatsSpecificationFlag": "标记", "CustomFormatsSpecificationLanguage": "语言", @@ -1849,7 +1849,7 @@ "NotificationsSettingsWebhookMethodHelpText": "向 Web 服务提交数据时使用哪种 HTTP 方法", "NotificationsTelegramSettingsTopicId": "主题 ID", "NotificationsValidationInvalidApiKeyExceptionMessage": "API 密钥无效: {exceptionMessage}", - "NotificationsDiscordSettingsOnGrabFieldsHelpText": "更改用于“抓取”通知的字段", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "更改用于 “抓取” 通知的字段", "NotificationsGotifySettingsAppToken": "App Token", "NotificationsPushoverSettingsRetry": "重试", "NotificationsPushoverSettingsDevices": "设备", @@ -1901,7 +1901,7 @@ "NoBlocklistItems": "无黑名单项目", "DownloadClientDelugeSettingsDirectoryHelpText": "下载位置可选择,留空使用 Deluge 默认位置", "IndexerSettingsMultiLanguageReleaseHelpText": "此索引器的多版本中通常包含哪些语言?", - "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "更改用于“手动操作”通知的字段", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "更改用于 “手动操作” 通知的字段", "NotificationsTwitterSettingsConsumerSecret": "Consumer Secret", "NotificationsTwitterSettingsDirectMessage": "私信", "NotificationsTwitterSettingsDirectMessageHelpText": "发送私信而非公共消息", @@ -1938,7 +1938,7 @@ "NotificationsDiscordSettingsOnGrabFields": "抓取时字段", "LogSizeLimit": "日志大小限制", "LogSizeLimitHelpText": "存档前的最大日志文件大小(MB)。默认值为 1 MB。", - "NotificationsDiscordSettingsOnImportFieldsHelpText": "更改用于“导入”通知的字段", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "更改用于 “导入” 通知的字段", "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", "NotificationsPushoverSettingsRetryHelpText": "紧急警报的重试间隔,最少 30 秒", "NotificationsSlackSettingsIconHelpText": "更改用于发送到 Slack 的消息图标(表情符号或 URL)", @@ -1950,5 +1950,9 @@ "SkipFreeSpaceCheckHelpText": "当 {appName} 无法检测到根目录的剩余空间时使用", "MinimumCustomFormatScoreIncrement": "自定义格式分数最小增量", "MinimumCustomFormatScoreIncrementHelpText": "{appName} 将新版本视为升级版本之前,新版本资源相较于现有版本在自定义格式分数上的最小提升", - "LastSearched": "最近搜索" + "LastSearched": "最近搜索", + "InstallMajorVersionUpdateMessageLink": "请查看 [{domain}]({url}) 以获取更多信息。", + "Install": "安装", + "InstallMajorVersionUpdate": "安装更新", + "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?" } From 135b5c2ddd8f0a274b0d59eb07f75aaf1446b9da Mon Sep 17 00:00:00 2001 From: Hadrien Patte Date: Sat, 26 Oct 2024 23:14:20 +0200 Subject: [PATCH 591/762] Use `OperatingSystem` class to get OS information --- src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs | 89 ++++--------------- 1 file changed, 19 insertions(+), 70 deletions(-) diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index f37b40fec..aece27859 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using NLog; @@ -25,22 +24,25 @@ namespace NzbDrone.Common.EnvironmentInfo static OsInfo() { - var platform = Environment.OSVersion.Platform; - - switch (platform) + if (OperatingSystem.IsWindows()) { - case PlatformID.Win32NT: - { - Os = Os.Windows; - break; - } - - case PlatformID.MacOSX: - case PlatformID.Unix: - { - Os = GetPosixFlavour(); - break; - } + Os = Os.Windows; + } + else if (OperatingSystem.IsMacOS()) + { + Os = Os.Osx; + } + else if (OperatingSystem.IsFreeBSD()) + { + Os = Os.Bsd; + } + else + { +#if ISMUSL + Os = Os.LinuxMusl; +#else + Os = Os.Linux; +#endif } } @@ -84,59 +86,6 @@ namespace NzbDrone.Common.EnvironmentInfo IsDocker = true; } } - - private static Os GetPosixFlavour() - { - var output = RunAndCapture("uname", "-s"); - - if (output.StartsWith("Darwin")) - { - return Os.Osx; - } - else if (output.Contains("BSD")) - { - return Os.Bsd; - } - else - { -#if ISMUSL - return Os.LinuxMusl; -#else - return Os.Linux; -#endif - } - } - - private static string RunAndCapture(string filename, string args) - { - var processStartInfo = new ProcessStartInfo - { - FileName = filename, - Arguments = args, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true - }; - - var output = string.Empty; - - try - { - using (var p = Process.Start(processStartInfo)) - { - // To avoid deadlocks, always read the output stream first and then wait. - output = p.StandardOutput.ReadToEnd(); - - p.WaitForExit(1000); - } - } - catch (Exception) - { - output = string.Empty; - } - - return output; - } } public interface IOsInfo From 07374de74788de1532a181380204fc6b4b528bc6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 27 Oct 2024 00:14:58 +0300 Subject: [PATCH 592/762] Fixed: Matched alternative titles and tags in series search results --- frontend/src/Components/Page/Header/SeriesSearchResult.js | 4 ++-- frontend/src/Components/Page/Header/fuse.worker.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Components/Page/Header/SeriesSearchResult.js b/frontend/src/Components/Page/Header/SeriesSearchResult.js index 8a9c35a73..6d1c76416 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchResult.js +++ b/frontend/src/Components/Page/Header/SeriesSearchResult.js @@ -22,9 +22,9 @@ function SeriesSearchResult(props) { let tag = null; if (match.key === 'alternateTitles.title') { - alternateTitle = alternateTitles[match.arrayIndex]; + alternateTitle = alternateTitles[match.refIndex]; } else if (match.key === 'tags.label') { - tag = tags[match.arrayIndex]; + tag = tags[match.refIndex]; } return ( diff --git a/frontend/src/Components/Page/Header/fuse.worker.js b/frontend/src/Components/Page/Header/fuse.worker.js index 23a070617..154607038 100644 --- a/frontend/src/Components/Page/Header/fuse.worker.js +++ b/frontend/src/Components/Page/Header/fuse.worker.js @@ -37,7 +37,7 @@ function getSuggestions(series, value) { key: 'title' } ], - arrayIndex: 0 + refIndex: 0 }); if (suggestions.length > limit) { break; From fe40d83aa4e671260a5dade1dcc5670b54dc8e48 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 27 Oct 2024 00:15:27 +0300 Subject: [PATCH 593/762] Fixed: Dedupe releases for single daily and anime episode searches Closes #7288 --- src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 10b9fc0fb..1a6cf9a9c 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -339,7 +339,9 @@ namespace NzbDrone.Core.IndexerSearch var searchSpec = Get(series, new List { episode }, monitoredOnly, userInvokedSearch, interactiveSearch); searchSpec.AirDate = airDate; - return await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + var downloadDecisions = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + + return DeDupeDecisions(downloadDecisions); } private async Task> SearchAnime(Series series, Episode episode, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch, bool isSeasonSearch = false) @@ -352,7 +354,9 @@ namespace NzbDrone.Core.IndexerSearch searchSpec.EpisodeNumber = episode.SceneEpisodeNumber ?? episode.EpisodeNumber; searchSpec.AbsoluteEpisodeNumber = episode.SceneAbsoluteEpisodeNumber ?? episode.AbsoluteEpisodeNumber ?? 0; - return await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + var downloadDecisions = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + + return DeDupeDecisions(downloadDecisions); } private async Task> SearchSpecial(Series series, List episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) From f502eaffe320caffbb6364d9baf01b6b62a737d0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 10 Oct 2024 18:44:31 +0300 Subject: [PATCH 594/762] Bump frontend packages --- package.json | 98 +- yarn.lock | 3191 +++++++++++++++++++++++--------------------------- 2 files changed, 1528 insertions(+), 1761 deletions(-) diff --git a/package.json b/package.json index d3af32a44..cff0e309d 100644 --- a/package.json +++ b/package.json @@ -21,42 +21,42 @@ "defaults" ], "dependencies": { - "@fortawesome/fontawesome-free": "6.4.0", - "@fortawesome/fontawesome-svg-core": "6.4.0", - "@fortawesome/free-regular-svg-icons": "6.4.0", - "@fortawesome/free-solid-svg-icons": "6.4.0", - "@fortawesome/react-fontawesome": "0.2.0", + "@fortawesome/fontawesome-free": "6.6.0", + "@fortawesome/fontawesome-svg-core": "6.6.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", + "@fortawesome/free-solid-svg-icons": "6.6.0", + "@fortawesome/react-fontawesome": "0.2.2", "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", - "@sentry/browser": "7.100.0", - "@types/node": "18.19.31", + "@sentry/browser": "7.119.1", + "@types/node": "20.16.11", "@types/react": "18.2.79", "@types/react-dom": "18.2.25", - "classnames": "2.3.2", + "classnames": "2.5.1", "connected-react-router": "6.9.3", "copy-to-clipboard": "3.3.3", "element-class": "0.2.2", - "filesize": "10.0.7", + "filesize": "10.1.6", "fuse.js": "6.6.2", - "history": "4.9.0", + "history": "4.10.1", "jdu": "1.0.0", - "jquery": "3.7.0", + "jquery": "3.7.1", "lodash": "4.17.21", "mobile-detect": "1.4.5", - "moment": "2.29.4", + "moment": "2.30.1", "mousetrap": "1.6.5", "normalize.css": "8.0.1", "prop-types": "15.8.1", - "qs": "6.11.1", + "qs": "6.13.0", "react": "17.0.2", "react-addons-shallow-compare": "15.6.3", "react-async-script": "1.2.0", "react-autosuggest": "10.1.0", "react-custom-scrollbars-2": "4.5.0", - "react-dnd": "14.0.2", - "react-dnd-html5-backend": "14.0.0", + "react-dnd": "14.0.4", + "react-dnd-html5-backend": "14.0.2", "react-dnd-multi-backend": "6.0.2", - "react-dnd-touch-backend": "14.0.0", + "react-dnd-touch-backend": "14.1.1", "react-document-title": "2.0.3", "react-dom": "17.0.2", "react-focus-lock": "2.9.4", @@ -64,16 +64,16 @@ "react-lazyload": "3.2.0", "react-measure": "1.4.7", "react-middle-truncate": "1.0.3", - "react-popper": "1.3.3", + "react-popper": "1.3.7", "react-redux": "7.2.4", "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-slider": "1.1.4", "react-tabs": "4.3.0", - "react-text-truncate": "0.18.0", + "react-text-truncate": "0.19.0", "react-use-measure": "2.1.1", "react-virtualized": "9.21.1", - "react-window": "1.8.9", + "react-window": "1.8.10", "redux": "4.2.1", "redux-actions": "2.6.5", "redux-batched-actions": "0.5.0", @@ -84,46 +84,46 @@ "typescript": "5.1.6" }, "devDependencies": { - "@babel/core": "7.25.2", - "@babel/eslint-parser": "7.25.1", - "@babel/plugin-proposal-export-default-from": "7.24.7", + "@babel/core": "7.25.8", + "@babel/eslint-parser": "7.25.8", + "@babel/plugin-proposal-export-default-from": "7.25.8", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.25.3", - "@babel/preset-react": "7.24.7", - "@babel/preset-typescript": "7.24.7", - "@types/lodash": "4.14.194", - "@types/qs": "6.9.15", - "@types/react-document-title": "2.0.9", - "@types/react-lazyload": "3.2.0", + "@babel/preset-env": "7.25.8", + "@babel/preset-react": "7.25.7", + "@babel/preset-typescript": "7.25.7", + "@types/lodash": "4.14.195", + "@types/qs": "6.9.16", + "@types/react-document-title": "2.0.10", + "@types/react-lazyload": "3.2.3", "@types/react-router-dom": "5.3.3", - "@types/react-text-truncate": "0.14.1", - "@types/react-window": "1.8.5", - "@types/redux-actions": "2.6.2", - "@types/webpack-livereload-plugin": "^2.3.3", + "@types/react-text-truncate": "0.19.0", + "@types/react-window": "1.8.8", + "@types/redux-actions": "2.6.5", + "@types/webpack-livereload-plugin": "2.3.6", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "autoprefixer": "10.4.20", - "babel-loader": "9.1.3", + "babel-loader": "9.2.1", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.38.0", + "core-js": "3.38.1", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", - "eslint": "8.57.0", + "eslint": "8.57.1", "eslint-config-prettier": "8.10.0", "eslint-plugin-filenames": "1.3.2", - "eslint-plugin-import": "2.29.1", + "eslint-plugin-import": "2.31.0", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.34.1", - "eslint-plugin-react-hooks": "4.6.0", - "eslint-plugin-simple-import-sort": "12.1.0", + "eslint-plugin-react": "7.37.1", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-simple-import-sort": "12.1.1", "file-loader": "6.2.0", "filemanager-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0", - "html-webpack-plugin": "5.5.1", + "html-webpack-plugin": "5.6.0", "loader-utils": "^3.2.1", - "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.41", + "mini-css-extract-plugin": "2.9.1", + "postcss": "8.4.47", "postcss-color-function": "4.1.0", "postcss-loader": "7.3.0", "postcss-mixins": "9.0.4", @@ -132,16 +132,16 @@ "postcss-url": "10.1.3", "prettier": "2.8.8", "require-nocache": "1.0.0", - "rimraf": "4.4.1", + "rimraf": "6.0.1", "style-loader": "3.3.2", "stylelint": "15.6.1", - "stylelint-order": "6.0.3", - "terser-webpack-plugin": "5.3.8", - "ts-loader": "9.4.2", + "stylelint-order": "6.0.4", + "terser-webpack-plugin": "5.3.10", + "ts-loader": "9.5.1", "typescript-plugin-css-modules": "5.0.1", "url-loader": "4.1.1", - "webpack": "5.82.1", - "webpack-cli": "5.1.1", + "webpack": "5.95.0", + "webpack-cli": "5.1.4", "webpack-livereload-plugin": "3.0.2", "worker-loader": "3.0.8" }, diff --git a/yarn.lock b/yarn.lock index 4a387bed4..2870d9142 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,10 @@ # yarn lockfile v1 -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - "@adobe/css-tools@^4.0.1": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" - integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== "@ampproject/remapping@^2.2.0": version "2.3.0" @@ -20,151 +15,111 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" - integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" + integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== dependencies: - "@babel/highlight" "^7.24.2" + "@babel/highlight" "^7.25.7" picocolors "^1.0.0" -"@babel/code-frame@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== - dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" + integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" - integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== - -"@babel/compat-data@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" - integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== - -"@babel/core@7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" - integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== +"@babel/core@7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6" + integrity sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.0" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-module-transforms" "^7.25.2" - "@babel/helpers" "^7.25.0" - "@babel/parser" "^7.25.0" - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.2" - "@babel/types" "^7.25.2" + "@babel/code-frame" "^7.25.7" + "@babel/generator" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helpers" "^7.25.7" + "@babel/parser" "^7.25.8" + "@babel/template" "^7.25.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.8" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@7.25.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz#469cee4bd18a88ff3edbdfbd227bd20e82aa9b82" - integrity sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg== +"@babel/eslint-parser@7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.8.tgz#0119dec46be547d7a339978dedb9d29e517c2443" + integrity sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" - integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== +"@babel/generator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" + integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== dependencies: - "@babel/types" "^7.25.0" + "@babel/types" "^7.25.7" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^2.5.1" + jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== +"@babel/helper-annotate-as-pure@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" + integrity sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.25.7" -"@babel/helper-annotate-as-pure@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" - integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" + integrity sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg== dependencies: - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" - integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" + integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-compilation-targets@^7.22.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" - integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== - dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-validator-option" "^7.23.5" - browserslist "^4.22.2" + "@babel/compat-data" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" - integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== +"@babel/helper-create-class-features-plugin@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" + integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== dependencies: - "@babel/compat-data" "^7.25.2" - "@babel/helper-validator-option" "^7.24.8" - browserslist "^4.23.1" - lru-cache "^5.1.1" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-member-expression-to-functions" "^7.25.7" + "@babel/helper-optimise-call-expression" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/traverse" "^7.25.7" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz#a109bf9c3d58dfed83aaf42e85633c89f43a6253" - integrity sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" + integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/traverse" "^7.25.0" + "@babel/helper-annotate-as-pure" "^7.25.7" + regexpu-core "^6.1.1" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" - integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" - integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" - integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== +"@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -172,348 +127,212 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-member-expression-to-functions@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" - integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== +"@babel/helper-member-expression-to-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" + integrity sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA== dependencies: - "@babel/traverse" "^7.24.8" - "@babel/types" "^7.24.8" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-module-imports@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" - integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== +"@babel/helper-module-imports@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472" + integrity sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" - integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== +"@babel/helper-module-transforms@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz#2ac9372c5e001b19bc62f1fe7d96a18cb0901d1a" + integrity sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-simple-access" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.2" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-simple-access" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-optimise-call-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" - integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== +"@babel/helper-optimise-call-expression@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz#1de1b99688e987af723eed44fa7fc0ee7b97d77a" + integrity sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng== dependencies: - "@babel/types" "^7.24.7" + "@babel/types" "^7.25.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a" - integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" + integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== -"@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" - integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== - -"@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" - integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== +"@babel/helper-remap-async-to-generator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" + integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-wrap-function" "^7.25.0" - "@babel/traverse" "^7.25.0" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-wrap-function" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" - integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== +"@babel/helper-replace-supers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" + integrity sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/traverse" "^7.25.0" + "@babel/helper-member-expression-to-functions" "^7.25.7" + "@babel/helper-optimise-call-expression" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-simple-access@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" - integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== +"@babel/helper-simple-access@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0" + integrity sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" - integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== +"@babel/helper-skip-transparent-expression-wrappers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz#382831c91038b1a6d32643f5f49505b8442cb87c" + integrity sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-string-parser@^7.23.4": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" - integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== +"@babel/helper-string-parser@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" + integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== -"@babel/helper-string-parser@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" - integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== +"@babel/helper-validator-identifier@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" + integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-option@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" + integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== -"@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== - -"@babel/helper-validator-option@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" - integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== - -"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" - integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== - -"@babel/helper-wrap-function@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" - integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== +"@babel/helper-wrap-function@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz#9f6021dd1c4fdf4ad515c809967fc4bac9a70fe7" + integrity sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg== dependencies: - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/template" "^7.25.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helpers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" - integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== +"@babel/helpers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2" + integrity sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA== dependencies: - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/template" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/highlight@^7.24.2": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" - integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== +"@babel/highlight@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" + integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== dependencies: - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-validator-identifier" "^7.25.7" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== +"@babel/parser@^7.25.7", "@babel/parser@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2" + integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ== dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" + "@babel/types" "^7.25.8" -"@babel/parser@^7.25.0", "@babel/parser@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" - integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz#93969ac50ef4d68b2504b01b758af714e4cbdd64" + integrity sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ== dependencies: - "@babel/types" "^7.25.2" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" - integrity sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz#a338d611adb9dcd599b8b1efa200c88ebeffe046" + integrity sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz#cd0c583e01369ef51676bdb3d7b603e17d2b3f73" - integrity sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" + integrity sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" - integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz#3b7ea04492ded990978b6deaa1dfca120ad4455a" + integrity sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/plugin-transform-optional-chaining" "^7.25.7" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" - integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz#9622b1d597a703aa3a921e6f58c9c2d9a028d2c5" + integrity sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz#3a82a70e7cb7294ad2559465ebcb871dfbf078fb" - integrity sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw== +"@babel/plugin-proposal-export-default-from@7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.8.tgz#fa22151caa240683c3659796037237813767f348" + integrity sha512-5SLPHA/Gk7lNdaymtSVS9jH77Cs7yuHTR3dYj+9q+M7R7tNLXhNuvnmOfafRIzpWL+dtMibuu1I4ofrc768Gkw== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.0" - -"@babel/plugin-proposal-export-default-from@7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.7.tgz#0b539c46b8ac804f694e338f803c8354c0f788b6" - integrity sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@7.8.3", "@babel/plugin-syntax-dynamic-import@^7.8.3": +"@babel/plugin-syntax-dynamic-import@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-default-from@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.7.tgz#85dae9098933573aae137fb52141dd3ca52ae7ac" - integrity sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw== +"@babel/plugin-syntax-import-assertions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz#8ce248f9f4ed4b7ed4cb2e0eb4ed9efd9f52921f" + integrity sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== +"@babel/plugin-syntax-import-attributes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz#d78dd0499d30df19a598e63ab895e21b909bc43f" + integrity sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-import-assertions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" - integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== +"@babel/plugin-syntax-jsx@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz#5352d398d11ea5e7ef330c854dea1dae0bf18165" + integrity sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" - integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== +"@babel/plugin-syntax-typescript@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz#bfc05b0cc31ebd8af09964650cee723bb228108b" + integrity sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" - integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" - integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -523,530 +342,503 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" - integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== +"@babel/plugin-transform-arrow-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz#1b9ed22e6890a0e9ff470371c73b8c749bcec386" + integrity sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-async-generator-functions@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz#b785cf35d73437f6276b1e30439a57a50747bddf" - integrity sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q== +"@babel/plugin-transform-async-generator-functions@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz#3331de02f52cc1f2c75b396bec52188c85b0b1ec" + integrity sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-remap-async-to-generator" "^7.25.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/traverse" "^7.25.0" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-remap-async-to-generator" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" - integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== +"@babel/plugin-transform-async-to-generator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz#a44c7323f8d4285a6c568dd43c5c361d6367ec52" + integrity sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-remap-async-to-generator" "^7.24.7" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-remap-async-to-generator" "^7.25.7" -"@babel/plugin-transform-block-scoped-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" - integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== +"@babel/plugin-transform-block-scoped-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz#e0b8843d5571719a2f1bf7e284117a3379fcc17c" + integrity sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-block-scoping@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" - integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== +"@babel/plugin-transform-block-scoping@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz#6dab95e98adf780ceef1b1c3ab0e55cd20dd410a" + integrity sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-class-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" - integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== +"@babel/plugin-transform-class-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" + integrity sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-class-static-block@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" - integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== +"@babel/plugin-transform-class-static-block@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz#a8af22028920fe404668031eceb4c3aadccb5262" + integrity sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-classes@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz#63122366527d88e0ef61b612554fe3f8c793991e" - integrity sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw== +"@babel/plugin-transform-classes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz#5103206cf80d02283bbbd044509ea3b65d0906bb" + integrity sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/traverse" "^7.25.0" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" + "@babel/traverse" "^7.25.7" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" - integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== +"@babel/plugin-transform-computed-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz#7f621f0aa1354b5348a935ab12e3903842466f65" + integrity sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/template" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/template" "^7.25.7" -"@babel/plugin-transform-destructuring@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" - integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== +"@babel/plugin-transform-destructuring@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz#f6f26a9feefb5aa41fd45b6f5838901b5333d560" + integrity sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-dotall-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" - integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== +"@babel/plugin-transform-dotall-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz#9d775c4a3ff1aea64045300fcd4309b4a610ef02" + integrity sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-duplicate-keys@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" - integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== +"@babel/plugin-transform-duplicate-keys@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz#fbba7d1155eab76bd4f2a038cbd5d65883bd7a93" + integrity sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz#809af7e3339466b49c034c683964ee8afb3e2604" - integrity sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz#102b31608dcc22c08fbca1894e104686029dc141" + integrity sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-dynamic-import@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" - integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== +"@babel/plugin-transform-dynamic-import@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz#f1edbe75b248cf44c70c8ca8ed3818a668753aaa" + integrity sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-exponentiation-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" - integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== +"@babel/plugin-transform-exponentiation-operator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz#5961a3a23a398faccd6cddb34a2182807d75fb5f" + integrity sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-export-namespace-from@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" - integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== +"@babel/plugin-transform-export-namespace-from@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz#d1988c3019a380b417e0516418b02804d3858145" + integrity sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-for-of@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" - integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== +"@babel/plugin-transform-for-of@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz#0acfea0f27aa290818b5b48a5a44b3f03fc13669" + integrity sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-function-name@^7.25.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" - integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== +"@babel/plugin-transform-function-name@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz#7e394ccea3693902a8b50ded8b6ae1fa7b8519fd" + integrity sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ== dependencies: - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.1" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-json-strings@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" - integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== +"@babel/plugin-transform-json-strings@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz#6fb3ec383a2ea92652289fdba653e3f9de722694" + integrity sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-literals@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" - integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== +"@babel/plugin-transform-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz#70cbdc742f2cfdb1a63ea2cbd018d12a60b213c3" + integrity sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-logical-assignment-operators@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" - integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== +"@babel/plugin-transform-logical-assignment-operators@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz#01868ff92daa9e525b4c7902aa51979082a05710" + integrity sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-member-expression-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" - integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== +"@babel/plugin-transform-member-expression-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz#0a36c3fbd450cc9e6485c507f005fa3d1bc8fca5" + integrity sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-amd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" - integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== +"@babel/plugin-transform-modules-amd@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz#bb4e543b5611f6c8c685a2fd485408713a3adf3d" + integrity sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA== dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" - integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== +"@babel/plugin-transform-modules-commonjs@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz#173f0c791bb7407c092ce6d77ee90eb3f2d1d2fd" + integrity sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg== dependencies: - "@babel/helper-module-transforms" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-simple-access" "^7.25.7" -"@babel/plugin-transform-modules-systemjs@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz#8f46cdc5f9e5af74f3bd019485a6cbe59685ea33" - integrity sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw== +"@babel/plugin-transform-modules-systemjs@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz#8b14d319a177cc9c85ef8b0512afd429d9e2e60b" + integrity sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g== dependencies: - "@babel/helper-module-transforms" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.0" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-modules-umd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" - integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== +"@babel/plugin-transform-modules-umd@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz#00ee7a7e124289549381bfb0e24d87fd7f848367" + integrity sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw== dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" - integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz#a2f3f6d7f38693b462542951748f0a72a34d196d" + integrity sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-new-target@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" - integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== +"@babel/plugin-transform-new-target@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz#52b2bde523b76c548749f38dc3054f1f45e82bc9" + integrity sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" - integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz#befb4900c130bd52fccf2b926314557987f1b552" + integrity sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-numeric-separator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" - integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== +"@babel/plugin-transform-numeric-separator@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz#91e370486371637bd42161052f2602c701386891" + integrity sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-object-rest-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" - integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== +"@babel/plugin-transform-object-rest-spread@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz#0904ac16bcce41df4db12d915d6780f85c7fb04b" + integrity sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g== dependencies: - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-transform-parameters" "^7.25.7" -"@babel/plugin-transform-object-super@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" - integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== +"@babel/plugin-transform-object-super@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz#582a9cea8cf0a1e02732be5b5a703a38dedf5661" + integrity sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" -"@babel/plugin-transform-optional-catch-binding@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" - integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== +"@babel/plugin-transform-optional-catch-binding@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz#2649b86a3bb202c6894ec81a6ddf41b94d8f3103" + integrity sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" - integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== +"@babel/plugin-transform-optional-chaining@^7.25.7", "@babel/plugin-transform-optional-chaining@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz#f46283b78adcc5b6ab988a952f989e7dce70653f" + integrity sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-parameters@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" - integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== +"@babel/plugin-transform-parameters@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" + integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-private-methods@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" - integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== +"@babel/plugin-transform-private-methods@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" + integrity sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-private-property-in-object@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" - integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== +"@babel/plugin-transform-private-property-in-object@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz#1234f856ce85e061f9688764194e51ea7577c434" + integrity sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-property-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" - integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== +"@babel/plugin-transform-property-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz#a8612b4ea4e10430f00012ecf0155662c7d6550d" + integrity sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-react-display-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" - integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== +"@babel/plugin-transform-react-display-name@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz#2753e875a1b702fb1d806c4f5d4c194d64cadd88" + integrity sha512-r0QY7NVU8OnrwE+w2IWiRom0wwsTbjx4+xH2RTd7AVdof3uurXOF+/mXHQDRk+2jIvWgSaCHKMgggfvM4dyUGA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-react-jsx-development@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" - integrity sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ== +"@babel/plugin-transform-react-jsx-development@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.7.tgz#2fbd77887b8fa2942d7cb61edf1029ea1b048554" + integrity sha512-5yd3lH1PWxzW6IZj+p+Y4OLQzz0/LzlOG8vGqonHfVR3euf1vyzyMUJk9Ac+m97BH46mFc/98t9PmYLyvgL3qg== dependencies: - "@babel/plugin-transform-react-jsx" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.25.7" -"@babel/plugin-transform-react-jsx@^7.24.7": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz#e37e8ebfa77e9f0b16ba07fadcb6adb47412227a" - integrity sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA== +"@babel/plugin-transform-react-jsx@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz#f5e2af6020a562fe048dd343e571c4428e6c5632" + integrity sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/types" "^7.25.2" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-syntax-jsx" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/plugin-transform-react-pure-annotations@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" - integrity sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA== +"@babel/plugin-transform-react-pure-annotations@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.7.tgz#6d0b8dadb2d3c5cbb8ade68c5efd49470b0d65f7" + integrity sha512-6YTHJ7yjjgYqGc8S+CbEXhLICODk0Tn92j+vNJo07HFk9t3bjFgAKxPLFhHwF2NjmQVSI1zBRfBWUeVBa2osfA== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-regenerator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" - integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== +"@babel/plugin-transform-regenerator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz#6eb006e6d26f627bc2f7844a9f19770721ad6f3e" + integrity sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" - integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== +"@babel/plugin-transform-reserved-words@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz#dc56b25e02afaabef3ce0c5b06b0916e8523e995" + integrity sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-shorthand-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" - integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== +"@babel/plugin-transform-shorthand-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz#92690a9c671915602d91533c278cc8f6bf12275f" + integrity sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" - integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== +"@babel/plugin-transform-spread@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz#df83e899a9fc66284ee601a7b738568435b92998" + integrity sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-sticky-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" - integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== +"@babel/plugin-transform-sticky-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz#341c7002bef7f29037be7fb9684e374442dd0d17" + integrity sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-template-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" - integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== +"@babel/plugin-transform-template-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz#e566c581bb16d8541dd8701093bb3457adfce16b" + integrity sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-typeof-symbol@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" - integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== +"@babel/plugin-transform-typeof-symbol@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz#debb1287182efd20488f126be343328c679b66eb" + integrity sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-typescript@^7.24.7": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" - integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== +"@babel/plugin-transform-typescript@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz#8fc7c3d28ddd36bce45b9b48594129d0e560cfbe" + integrity sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-typescript" "^7.24.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/plugin-syntax-typescript" "^7.25.7" -"@babel/plugin-transform-unicode-escapes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" - integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== +"@babel/plugin-transform-unicode-escapes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz#973592b6d13a914794e1de8cf1383e50e0f87f81" + integrity sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-unicode-property-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" - integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== +"@babel/plugin-transform-unicode-property-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz#25349197cce964b1343f74fa7cfdf791a1b1919e" + integrity sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-unicode-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" - integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== +"@babel/plugin-transform-unicode-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz#f93a93441baf61f713b6d5552aaa856bfab34809" + integrity sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-unicode-sets-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" - integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== +"@babel/plugin-transform-unicode-sets-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz#d1b3295d29e0f8f4df76abc909ad1ebee919560c" + integrity sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/preset-env@7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.3.tgz#0bf4769d84ac51d1073ab4a86f00f30a3a83c67c" - integrity sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g== +"@babel/preset-env@7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.8.tgz#dc6b719627fb29cd9cccbbbe041802fd575b524c" + integrity sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg== dependencies: - "@babel/compat-data" "^7.25.2" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-validator-option" "^7.24.8" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.3" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.0" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.0" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.0" + "@babel/compat-data" "^7.25.8" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.7" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.7" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.24.7" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-import-assertions" "^7.25.7" + "@babel/plugin-syntax-import-attributes" "^7.25.7" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.24.7" - "@babel/plugin-transform-async-generator-functions" "^7.25.0" - "@babel/plugin-transform-async-to-generator" "^7.24.7" - "@babel/plugin-transform-block-scoped-functions" "^7.24.7" - "@babel/plugin-transform-block-scoping" "^7.25.0" - "@babel/plugin-transform-class-properties" "^7.24.7" - "@babel/plugin-transform-class-static-block" "^7.24.7" - "@babel/plugin-transform-classes" "^7.25.0" - "@babel/plugin-transform-computed-properties" "^7.24.7" - "@babel/plugin-transform-destructuring" "^7.24.8" - "@babel/plugin-transform-dotall-regex" "^7.24.7" - "@babel/plugin-transform-duplicate-keys" "^7.24.7" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.0" - "@babel/plugin-transform-dynamic-import" "^7.24.7" - "@babel/plugin-transform-exponentiation-operator" "^7.24.7" - "@babel/plugin-transform-export-namespace-from" "^7.24.7" - "@babel/plugin-transform-for-of" "^7.24.7" - "@babel/plugin-transform-function-name" "^7.25.1" - "@babel/plugin-transform-json-strings" "^7.24.7" - "@babel/plugin-transform-literals" "^7.25.2" - "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" - "@babel/plugin-transform-member-expression-literals" "^7.24.7" - "@babel/plugin-transform-modules-amd" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.8" - "@babel/plugin-transform-modules-systemjs" "^7.25.0" - "@babel/plugin-transform-modules-umd" "^7.24.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" - "@babel/plugin-transform-new-target" "^7.24.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" - "@babel/plugin-transform-numeric-separator" "^7.24.7" - "@babel/plugin-transform-object-rest-spread" "^7.24.7" - "@babel/plugin-transform-object-super" "^7.24.7" - "@babel/plugin-transform-optional-catch-binding" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.8" - "@babel/plugin-transform-parameters" "^7.24.7" - "@babel/plugin-transform-private-methods" "^7.24.7" - "@babel/plugin-transform-private-property-in-object" "^7.24.7" - "@babel/plugin-transform-property-literals" "^7.24.7" - "@babel/plugin-transform-regenerator" "^7.24.7" - "@babel/plugin-transform-reserved-words" "^7.24.7" - "@babel/plugin-transform-shorthand-properties" "^7.24.7" - "@babel/plugin-transform-spread" "^7.24.7" - "@babel/plugin-transform-sticky-regex" "^7.24.7" - "@babel/plugin-transform-template-literals" "^7.24.7" - "@babel/plugin-transform-typeof-symbol" "^7.24.8" - "@babel/plugin-transform-unicode-escapes" "^7.24.7" - "@babel/plugin-transform-unicode-property-regex" "^7.24.7" - "@babel/plugin-transform-unicode-regex" "^7.24.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" + "@babel/plugin-transform-arrow-functions" "^7.25.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.8" + "@babel/plugin-transform-async-to-generator" "^7.25.7" + "@babel/plugin-transform-block-scoped-functions" "^7.25.7" + "@babel/plugin-transform-block-scoping" "^7.25.7" + "@babel/plugin-transform-class-properties" "^7.25.7" + "@babel/plugin-transform-class-static-block" "^7.25.8" + "@babel/plugin-transform-classes" "^7.25.7" + "@babel/plugin-transform-computed-properties" "^7.25.7" + "@babel/plugin-transform-destructuring" "^7.25.7" + "@babel/plugin-transform-dotall-regex" "^7.25.7" + "@babel/plugin-transform-duplicate-keys" "^7.25.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.7" + "@babel/plugin-transform-dynamic-import" "^7.25.8" + "@babel/plugin-transform-exponentiation-operator" "^7.25.7" + "@babel/plugin-transform-export-namespace-from" "^7.25.8" + "@babel/plugin-transform-for-of" "^7.25.7" + "@babel/plugin-transform-function-name" "^7.25.7" + "@babel/plugin-transform-json-strings" "^7.25.8" + "@babel/plugin-transform-literals" "^7.25.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.8" + "@babel/plugin-transform-member-expression-literals" "^7.25.7" + "@babel/plugin-transform-modules-amd" "^7.25.7" + "@babel/plugin-transform-modules-commonjs" "^7.25.7" + "@babel/plugin-transform-modules-systemjs" "^7.25.7" + "@babel/plugin-transform-modules-umd" "^7.25.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.7" + "@babel/plugin-transform-new-target" "^7.25.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.8" + "@babel/plugin-transform-numeric-separator" "^7.25.8" + "@babel/plugin-transform-object-rest-spread" "^7.25.8" + "@babel/plugin-transform-object-super" "^7.25.7" + "@babel/plugin-transform-optional-catch-binding" "^7.25.8" + "@babel/plugin-transform-optional-chaining" "^7.25.8" + "@babel/plugin-transform-parameters" "^7.25.7" + "@babel/plugin-transform-private-methods" "^7.25.7" + "@babel/plugin-transform-private-property-in-object" "^7.25.8" + "@babel/plugin-transform-property-literals" "^7.25.7" + "@babel/plugin-transform-regenerator" "^7.25.7" + "@babel/plugin-transform-reserved-words" "^7.25.7" + "@babel/plugin-transform-shorthand-properties" "^7.25.7" + "@babel/plugin-transform-spread" "^7.25.7" + "@babel/plugin-transform-sticky-regex" "^7.25.7" + "@babel/plugin-transform-template-literals" "^7.25.7" + "@babel/plugin-transform-typeof-symbol" "^7.25.7" + "@babel/plugin-transform-unicode-escapes" "^7.25.7" + "@babel/plugin-transform-unicode-property-regex" "^7.25.7" + "@babel/plugin-transform-unicode-regex" "^7.25.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.7" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-corejs3 "^0.10.6" babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.37.1" + core-js-compat "^3.38.1" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -1058,95 +850,81 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" - integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== +"@babel/preset-react@7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.7.tgz#081cbe1dea363b732764d06a0fdda67ffa17735d" + integrity sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-transform-react-display-name" "^7.24.7" - "@babel/plugin-transform-react-jsx" "^7.24.7" - "@babel/plugin-transform-react-jsx-development" "^7.24.7" - "@babel/plugin-transform-react-pure-annotations" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-transform-react-display-name" "^7.25.7" + "@babel/plugin-transform-react-jsx" "^7.25.7" + "@babel/plugin-transform-react-jsx-development" "^7.25.7" + "@babel/plugin-transform-react-pure-annotations" "^7.25.7" -"@babel/preset-typescript@7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" - integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== +"@babel/preset-typescript@7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz#43c5b68eccb856ae5b52274b77b1c3c413cde1b7" + integrity sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.7" - "@babel/plugin-transform-typescript" "^7.24.7" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-syntax-jsx" "^7.25.7" + "@babel/plugin-transform-modules-commonjs" "^7.25.7" + "@babel/plugin-transform-typescript" "^7.25.7" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" - integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.24.7", "@babel/template@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" - integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== +"@babel/template@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769" + integrity sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA== dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/parser" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/code-frame" "^7.25.7" + "@babel/parser" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" - integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== +"@babel/traverse@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" + integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.0" - "@babel/parser" "^7.25.3" - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.2" + "@babel/code-frame" "^7.25.7" + "@babel/generator" "^7.25.7" + "@babel/parser" "^7.25.7" + "@babel/template" "^7.25.7" + "@babel/types" "^7.25.7" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.5", "@babel/types@^7.4.4": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" - integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== +"@babel/types@^7.25.7", "@babel/types@^7.25.8", "@babel/types@^7.4.4": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" + integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== dependencies: - "@babel/helper-string-parser" "^7.23.4" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - -"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" - integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== - dependencies: - "@babel/helper-string-parser" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" + "@babel/helper-string-parser" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" "@csstools/css-parser-algorithms@^2.1.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd" - integrity sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz#6d93a8f7d8aeb7cd9ed0868f946e46f021b6aa70" + integrity sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw== "@csstools/css-tokenizer@^2.1.1": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz#a4b8718ed7fcd2dcd555de16b31ca59ad4b96a06" - integrity sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw== + version "2.4.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz#1d8b2e200197cf5f35ceb07ca2dade31f3a00ae8" + integrity sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg== "@csstools/media-query-list-parser@^2.0.4": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz#feb4b7268f998956eb3ded69507869e73d005dda" - integrity sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA== + version "2.1.13" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz#f00be93f6bede07c14ddf51a168ad2748e4fe9e5" + integrity sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA== "@csstools/selector-specificity@^2.2.0": version "2.2.0" @@ -1166,9 +944,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + version "4.11.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -1185,55 +963,55 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@fortawesome/fontawesome-common-types@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" - integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ== +"@fortawesome/fontawesome-common-types@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f" + integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw== -"@fortawesome/fontawesome-free@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe" - integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ== +"@fortawesome/fontawesome-free@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz#0e984f0f2344ee513c185d87d77defac4c0c8224" + integrity sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow== -"@fortawesome/fontawesome-svg-core@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz#3727552eff9179506e9203d72feb5b1063c11a21" - integrity sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw== +"@fortawesome/fontawesome-svg-core@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff" + integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.6.0" -"@fortawesome/free-regular-svg-icons@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz#cacc53bd8d832d46feead412d9ea9ce80a55e13a" - integrity sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw== +"@fortawesome/free-regular-svg-icons@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz#fc49a947ac8dfd20403c9ea5f37f0919425bdf04" + integrity sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.6.0" -"@fortawesome/free-solid-svg-icons@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz#48c0e790847fa56299e2f26b82b39663b8ad7119" - integrity sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ== +"@fortawesome/free-solid-svg-icons@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz#061751ca43be4c4d814f0adbda8f006164ec9f3b" + integrity sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.6.0" -"@fortawesome/react-fontawesome@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" - integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== +"@fortawesome/react-fontawesome@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4" + integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g== dependencies: prop-types "^15.8.1" -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" + "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" minimatch "^3.0.5" @@ -1242,11 +1020,23 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": +"@humanwhocodes/object-schema@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" @@ -1275,11 +1065,11 @@ "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -1346,76 +1136,92 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@sentry-internal/feedback@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.100.0.tgz#38d8d4cb8ac3e6e24d91b13878bd6208a55bcab3" - integrity sha512-SMW2QhNKOuSjw8oPtvryDlJjiwrNyAKljbgtMk057os/fd8QMp38Yt1ImqLCM4B2rTQZ6REJ6hRGRTRcfqoG+w== - dependencies: - "@sentry/core" "7.100.0" - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/replay-canvas@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.100.0.tgz#b462346832631ed5a9446686419113ff331bd984" - integrity sha512-DePinj5IgNiC4RZv0yX0DLccMZebfFdKl3zHwDeLBeZqtMz9VrPzchv57IWP+5MI1+iuOn+WOg4oTNBUG6hFRw== +"@sentry-internal/feedback@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.119.1.tgz#98285dc9dba0ab62369d758124901b00faf58697" + integrity sha512-EPyW6EKZmhKpw/OQUPRkTynXecZdYl4uhZwdZuGqnGMAzswPOgQvFrkwsOuPYvoMfXqCH7YuRqyJrox3uBOrTA== dependencies: - "@sentry/core" "7.100.0" - "@sentry/replay" "7.100.0" - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry-internal/tracing@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.100.0.tgz#01f0925a287a6e5d0becd731ab361cabbd27c007" - integrity sha512-qf4W1STXky9WOQYoPSw2AmCBDK4FzvAyq5yeD2sLU7OCUEfbRUcN0lQljUvmWRKv/jTIAyeU5icDLJPZuR50nA== +"@sentry-internal/replay-canvas@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.119.1.tgz#b1413fb37734d609b0745ac24d49ddf9d63b9c51" + integrity sha512-O/lrzENbMhP/UDr7LwmfOWTjD9PLNmdaCF408Wx8SDuj7Iwc+VasGfHg7fPH4Pdr4nJON6oh+UqoV4IoG05u+A== dependencies: - "@sentry/core" "7.100.0" - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" + "@sentry/core" "7.119.1" + "@sentry/replay" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/browser@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.100.0.tgz#adf57f660baa6190a7e1709605f73b94818ee04b" - integrity sha512-XpM0jEVe6DJWXjMSOjtJxsSNR/XnJKrlcuyoI4Re3qLG+noEF5QLc0r3VJkySXPRFnmdW05sLswQ6a/n9Sijmg== +"@sentry-internal/tracing@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.119.1.tgz#500d50d451bfd0ce6b185e9f112208229739ab03" + integrity sha512-cI0YraPd6qBwvUA3wQdPGTy8PzAoK0NZiaTN1LM3IczdPegehWOaEG5GVTnpGnTsmBAzn1xnBXNBhgiU4dgcrQ== dependencies: - "@sentry-internal/feedback" "7.100.0" - "@sentry-internal/replay-canvas" "7.100.0" - "@sentry-internal/tracing" "7.100.0" - "@sentry/core" "7.100.0" - "@sentry/replay" "7.100.0" - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/core@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.100.0.tgz#5b28c7b3e41e45e4d50e3bdea5d35434fd78e86b" - integrity sha512-eWRPuP0Zdj4a2F7SybqNjf13LGOVgGwvW6sojweQp9oxGAfCPp/EMDGBhlpYbMJeLbzmqzJ4ZFHIedaiEC+7kg== +"@sentry/browser@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.119.1.tgz#260470dd7fd18de366017c3bf23a252a24d2ff05" + integrity sha512-aMwAnFU4iAPeLyZvqmOQaEDHt/Dkf8rpgYeJ0OEi50dmP6AjG+KIAMCXU7CYCCQDn70ITJo8QD5+KzCoZPYz0A== dependencies: - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" + "@sentry-internal/feedback" "7.119.1" + "@sentry-internal/replay-canvas" "7.119.1" + "@sentry-internal/tracing" "7.119.1" + "@sentry/core" "7.119.1" + "@sentry/integrations" "7.119.1" + "@sentry/replay" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/replay@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.100.0.tgz#4f2e35155626ab286692ade3e31da282c73bd402" - integrity sha512-6Yo56J+x+eedaMXri8pPlFxXOofnSXVdsUuFj+kJ7lC/qHrwIbgC5g1ONEK/WlYwpVH4gA0aNnCa5AOkMu+ZTg== +"@sentry/core@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.119.1.tgz#63e949cad167a0ee5e52986c93b96ff1d6a05b57" + integrity sha512-YUNnH7O7paVd+UmpArWCPH4Phlb5LwrkWVqzFWqL3xPyCcTSof2RL8UmvpkTjgYJjJ+NDfq5mPFkqv3aOEn5Sw== dependencies: - "@sentry-internal/tracing" "7.100.0" - "@sentry/core" "7.100.0" - "@sentry/types" "7.100.0" - "@sentry/utils" "7.100.0" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/types@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.100.0.tgz#a16f60d78613bd9810298e9e8d80134be58b24f8" - integrity sha512-c+RHwZwpKeBk7h8sUX4nQcelxBz8ViCojifnbEe3tcn8O15HOLvZqRKgLLOiff3MoErxiv4oxs0sPbEFRm/IvA== - -"@sentry/utils@7.100.0": - version "7.100.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.100.0.tgz#a9d36c01eede117c3e17b0350d399a87934e9c66" - integrity sha512-LAhZMEGq3C125prZN/ShqeXpRfdfgJkl9RAKjfq8cmMFsF7nsF72dEHZgIwrZ0lgNmtaWAB83AwJcyN83RwOxQ== +"@sentry/integrations@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.119.1.tgz#9fc17aa9fcb942fbd2fc12eecd77a0f316897960" + integrity sha512-CGmLEPnaBqbUleVqrmGYjRjf5/OwjUXo57I9t0KKWViq81mWnYhaUhRZWFNoCNQHns+3+GPCOMvl0zlawt+evw== dependencies: - "@sentry/types" "7.100.0" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" + localforage "^1.8.1" + +"@sentry/replay@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.119.1.tgz#117cf493a3008a39943b7d571d451c6218542847" + integrity sha512-4da+ruMEipuAZf35Ybt2StBdV1S+oJbSVccGpnl9w6RoeQoloT4ztR6ML3UcFDTXeTPT1FnHWDCyOfST0O7XMw== + dependencies: + "@sentry-internal/tracing" "7.119.1" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" + +"@sentry/types@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.119.1.tgz#f9c3c12e217c9078a6d556c92590e42a39b750dd" + integrity sha512-4G2mcZNnYzK3pa2PuTq+M2GcwBRY/yy1rF+HfZU+LAPZr98nzq2X3+mJHNJoobeHRkvVh7YZMPi4ogXiIS5VNQ== + +"@sentry/utils@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.119.1.tgz#08b28fa8170987a60e149e2102e83395a95e9a89" + integrity sha512-ju/Cvyeu/vkfC5/XBV30UNet5kLEicZmXSyuLwZu95hEbL+foPdxN+re7pCI/eNqfe3B2vz7lvz5afLVOlQ2Hg== + dependencies: + "@sentry/types" "7.119.1" "@types/archiver@^5.3.1": version "5.3.4" @@ -1424,26 +1230,10 @@ dependencies: "@types/readdir-glob" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.56.10" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" - integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/history@^4.7.11": version "4.7.11" @@ -1463,7 +1253,7 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1473,10 +1263,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@4.14.194": - version "4.14.194" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" - integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== +"@types/lodash@4.14.195": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== "@types/minimist@^1.2.0": version "1.2.5" @@ -1484,18 +1274,18 @@ integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@*": - version "20.12.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" - integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" -"@types/node@18.19.31": - version "18.19.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.31.tgz#b7d4a00f7cb826b60a543cebdbda5d189aaecdcd" - integrity sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA== +"@types/node@20.16.11": + version "20.16.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.11.tgz#9b544c3e716b1577ac12e70f9145193f32750b33" + integrity sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw== dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" "@types/normalize-package-data@^2.4.0": version "2.4.4" @@ -1522,19 +1312,19 @@ postcss "^8.0.0" "@types/prop-types@*": - version "15.7.12" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" - integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== -"@types/qs@6.9.15": - version "6.9.15" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" - integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== +"@types/qs@6.9.16": + version "6.9.16" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" + integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== -"@types/react-document-title@2.0.9": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.9.tgz#65fd57e6086ef8ced5ad45ac8a439dd878ca71d6" - integrity sha512-Q8bSnESgyVoMCo0vAKJj2N4wD/yl7EnutaFntKpaL/edUUo4kTNH8M6A5iCoje9sknRdLx7cfB39cpdTNr5Z+Q== +"@types/react-document-title@2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.10.tgz#f9c4563744b735750d84519ba1bc7099e1b2d1d0" + integrity sha512-a5RYXFccVqVhc429yXUn9zjJvaQwdx3Kueb8v8pEymUyExHoatHv0iS8BlOE3YuS+csA2pHbL2Hatnp7QEtLxQ== dependencies: "@types/react" "*" @@ -1545,17 +1335,17 @@ dependencies: "@types/react" "*" -"@types/react-lazyload@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.2.0.tgz#b763f8f0c724df2c969d7e0b3a56c6aa2720fa1f" - integrity sha512-4+r+z8Cf7L/mgxA1vl5uHx5GS/8gY2jqq2p5r5WCm+nUsg9KilwQ+8uaJA3EUlLj57AOzOfGGwwRJ5LOVl8fwA== +"@types/react-lazyload@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.2.3.tgz#42129b6e11353bfe8ed2ba5e1b964676ee23668b" + integrity sha512-s03gWlHXiFqZr7TEFDTx8Lkl+ZEYrwTkXP9MNZ3Z3blzsPrnkYjgeSK2tjfzVv/TYVCnDk6TZwNRDHQlHy/1Ug== dependencies: "@types/react" "*" "@types/react-redux@^7.1.16": - version "7.1.33" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" - integrity sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg== + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1579,21 +1369,29 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-text-truncate@0.14.1": - version "0.14.1" - resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28" - integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw== +"@types/react-text-truncate@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.19.0.tgz#322dd718dcbe1267a9d1279f8ac4dc844c322a13" + integrity sha512-8H7BjVf7Rp3ERTTiFZpQf6a5hllwdJrWuQ92nwQGp7DWQ2Ju89GRuzXHuZHXU9T+hLTGLCUPbimjQnW1mAogqQ== dependencies: "@types/react" "*" -"@types/react-window@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" - integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== +"@types/react-window@1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.79": +"@types/react@*": + version "18.3.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.11.tgz#9d530601ff843ee0d7030d4227ea4360236bd537" + integrity sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/react@18.2.79": version "18.2.79" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== @@ -1608,10 +1406,10 @@ dependencies: "@types/node" "*" -"@types/redux-actions@2.6.2": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.2.tgz#5956d9e7b9a644358e2c0610f47b1fa3060edc21" - integrity sha512-TvcINy8rWFANcpc3EiEQX9Yv3owM3d3KIrqr2ryUIOhYIYzXA/bhDZeGSSSuai62iVR2qMZUgz9tQ5kr0Kl+Tg== +"@types/redux-actions@2.6.5": + version "2.6.5" + resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.5.tgz#3728477ada6c4bc0fe54ffae11def23a65c0714b" + integrity sha512-RgXOigay5cNweP+xH1ru+Vaaj1xXYLpWIfSVO8cSA8Ii2xvR+HRfWYdLe1UVOA8X0kIklalGOa0DTDyld0obkg== "@types/semver@^7.5.0": version "7.5.8" @@ -1635,7 +1433,7 @@ dependencies: source-map "^0.6.1" -"@types/webpack-livereload-plugin@^2.3.3": +"@types/webpack-livereload-plugin@2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.6.tgz#2c3ccefc8858525f40aeb8be0f784d5027144e23" integrity sha512-H8nZSOWSiY/6kCpOmbutZPu7Sai1xyEXo/SrXQPCymMPNBwpYWAdOsjKqr32d+IrVjnn9GGgKSYY34TEPRxJ/A== @@ -1652,9 +1450,9 @@ source-map "^0.7.3" "@types/webpack@^4": - version "4.41.38" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.38.tgz#5a40ac81bdd052bf405e8bdcf3e1236f6db6dc26" - integrity sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw== + version "4.41.39" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.39.tgz#ab6feaeef8e074d0b584bbe4a4e2dc604b58eed7" + integrity sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg== dependencies: "@types/node" "*" "@types/tapable" "^1" @@ -1754,7 +1552,7 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.11.5": +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== @@ -1820,7 +1618,7 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== -"@webassemblyjs/wasm-edit@^1.11.5": +"@webassemblyjs/wasm-edit@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== @@ -1855,7 +1653,7 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.11.5": +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== @@ -1875,17 +1673,17 @@ "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" -"@webpack-cli/configtest@^2.1.0": +"@webpack-cli/configtest@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== -"@webpack-cli/info@^2.0.1": +"@webpack-cli/info@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== -"@webpack-cli/serve@^2.0.4": +"@webpack-cli/serve@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== @@ -1907,10 +1705,10 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -acorn-import-assertions@^1.7.6: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -1918,9 +1716,9 @@ acorn-jsx@^5.3.2: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== add-px-to-style@1.0.0: version "1.0.0" @@ -1965,20 +1763,25 @@ ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1993,6 +1796,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@^3.0.0, anymatch@^3.1.1, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -2059,7 +1867,7 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" -array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== @@ -2076,7 +1884,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.findlast@^1.2.4: +array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== @@ -2088,7 +1896,7 @@ array.prototype.findlast@^1.2.4: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.findlastindex@^1.2.3: +array.prototype.findlastindex@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== @@ -2120,25 +1928,15 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" - integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== - dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.1.0" + es-abstract "^1.23.3" + es-errors "^1.3.0" es-shim-unscopables "^1.0.2" arraybuffer.prototype.slice@^1.0.3: @@ -2160,11 +1958,6 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -2178,9 +1971,9 @@ async@^2.6.4: lodash "^4.17.14" async@^3.2.4: - version "3.2.5" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" - integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== autoprefixer@10.4.20: version "10.4.20" @@ -2201,10 +1994,10 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -babel-loader@9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== +babel-loader@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== dependencies: find-cache-dir "^4.0.0" schema-utils "^4.0.0" @@ -2215,28 +2008,28 @@ babel-plugin-inline-classnames@2.0.1: integrity sha512-Pq/jJ6hTiGiqcMmy2d4CyJcfBDeUHOdQl1t1MDWNaSKR2RxDmShSAx4Zqz6NDmFaiinaRqF8eQoTVgSRGU+McQ== babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.10" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" - integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.1" + "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.4: - version "0.10.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" - integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" - core-js-compat "^3.36.1" + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" - integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" + "@babel/helper-define-polyfill-provider" "^0.6.2" babel-plugin-transform-react-remove-prop-types@0.4.24: version "0.4.24" @@ -2320,30 +2113,20 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" -browserslist@^4.14.5, browserslist@^4.22.2, browserslist@^4.23.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" - integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== +browserslist@^4.21.10, browserslist@^4.23.3, browserslist@^4.24.0: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== dependencies: - caniuse-lite "^1.0.30001587" - electron-to-chromium "^1.4.668" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" - -browserslist@^4.23.1, browserslist@^4.23.3: - version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" - integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== - dependencies: - caniuse-lite "^1.0.30001646" - electron-to-chromium "^1.5.4" + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" node-releases "^2.0.18" update-browserslist-db "^1.1.0" @@ -2413,10 +2196,10 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001646: - version "1.0.30001651" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" - integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== +caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: + version "1.0.30001667" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz#99fc5ea0d9c6e96897a104a8352604378377f949" + integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -2435,7 +2218,7 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: +chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -2450,17 +2233,19 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -classnames@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== - -classnames@^2.2.6: +classnames@2.5.1, classnames@^2.2.6: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -2616,29 +2401,17 @@ copy-to-clipboard@3.3.3: dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.36.1: - version "3.37.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73" - integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA== - dependencies: - browserslist "^4.23.0" - -core-js-compat@^3.37.1: - version "3.38.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.0.tgz#d93393b1aa346b6ee683377b0c31172ccfe607aa" - integrity sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A== +core-js-compat@^3.38.0, core-js-compat@^3.38.1: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" + integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== dependencies: browserslist "^4.23.3" -core-js@3.38.0: - version "3.38.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.0.tgz#8acb7c050bf2ccbb35f938c0d040132f6110f636" - integrity sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug== - -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA== +core-js@3.38.1: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" + integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== core-js@^2.4.0: version "2.6.12" @@ -2684,15 +2457,15 @@ crc32-stream@^4.0.2: crc-32 "^1.2.0" readable-stream "^3.4.0" -create-react-context@<=0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" - integrity sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A== +create-react-context@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c" + integrity sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw== dependencies: - fbjs "^0.8.0" gud "^1.0.0" + warning "^4.0.3" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2712,9 +2485,9 @@ css-color-function@~1.3.3: rgb "~0.1.0" css-functions-list@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.1.tgz#2eb205d8ce9f9ce74c5c1d7490b66b77c45ce3ea" - integrity sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ== + version "3.2.3" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" + integrity sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA== css-loader@6.7.3: version "6.7.3" @@ -2817,11 +2590,11 @@ debug@^3.1.0, debug@^3.2.7: ms "^2.1.1" debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" decamelize-keys@^1.1.0: version "1.1.1" @@ -2836,6 +2609,18 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +deep-equal@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.2.tgz#78a561b7830eef3134c7f6f3a3d6af272a678761" + integrity sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg== + dependencies: + is-arguments "^1.1.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + object-is "^1.1.5" + object-keys "^1.1.1" + regexp.prototype.flags "^1.5.1" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -2890,14 +2675,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dnd-core@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.0.tgz#973ab3470d0a9ac5a0fa9021c4feba93ad12347d" - integrity sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA== +dnd-core@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e" + integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A== dependencies: "@react-dnd/asap" "^4.0.0" "@react-dnd/invariant" "^2.0.0" - redux "^4.0.5" + redux "^4.1.1" dnd-multi-backend@^6.0.0: version "6.0.0" @@ -2984,15 +2769,15 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== -electron-to-chromium@^1.4.668: - version "1.4.745" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290" - integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -electron-to-chromium@^1.5.4: - version "1.5.9" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.9.tgz#3e92950e3a409d109371b7a153a9c5712e2509fd" - integrity sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA== +electron-to-chromium@^1.5.28: + version "1.5.35" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.35.tgz#1d38d386186c72b1fa6e74c3a7de5f888b503100" + integrity sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A== element-class@0.2.2: version "0.2.2" @@ -3004,18 +2789,16 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -encoding@^0.1.11: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -3023,10 +2806,10 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.14.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -3037,9 +2820,9 @@ entities@^2.0.0: integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== envinfo@^7.7.3: - version "7.12.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.12.0.tgz#b56723b39c2053d67ea5714f026d05d4f5cc7acd" - integrity sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg== + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== errno@^0.1.1: version "0.1.8" @@ -3069,7 +2852,7 @@ error@^7.0.0: dependencies: string-template "~0.2.1" -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -3128,35 +2911,35 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-iterator-helpers@^1.0.17: - version "1.0.18" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" - integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== +es-iterator-helpers@^1.0.19: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" + integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.3" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" get-intrinsic "^1.2.4" - globalthis "^1.0.3" + globalthis "^1.0.4" has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" internal-slot "^1.0.7" - iterator.prototype "^1.1.2" + iterator.prototype "^1.1.3" safe-array-concat "^1.1.2" es-module-lexer@^1.2.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236" - integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw== + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es-object-atoms@^1.0.0: version "1.0.0" @@ -3195,10 +2978,10 @@ es6-promise@^4.2.8: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== -escalade@^3.1.1, escalade@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^1.0.5: version "1.0.5" @@ -3224,10 +3007,10 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-module-utils@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" - integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" @@ -3241,27 +3024,29 @@ eslint-plugin-filenames@1.3.2: lodash.snakecase "4.1.1" lodash.upperfirst "4.3.1" -eslint-plugin-import@2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" - integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== +eslint-plugin-import@2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: - array-includes "^3.1.7" - array.prototype.findlastindex "^1.2.3" + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" array.prototype.flat "^1.3.2" array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.8.0" - hasown "^2.0.0" - is-core-module "^2.13.1" + eslint-module-utils "^2.12.0" + hasown "^2.0.2" + is-core-module "^2.15.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.fromentries "^2.0.7" - object.groupby "^1.0.1" - object.values "^1.1.7" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" semver "^6.3.1" + string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" eslint-plugin-prettier@4.2.1: @@ -3271,39 +3056,39 @@ eslint-plugin-prettier@4.2.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== +eslint-plugin-react-hooks@4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@7.34.1: - version "7.34.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" - integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== +eslint-plugin-react@7.37.1: + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== dependencies: - array-includes "^3.1.7" - array.prototype.findlast "^1.2.4" + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" - array.prototype.tosorted "^1.1.3" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.17" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" - object.hasown "^1.1.3" - object.values "^1.1.7" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.10" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" -eslint-plugin-simple-import-sort@12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.0.tgz#8186ad55474d2f5c986a2f1bf70625a981e30d05" - integrity sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig== +eslint-plugin-simple-import-sort@12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz#e64bfdaf91c5b98a298619aa634a9f7aa43b709e" + integrity sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA== eslint-scope@5.1.1: version "5.1.1" @@ -3331,16 +3116,16 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.57.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== +eslint@8.57.1: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -3385,9 +3170,9 @@ espree@^9.6.0, espree@^9.6.1: eslint-visitor-keys "^3.4.1" esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -3459,6 +3244,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" + integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== + fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -3478,19 +3268,6 @@ faye-websocket@~0.10.0: dependencies: websocket-driver ">=0.5.1" -fbjs@^0.8.0: - version "0.8.18" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a" - integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA== - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - 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" @@ -3527,15 +3304,15 @@ filemanager-webpack-plugin@8.0.0: normalize-path "^3.0.0" schema-utils "^4.0.0" -filesize@10.0.7: - version "10.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.7.tgz#2237a816ee60a83fd0c3382ae70800e54eced3ad" - integrity sha512-iMRG7Qo9nayLoU3PNCiLizYtsy4W1ClrapeCwEgtiQelOAOuRJiw4QaLI+sSr8xr901dgHv+EYP2bCusGZgoiA== +filesize@10.1.6: + version "10.1.6" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361" + integrity sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3604,6 +3381,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fork-ts-checker-webpack-plugin@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz#dae45dfe7298aa5d553e2580096ced79b6179504" @@ -3642,9 +3427,9 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: universalify "^2.0.0" fs-monkey@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" - integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== fs.realpath@^1.0.0: version "1.0.0" @@ -3661,7 +3446,7 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: +function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== @@ -3730,6 +3515,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3742,16 +3539,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.3.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" - integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== - dependencies: - fs.realpath "^1.0.0" - minimatch "^8.0.2" - minipass "^4.2.4" - path-scurry "^1.6.1" - global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -3780,12 +3567,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== +globalthis@^1.0.3, globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - define-properties "^1.1.3" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.0.1, globby@^11.1.0: version "11.1.0" @@ -3811,7 +3599,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3882,19 +3670,7 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -history@4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" - integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^2.2.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^0.4.0" - -history@^4.9.0: +history@4.10.1, history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== @@ -3943,10 +3719,10 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== -html-webpack-plugin@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763" - integrity sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA== +html-webpack-plugin@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" + integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -3969,7 +3745,7 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== -iconv-lite@^0.6.2, iconv-lite@^0.6.3: +iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -3987,19 +3763,24 @@ ieee754@^1.1.13: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + "immutable@^3.8.1 || ^4.0.0", immutable@^4.0.0: - version "4.3.5" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" - integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" @@ -4015,9 +3796,9 @@ import-lazy@^4.0.0: integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -4071,6 +3852,14 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -4118,12 +3907,12 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.5.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: - hasown "^2.0.0" + hasown "^2.0.2" is-data-view@^1.0.1: version "1.0.1" @@ -4239,11 +4028,6 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -4320,18 +4104,10 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA== - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== +iterator.prototype@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" + integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== dependencies: define-properties "^1.2.1" get-intrinsic "^1.2.1" @@ -4339,6 +4115,13 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" + jdu@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jdu/-/jdu-1.0.0.tgz#28f1e388501785ae0a1d93e93ed0b14dd41e51ce" @@ -4354,14 +4137,14 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jiti@^1.18.2: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== -jquery@3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.0.tgz#fe2c01a05da500709006d8790fe21c8a39d75612" - integrity sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ== +jquery@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -4375,15 +4158,10 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@^3.0.2, jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== json-buffer@3.0.1: version "3.0.1" @@ -4505,6 +4283,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + lilconfig@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -4556,9 +4341,16 @@ loader-utils@^2.0.0: json5 "^2.1.2" loader-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + version "3.3.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== + +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" locate-path@^5.0.0: version "5.0.0" @@ -4665,10 +4457,10 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lru-cache@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +lru-cache@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.1.tgz#3a732fbfedb82c5ba7bca6564ad3f42afcb6e147" + integrity sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ== lru-cache@^5.1.1: version "5.1.1" @@ -4760,11 +4552,11 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0: @@ -4802,12 +4594,13 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.12.1" tiny-warning "^1.0.3" -mini-css-extract-plugin@2.7.5: - version "2.7.5" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5" - integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ== +mini-css-extract-plugin@2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758" + integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ== dependencies: schema-utils "^4.0.0" + tapable "^2.2.1" minimatch@9.0.3: version "9.0.3" @@ -4816,6 +4609,13 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4830,13 +4630,6 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^8.0.2: - version "8.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" - integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== - dependencies: - brace-expansion "^2.0.1" - minimatch@~3.0.4: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -4858,15 +4651,10 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" - integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mkdirp@^0.5.6: version "0.5.6" @@ -4880,22 +4668,17 @@ mobile-detect@1.4.5: resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.5.tgz#da393c3c413ca1a9bcdd9ced653c38281c0fb6ad" integrity sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g== -moment@2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +moment@2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== mousetrap@1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4936,14 +4719,6 @@ node-abort-controller@^3.0.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -4951,11 +4726,6 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== - node-releases@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" @@ -5014,9 +4784,17 @@ object-assign@^4.1.0, object-assign@^4.1.1: integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" object-keys@^1.1.1: version "1.1.1" @@ -5033,7 +4811,7 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.7: +object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -5042,7 +4820,7 @@ object.entries@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -object.fromentries@^2.0.7: +object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== @@ -5052,7 +4830,7 @@ object.fromentries@^2.0.7: es-abstract "^1.23.2" es-object-atoms "^1.0.0" -object.groupby@^1.0.1: +object.groupby@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== @@ -5061,16 +4839,7 @@ object.groupby@^1.0.1: define-properties "^1.2.1" es-abstract "^1.23.2" -object.hasown@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -5087,16 +4856,16 @@ once@^1.3.0, once@^1.4.0: wrappy "1" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" + word-wrap "^1.2.5" p-limit@^2.2.0: version "2.3.0" @@ -5152,6 +4921,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -5215,18 +4989,18 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.6.1: - version "1.10.2" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" - integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + lru-cache "^11.0.0" + minipass "^7.1.2" path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== dependencies: isarray "0.0.1" @@ -5240,15 +5014,10 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" @@ -5384,24 +5153,16 @@ postcss-nested@6.2.0: postcss-selector-parser "^6.1.1" postcss-resolve-nested-selector@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" - integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== + version "0.1.6" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz#3d84dec809f34de020372c41b039956966896686" + integrity sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw== postcss-safe-parser@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.16" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" - integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^6.1.1: +postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.1.1: version "6.1.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== @@ -5439,14 +5200,14 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.41: - version "8.4.41" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681" - integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== +postcss@8.4.47, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.32: + version "8.4.47" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== dependencies: nanoid "^3.3.7" - picocolors "^1.0.1" - source-map-js "^1.2.0" + picocolors "^1.1.0" + source-map-js "^1.2.1" postcss@^6.0.23: version "6.0.23" @@ -5457,15 +5218,6 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" - prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -5501,13 +5253,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - prop-types@15.8.1, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -5532,17 +5277,10 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.11.1: - version "6.11.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" - integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== - dependencies: - side-channel "^1.0.4" - -qs@^6.4.0: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== +qs@6.13.0, qs@^6.4.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" @@ -5625,12 +5363,12 @@ react-custom-scrollbars-2@4.5.0: prop-types "^15.5.10" raf "^3.1.0" -react-dnd-html5-backend@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz#28d660a2ad1e07447c34a65cd25f7de8f1657194" - integrity sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g== +react-dnd-html5-backend@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c" + integrity sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw== dependencies: - dnd-core "14.0.0" + dnd-core "14.0.1" react-dnd-multi-backend@6.0.2: version "6.0.2" @@ -5648,22 +5386,22 @@ react-dnd-preview@^6.0.2: dependencies: prop-types "^15.7.2" -react-dnd-touch-backend@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/react-dnd-touch-backend/-/react-dnd-touch-backend-14.0.0.tgz#f7f144ca4af17946e20cdfd06a766ebadd170d4d" - integrity sha512-fNt3isf9h0xgjj86dIXhBi3dJ7OhC88vKUYdEvsOGypdp3LOFD+TobBAuBu00v26WmJ6II6xqbkhOx+KOcyHxQ== +react-dnd-touch-backend@14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/react-dnd-touch-backend/-/react-dnd-touch-backend-14.1.1.tgz#d8875ef1cf8dcbf1741a4e03dd5b147c4fbda5e4" + integrity sha512-ITmfzn3fJrkUBiVLO6aJZcnu7T8C+GfwZitFryGsXKn5wYcUv+oQBeh9FYcMychmVbDdeUCfvEtTk9O+DKmAaw== dependencies: "@react-dnd/invariant" "^2.0.0" - dnd-core "14.0.0" + dnd-core "14.0.1" -react-dnd@14.0.2: - version "14.0.2" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.2.tgz#57266baec92b887301f81fa3b77f87168d159733" - integrity sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A== +react-dnd@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.4.tgz#ffb4ea0e2a3a5532f9c6294d565742008a52b8b0" + integrity sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg== dependencies: "@react-dnd/invariant" "^2.0.0" "@react-dnd/shallowequal" "^2.0.0" - dnd-core "14.0.0" + dnd-core "14.0.1" fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" @@ -5738,13 +5476,14 @@ react-middle-truncate@1.0.3: normalize.css "^8.0.0" units-css "^0.4.0" -react-popper@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6" - integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w== +react-popper@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" + integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww== dependencies: "@babel/runtime" "^7.1.2" - create-react-context "<=0.2.2" + create-react-context "^0.3.0" + deep-equal "^1.1.1" popper.js "^1.14.4" prop-types "^15.6.1" typed-styles "^0.0.7" @@ -5811,10 +5550,10 @@ react-tabs@4.3.0: clsx "^1.1.0" prop-types "^15.5.0" -react-text-truncate@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.18.0.tgz#c65f4be660d24734badb903a4832467eddcf8058" - integrity sha512-0cc07CRPRPm3cTloVbPicVTSosJNauwVcmJb5FPa71u4KtDVgrRPJoxVvLBubl3gLyx1pVUozgREYMHpHM16jg== +react-text-truncate@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.19.0.tgz#60bc5ecf29a03ebc256f31f90a2d8402176aac91" + integrity sha512-QxHpZABfGG0Z3WEYbRTZ+rXdZn50Zvp+sWZXgVAd7FCKAMzv/kcwctTpNmWgXDTpAoHhMjOVwmgRtX3x5yeF4w== dependencies: prop-types "^15.5.7" @@ -5845,10 +5584,10 @@ react-virtualized@9.21.1: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react-window@1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8" - integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q== +react-window@1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" @@ -5909,6 +5648,11 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -5962,7 +5706,7 @@ redux-thunk@2.4.2: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@4.2.1, redux@^4.0.0, redux@^4.0.5: +redux@4.2.1, redux@^4.0.0, redux@^4.1.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -5982,10 +5726,10 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" -regenerate-unicode-properties@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" - integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== dependencies: regenerate "^1.4.2" @@ -6011,34 +5755,39 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== dependencies: - call-bind "^1.0.6" + call-bind "^1.0.7" define-properties "^1.2.1" es-errors "^1.3.0" - set-function-name "^2.0.1" + set-function-name "^2.0.2" -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== +regexpu-core@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.1.1.tgz#b469b245594cb2d088ceebc6369dceb8c00becac" + integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== dependencies: - "@babel/regjsgen" "^0.8.0" regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.11.0" unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.11.1.tgz#ae55c74f646db0c8fcb922d4da635e33da405149" + integrity sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ== dependencies: - jsesc "~0.5.0" + jsesc "~3.0.2" relateurl@^0.2.7: version "0.2.7" @@ -6103,11 +5852,6 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-pathname@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" - integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== - resolve-pathname@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" @@ -6141,12 +5885,13 @@ rgb@~0.1.0: resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" integrity sha512-F49dXX73a92N09uQkfCp2QjwXpmJcn9/i9PvjmwsSIXUGqRLCf/yx5Q9gRxuLQTq248kakqQuc8GX/U/CxSqlA== -rimraf@4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" - integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== +rimraf@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" + integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== dependencies: - glob "^9.2.0" + glob "^11.0.0" + package-json-from-dist "^1.0.0" rimraf@^3.0.2: version "3.0.2" @@ -6202,18 +5947,18 @@ safe-regex-test@^1.0.3: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.58.3: - version "1.75.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.75.0.tgz#91bbe87fb02dfcc34e052ddd6ab80f60d392be6c" - integrity sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw== + version "1.79.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.4.tgz#f9c45af35fbeb53d2c386850ec842098d9935267" + integrity sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg== dependencies: - chokidar ">=3.0.0 <4.0.0" + chokidar "^4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" sax@^1.2.4: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== sax@~1.2.4: version "1.2.4" @@ -6238,7 +5983,7 @@ schema-utils@>1.0.0, schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -6268,11 +6013,9 @@ semver@^6.0.0, semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== serialize-javascript@^6.0.1: version "6.0.2" @@ -6303,11 +6046,6 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -6366,10 +6104,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-support@~0.5.20: version "0.5.21" @@ -6389,7 +6127,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.3, source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -6416,9 +6154,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.17" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" - integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== stack-generator@^2.0.5: version "2.0.10" @@ -6454,7 +6192,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6463,7 +6201,25 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.10: +string-width@^4.1.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== @@ -6481,6 +6237,14 @@ string.prototype.matchall@^4.0.10: set-function-name "^2.0.2" side-channel "^1.0.6" +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -6528,13 +6292,27 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -6562,12 +6340,12 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== -stylelint-order@6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.3.tgz#160b78650bd90463241b992581efee7159baefc2" - integrity sha512-1j1lOb4EU/6w49qZeT2SQVJXm0Ht+Qnq9GMfUa3pMwoyojIWfuA+JUDmoR97Bht1RLn4ei0xtLGy87M7d29B1w== +stylelint-order@6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.4.tgz#3e80d876c61a98d2640de181433686f24284748b" + integrity sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA== dependencies: - postcss "^8.4.21" + postcss "^8.4.32" postcss-sorting "^8.0.2" stylelint@15.6.1: @@ -6656,9 +6434,9 @@ supports-color@^8.0.0: has-flag "^4.0.0" supports-hyperlinks@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" - integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz#b56150ff0173baacc15f21956450b61f2b18d3ac" + integrity sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -6700,18 +6478,7 @@ tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -terser-webpack-plugin@5.3.8: - version "5.3.8" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz#415e03d2508f7de63d59eca85c5d102838f06610" - integrity sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.8" - -terser-webpack-plugin@^5.3.7: +terser-webpack-plugin@5.3.10, terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== @@ -6722,10 +6489,10 @@ terser-webpack-plugin@^5.3.7: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@^5.10.0, terser@^5.16.8, terser@^5.26.0: - version "5.30.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" - integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== +terser@^5.10.0, terser@^5.26.0: + version "5.34.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6" + integrity sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -6796,9 +6563,9 @@ toggle-selection@^1.0.6: integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== "tough-cookie@^2.3.3 || ^3.0.1 || ^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== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -6820,15 +6587,16 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-loader@9.4.2: - version "9.4.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" - integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== +ts-loader@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89" + integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" micromatch "^4.0.0" semver "^7.3.4" + source-map "^0.7.4" tsconfig-paths@^3.15.0: version "3.15.0" @@ -6850,9 +6618,9 @@ tsconfig-paths@^4.1.2: strip-bom "^3.0.0" tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -6957,11 +6725,6 @@ typescript@5.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== -ua-parser-js@^0.7.30: - version "0.7.37" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" - integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -6972,15 +6735,15 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" @@ -6991,9 +6754,9 @@ unicode-match-property-ecmascript@^2.0.0: unicode-property-aliases-ecmascript "^2.0.0" unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" @@ -7018,21 +6781,13 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" + escalade "^3.2.0" + picocolors "^1.1.0" uri-js@^4.2.2: version "4.4.1" @@ -7096,11 +6851,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -value-equal@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" - integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== - value-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" @@ -7111,17 +6861,17 @@ viewport-dimensions@^0.2.0: resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" integrity sha512-94JqlKxEP4m7WO+N3rm4tFRGXZmXXwSPQCoV+EPxDnn8YAGiLU3T+Ha1imLreAjXsHl0K+ELnIqv64i1XZHLFQ== -warning@^4.0.2: +warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== dependencies: loose-envify "^1.0.0" -watchpack@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" - integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -7131,15 +6881,15 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webpack-cli@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.1.tgz#c211ac6d911e77c512978f7132f0d735d4a97ace" - integrity sha512-OLJwVMoXnXYH2ncNGU8gxVpUtm3ybvdioiTvHgUyBuyMLKiVvWy+QObzBsMtp5pH7qQoEuWgeEUQ/sU3ZJFzAw== +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== dependencies: "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^2.1.0" - "@webpack-cli/info" "^2.0.1" - "@webpack-cli/serve" "^2.0.4" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" colorette "^2.0.14" commander "^10.0.1" cross-spawn "^7.0.3" @@ -7174,34 +6924,33 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.82.1: - version "5.82.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.1.tgz#8f38c78e53467556e8a89054ebd3ef6e9f67dbab" - integrity sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw== +webpack@5.95.0: + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.14.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.2" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1: @@ -7218,11 +6967,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-fetch@>=0.10.0: - version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== - whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -7243,12 +6987,12 @@ which-boxed-primitive@^1.0.2: is-symbol "^1.0.3" which-builtin-type@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" - integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== dependencies: - function.prototype.name "^1.1.5" - has-tostringtag "^1.0.0" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" is-async-function "^2.0.0" is-date-object "^1.0.5" is-finalizationregistry "^1.0.2" @@ -7257,10 +7001,10 @@ which-builtin-type@^1.1.3: is-weakref "^1.0.2" isarray "^2.0.5" which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" + which-collection "^1.0.2" + which-typed-array "^1.1.15" -which-collection@^1.0.1: +which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -7270,7 +7014,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: +which-typed-array@^1.1.14, which-typed-array@^1.1.15: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -7300,6 +7044,11 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + worker-loader@3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37" @@ -7308,6 +7057,24 @@ worker-loader@3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7322,9 +7089,9 @@ write-file-atomic@^5.0.1: signal-exit "^4.0.1" ws@^7.4.5: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xxhashjs@~0.2.2: version "0.2.2" From 1e89a1a3cb8fa83e4415b047513cbecacbebc59c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 10 Oct 2024 21:15:27 +0300 Subject: [PATCH 595/762] Include exception message in SkyHook failure message --- frontend/src/AddSeries/AddNewSeries/AddNewSeries.js | 4 +++- src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeries.js b/frontend/src/AddSeries/AddNewSeries/AddNewSeries.js index 331d4849e..18cbffddb 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeries.js +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeries.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import TextInput from 'Components/Form/TextInput'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; @@ -129,7 +130,8 @@ class AddNewSeries extends Component {
{translate('AddNewSeriesError')}
-
{getErrorMessage(error)}
+ + {getErrorMessage(error)}
: null } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index c6656816d..f600057a7 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -147,17 +147,17 @@ namespace NzbDrone.Core.MetadataSource.SkyHook catch (HttpException ex) { _logger.Warn(ex); - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", ex, title); + throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook. {1}", ex, title, ex.Message); } catch (WebException ex) { _logger.Warn(ex); - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", ex, title, ex.Message); + throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook. {1}", ex, title, ex.Message); } catch (Exception ex) { _logger.Warn(ex); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", ex, title); + throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook. {1}", ex, title, ex.Message); } } From 57534db2f8e54e103ed9e4a4a75a17380b87051e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 27 Oct 2024 00:17:18 +0300 Subject: [PATCH 596/762] New: Display tags on import list cards --- .../ImportLists/ImportLists/ImportList.js | 18 ++++++++++++++---- .../ImportLists/ImportLists/ImportLists.js | 3 +++ .../ImportLists/ImportListsConnector.js | 9 ++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js index 75792c9ae..ba9f0761b 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import Card from 'Components/Card'; import Label from 'Components/Label'; import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; import { kinds } from 'Helpers/Props'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import translate from 'Utilities/String/translate'; @@ -57,6 +58,8 @@ class ImportList extends Component { id, name, enableAutomaticAdd, + tags, + tagList, minRefreshInterval } = this.props; @@ -72,16 +75,21 @@ class ImportList extends Component {
{ - enableAutomaticAdd && + enableAutomaticAdd ? + : + null } -
+ +
-
@@ -111,6 +119,8 @@ ImportList.propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, enableAutomaticAdd: PropTypes.bool.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, minRefreshInterval: PropTypes.string.isRequired, onConfirmDeleteImportList: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js index 11fcceb54..b6f6e5837 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.js @@ -49,6 +49,7 @@ class ImportLists extends Component { render() { const { items, + tagList, onConfirmDeleteImportList, ...otherProps } = this.props; @@ -71,6 +72,7 @@ class ImportLists extends Component { ); @@ -109,6 +111,7 @@ ImportLists.propTypes = { isFetching: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteImportList: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 017467e53..633d4f2f7 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -5,13 +5,20 @@ import { createSelector } from 'reselect'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; import sortByProp from 'Utilities/Array/sortByProp'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( createSortedSectionSelector('settings.importLists', sortByProp('name')), - (importLists) => importLists + createTagsSelector(), + (importLists, tagList) => { + return { + ...importLists, + tagList + }; + } ); } From 20ef22be94f4bdb5633ddfb080e91c8d5b0229da Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 27 Oct 2024 00:17:46 +0300 Subject: [PATCH 597/762] New: Real time UI updates for provider changes --- frontend/src/Components/SignalRConnector.js | 50 ++++++++++++++++++- .../DownloadClientController.cs | 5 +- .../ImportLists/ImportListController.cs | 8 ++- .../Indexers/IndexerController.cs | 7 ++- .../Metadata/MetadataController.cs | 5 +- .../Notifications/NotificationController.cs | 5 +- src/Sonarr.Api.V3/ProviderControllerBase.cs | 31 +++++++++++- 7 files changed, 98 insertions(+), 13 deletions(-) diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 918f53fa5..e59669883 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -168,7 +168,7 @@ class SignalRConnector extends Component { const status = resource.status; // Both successful and failed commands need to be - // completed, otherwise they spin until they timeout. + // completed, otherwise they spin until they time out. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -202,10 +202,58 @@ class SignalRConnector extends Component { } }; + handleDownloadclient = ({ action, resource }) => { + const section = 'settings.downloadClients'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + handleHealth = () => { this.props.dispatchFetchHealth(); }; + handleImportlist = ({ action, resource }) => { + const section = 'settings.importLists'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + + handleIndexer = ({ action, resource }) => { + const section = 'settings.indexers'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + + handleMetadata = ({ action, resource }) => { + const section = 'settings.metadata'; + + if (action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } + }; + + handleNotification = ({ action, resource }) => { + const section = 'settings.notifications'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + handleSeries = (body) => { const action = body.action; const section = 'series'; diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs index 0f79a83c6..1c0c53605 100644 --- a/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Download; +using NzbDrone.SignalR; using Sonarr.Http; namespace Sonarr.Api.V3.DownloadClient @@ -9,8 +10,8 @@ namespace Sonarr.Api.V3.DownloadClient public static readonly DownloadClientResourceMapper ResourceMapper = new (); public static readonly DownloadClientBulkResourceMapper BulkResourceMapper = new (); - public DownloadClientController(IDownloadClientFactory downloadClientFactory) - : base(downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper) + public DownloadClientController(IBroadcastSignalRMessage signalRBroadcaster, IDownloadClientFactory downloadClientFactory) + : base(signalRBroadcaster, downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper) { } } diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs index 499043f33..02a55d364 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs @@ -2,6 +2,7 @@ using FluentValidation; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; using Sonarr.Http; namespace Sonarr.Api.V3.ImportLists @@ -12,8 +13,11 @@ namespace Sonarr.Api.V3.ImportLists public static readonly ImportListResourceMapper ResourceMapper = new (); public static readonly ImportListBulkResourceMapper BulkResourceMapper = new (); - public ImportListController(IImportListFactory importListFactory, RootFolderExistsValidator rootFolderExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator) - : base(importListFactory, "importlist", ResourceMapper, BulkResourceMapper) + public ImportListController(IBroadcastSignalRMessage signalRBroadcaster, + IImportListFactory importListFactory, + RootFolderExistsValidator rootFolderExistsValidator, + QualityProfileExistsValidator qualityProfileExistsValidator) + : base(signalRBroadcaster, importListFactory, "importlist", ResourceMapper, BulkResourceMapper) { SharedValidator.RuleFor(c => c.RootFolderPath).Cascade(CascadeMode.Stop) .IsValidPath() diff --git a/src/Sonarr.Api.V3/Indexers/IndexerController.cs b/src/Sonarr.Api.V3/Indexers/IndexerController.cs index fffbc4d96..90d4d6b60 100644 --- a/src/Sonarr.Api.V3/Indexers/IndexerController.cs +++ b/src/Sonarr.Api.V3/Indexers/IndexerController.cs @@ -1,5 +1,6 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Validation; +using NzbDrone.SignalR; using Sonarr.Http; namespace Sonarr.Api.V3.Indexers @@ -10,8 +11,10 @@ namespace Sonarr.Api.V3.Indexers public static readonly IndexerResourceMapper ResourceMapper = new (); public static readonly IndexerBulkResourceMapper BulkResourceMapper = new (); - public IndexerController(IndexerFactory indexerFactory, DownloadClientExistsValidator downloadClientExistsValidator) - : base(indexerFactory, "indexer", ResourceMapper, BulkResourceMapper) + public IndexerController(IBroadcastSignalRMessage signalRBroadcaster, + IndexerFactory indexerFactory, + DownloadClientExistsValidator downloadClientExistsValidator) + : base(signalRBroadcaster, indexerFactory, "indexer", ResourceMapper, BulkResourceMapper) { SharedValidator.RuleFor(c => c.DownloadClientId).SetValidator(downloadClientExistsValidator); } diff --git a/src/Sonarr.Api.V3/Metadata/MetadataController.cs b/src/Sonarr.Api.V3/Metadata/MetadataController.cs index 006cab8ba..08f248284 100644 --- a/src/Sonarr.Api.V3/Metadata/MetadataController.cs +++ b/src/Sonarr.Api.V3/Metadata/MetadataController.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Extras.Metadata; +using NzbDrone.SignalR; using Sonarr.Http; namespace Sonarr.Api.V3.Metadata @@ -11,8 +12,8 @@ namespace Sonarr.Api.V3.Metadata public static readonly MetadataResourceMapper ResourceMapper = new (); public static readonly MetadataBulkResourceMapper BulkResourceMapper = new (); - public MetadataController(IMetadataFactory metadataFactory) - : base(metadataFactory, "metadata", ResourceMapper, BulkResourceMapper) + public MetadataController(IBroadcastSignalRMessage signalRBroadcaster, IMetadataFactory metadataFactory) + : base(signalRBroadcaster, metadataFactory, "metadata", ResourceMapper, BulkResourceMapper) { } diff --git a/src/Sonarr.Api.V3/Notifications/NotificationController.cs b/src/Sonarr.Api.V3/Notifications/NotificationController.cs index b20c0fae7..ff69bf3d5 100644 --- a/src/Sonarr.Api.V3/Notifications/NotificationController.cs +++ b/src/Sonarr.Api.V3/Notifications/NotificationController.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Notifications; +using NzbDrone.SignalR; using Sonarr.Http; namespace Sonarr.Api.V3.Notifications @@ -11,8 +12,8 @@ namespace Sonarr.Api.V3.Notifications public static readonly NotificationResourceMapper ResourceMapper = new (); public static readonly NotificationBulkResourceMapper BulkResourceMapper = new (); - public NotificationController(NotificationFactory notificationFactory) - : base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper) + public NotificationController(IBroadcastSignalRMessage signalRBroadcaster, NotificationFactory notificationFactory) + : base(signalRBroadcaster, notificationFactory, "notification", ResourceMapper, BulkResourceMapper) { } diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index 04b3c6b73..20f4a3f87 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -5,14 +5,21 @@ using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.Validation; +using NzbDrone.SignalR; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3 { - public abstract class ProviderControllerBase : RestController + public abstract class ProviderControllerBase : RestControllerWithSignalR, + IHandle>, + IHandle>, + IHandle> where TProviderDefinition : ProviderDefinition, new() where TProvider : IProvider where TProviderResource : ProviderResource, new() @@ -22,11 +29,13 @@ namespace Sonarr.Api.V3 private readonly ProviderResourceMapper _resourceMapper; private readonly ProviderBulkResourceMapper _bulkResourceMapper; - protected ProviderControllerBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper, ProviderBulkResourceMapper bulkResourceMapper) + : base(signalRBroadcaster) { _providerFactory = providerFactory; _resourceMapper = resourceMapper; @@ -263,6 +272,24 @@ namespace Sonarr.Api.V3 return Content(data.ToJson(), "application/json"); } + [NonAction] + public virtual void Handle(ProviderAddedEvent message) + { + BroadcastResourceChange(ModelAction.Created, message.Definition.Id); + } + + [NonAction] + public virtual void Handle(ProviderUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Definition.Id); + } + + [NonAction] + public virtual void Handle(ProviderDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.ProviderId); + } + private void Validate(TProviderDefinition definition, bool includeWarnings) { var validationResult = definition.Settings.Validate(); From 10b55bbee656774a81541904d6dbb2fd5c8c9b7a Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 27 Oct 2024 00:18:11 +0300 Subject: [PATCH 598/762] Fixed: Natural sorting for tags list in the UI Closes #7295 --- frontend/src/Settings/Tags/TagsConnector.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 9f8bdee5b..15f31d3c5 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -4,11 +4,13 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - (state) => state.tags, + createSortedSectionSelector('tags', sortByProp('label')), (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; From 0bc4903954b955fce0c368ef7fd2a6f3761d6a93 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 17 Oct 2024 04:46:15 +0300 Subject: [PATCH 599/762] Inherit trigger from pushed command models --- .../Messaging/Commands/CommandQueueManager.cs | 7 ++++--- src/Sonarr.Api.V3/Commands/CommandController.cs | 3 +-- src/Sonarr.Api.V3/Series/SeriesController.cs | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index 7be373e79..6f7f6d705 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -106,6 +106,8 @@ namespace NzbDrone.Core.Messaging.Commands _logger.Trace("Publishing {0}", command.Name); _logger.Trace("Checking if command is queued or started: {0}", command.Name); + command.Trigger = trigger; + lock (_commandQueue) { var existingCommands = QueuedOrStarted(command.Name); @@ -142,7 +144,6 @@ namespace NzbDrone.Core.Messaging.Commands var command = GetCommand(commandName); command.LastExecutionTime = lastExecutionTime; command.LastStartTime = lastStartTime; - command.Trigger = trigger; return Push(command, priority, trigger); } @@ -244,13 +245,13 @@ namespace NzbDrone.Core.Messaging.Commands _repo.Trim(); } - private dynamic GetCommand(string commandName) + private Command GetCommand(string commandName) { commandName = commandName.Split('.').Last(); var commands = _knownTypes.GetImplementations(typeof(Command)); var commandType = commands.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)); - return Json.Deserialize("{}", commandType); + return Json.Deserialize("{}", commandType) as Command; } private void Update(CommandModel command, CommandStatus status, string message) diff --git a/src/Sonarr.Api.V3/Commands/CommandController.cs b/src/Sonarr.Api.V3/Commands/CommandController.cs index 980a6f875..a0203eb09 100644 --- a/src/Sonarr.Api.V3/Commands/CommandController.cs +++ b/src/Sonarr.Api.V3/Commands/CommandController.cs @@ -66,9 +66,8 @@ namespace Sonarr.Api.V3.Commands ? CommandPriority.High : CommandPriority.Normal; - dynamic command = STJson.Deserialize(body, commandType); + var command = STJson.Deserialize(body, commandType) as Command; - command.Trigger = CommandTrigger.Manual; command.SuppressMessages = !command.SendUpdatesToClient; command.SendUpdatesToClient = true; command.ClientUserAgent = Request.Headers["UserAgent"]; diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 72bf0ee41..7122af7e9 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -190,9 +190,8 @@ namespace Sonarr.Api.V3.Series { SeriesId = series.Id, SourcePath = sourcePath, - DestinationPath = destinationPath, - Trigger = CommandTrigger.Manual - }); + DestinationPath = destinationPath + }, trigger: CommandTrigger.Manual); } var model = seriesResource.ToModel(series); From df672487cf1d5f067849367a2bfb0068defc315d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 18 Oct 2024 05:45:03 +0300 Subject: [PATCH 600/762] Improve message for grab errors due to no matching tags Co-authored-by: zakary --- .../Download/DownloadClientProviderFixture.cs | 10 +--------- .../Download/DownloadClientProvider.cs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs index cb326ba7a..6733bcde8 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs @@ -200,17 +200,9 @@ namespace NzbDrone.Core.Test.Download var seriesTags = new HashSet { 2 }; var clientTags = new HashSet { 1 }; - WithTorrentClient(0, clientTags); - WithTorrentClient(0, clientTags); - WithTorrentClient(0, clientTags); WithTorrentClient(0, clientTags); - var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); - var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); - var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); - var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); - - Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull(); + Assert.Throws(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags)); } [Test] diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 769928f2f..7497c6eac 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -41,18 +41,23 @@ namespace NzbDrone.Core.Download var blockedProviders = new HashSet(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId)); var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList(); - if (tags != null) + if (!availableProviders.Any()) + { + return null; + } + + if (tags is { Count: > 0 }) { var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList(); availableProviders = matchingTagsClients.Count > 0 ? matchingTagsClients : availableProviders.Where(i => i.Definition.Tags.Empty()).ToList(); - } - if (!availableProviders.Any()) - { - return null; + if (!availableProviders.Any()) + { + throw new DownloadClientUnavailableException("No download client was found without tags or a matching series tag. Please check your settings."); + } } if (indexerId > 0) From 404e6d68ea526ab521cd39ecda1bf3b02285765d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 19 Oct 2024 06:25:54 +0300 Subject: [PATCH 601/762] Cleanse exceptions in event logs --- .../Instrumentation/DatabaseTarget.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs index 181e60278..e67656589 100644 --- a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs +++ b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs @@ -60,33 +60,36 @@ namespace NzbDrone.Core.Instrumentation { try { - var log = new Log(); - log.Time = logEvent.TimeStamp; - log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage); - - log.Logger = logEvent.LoggerName; + var log = new Log + { + Time = logEvent.TimeStamp, + Logger = logEvent.LoggerName, + Level = logEvent.Level.Name + }; if (log.Logger.StartsWith("NzbDrone.")) { log.Logger = log.Logger.Remove(0, 9); } + var message = logEvent.FormattedMessage; + if (logEvent.Exception != null) { - if (string.IsNullOrWhiteSpace(log.Message)) + if (string.IsNullOrWhiteSpace(message)) { - log.Message = logEvent.Exception.Message; + message = logEvent.Exception.Message; } else { - log.Message += ": " + logEvent.Exception.Message; + message += ": " + logEvent.Exception.Message; } - log.Exception = logEvent.Exception.ToString(); + log.Exception = CleanseLogMessage.Cleanse(logEvent.Exception.ToString()); log.ExceptionType = logEvent.Exception.GetType().ToString(); } - log.Level = logEvent.Level.Name; + log.Message = CleanseLogMessage.Cleanse(message); var connectionInfo = _connectionStringFactory.LogDbConnection; From fcf68d92595b497050b22de9b28bbf2c856703c9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 20 Oct 2024 04:52:01 +0300 Subject: [PATCH 602/762] Fix settings fetching failure for updates --- frontend/src/System/Updates/Updates.tsx | 12 +++++++++--- src/NzbDrone.Core/Localization/Core/de.json | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 10 +++++----- src/NzbDrone.Core/Localization/Core/es.json | 2 +- src/NzbDrone.Core/Localization/Core/fi.json | 2 +- src/NzbDrone.Core/Localization/Core/fr.json | 2 +- src/NzbDrone.Core/Localization/Core/it.json | 2 +- src/NzbDrone.Core/Localization/Core/pt_BR.json | 2 +- src/NzbDrone.Core/Localization/Core/ru.json | 2 +- src/NzbDrone.Core/Localization/Core/zh_CN.json | 2 +- 10 files changed, 22 insertions(+), 16 deletions(-) diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index af4235cec..452d6ba56 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -77,7 +77,7 @@ function Updates() { const hasUpdates = isPopulated && !hasError && items.length > 0; const noUpdates = isPopulated && !hasError && !items.length; - const externalUpdaterPrefix = translate('UpdateSonarrDirectlyLoadError'); + const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); const externalUpdaterMessages: Partial> = { external: translate('ExternalUpdater'), apt: translate('AptUpdater'), @@ -262,10 +262,16 @@ function Updates() {
)} - {updatesError ?
{translate('FailedToFetchUpdates')}
: null} + {updatesError ? ( + + {translate('FailedToFetchUpdates')} + + ) : null} {generalSettingsError ? ( -
{translate('FailedToUpdateSettings')}
+ + {translate('FailedToFetchSettings')} + ) : null} Date: Sun, 27 Oct 2024 00:19:47 +0300 Subject: [PATCH 603/762] Fixed: Initial state for qBittorrent v5.0 --- .../QBittorrent/QBittorrentProxySelector.cs | 2 - .../Clients/QBittorrent/QBittorrentProxyV1.cs | 24 ++--------- .../Clients/QBittorrent/QBittorrentProxyV2.cs | 40 +++++++------------ .../Clients/QBittorrent/QBittorrentState.cs | 9 ++++- 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 85279be95..e702eef0d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -26,8 +26,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Dictionary GetLabels(QBittorrentSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); - void PauseTorrent(string hash, QBittorrentSettings settings); - void ResumeTorrent(string hash, QBittorrentSettings settings); void SetForceStart(string hash, bool enabled, QBittorrentSettings settings); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index 2432a47d0..cd74c91c7 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) { request.AddFormParameter("paused", true); } @@ -177,7 +177,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) { request.AddFormParameter("paused", true); } @@ -213,7 +213,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) { var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel") .Post() @@ -255,7 +255,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden) { return; } @@ -264,22 +264,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void PauseTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/command/pause") - .Post() - .AddFormParameter("hash", hash); - ProcessRequest(request, settings); - } - - public void ResumeTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/command/resume") - .Post() - .AddFormParameter("hash", hash); - ProcessRequest(request, settings); - } - public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/command/setForceStart") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index ab0c2e5a9..07e65acd9 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -247,14 +247,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.TvCategory); } - // Note: ForceStart is handled by separate api call - if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + // Avoid extraneous API version check if initial state is ForceStart + if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop) { - request.AddFormParameter("paused", false); - } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) - { - request.AddFormParameter("paused", true); + var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused"; + + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter(stoppedParameterName, false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) + { + request.AddFormParameter(stoppedParameterName, true); + } } if (settings.SequentialOrder) @@ -292,7 +298,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0 - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) { return; } @@ -314,7 +320,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict) { return; } @@ -323,22 +329,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void PauseTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/api/v2/torrents/pause") - .Post() - .AddFormParameter("hashes", hash); - ProcessRequest(request, settings); - } - - public void ResumeTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/api/v2/torrents/resume") - .Post() - .AddFormParameter("hashes", hash); - ProcessRequest(request, settings); - } - public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs index 56c5ddf1a..b8fddbc11 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs @@ -1,9 +1,16 @@ +using NzbDrone.Core.Annotations; + namespace NzbDrone.Core.Download.Clients.QBittorrent { public enum QBittorrentState { + [FieldOption(Label = "Started")] Start = 0, + + [FieldOption(Label = "Force Started")] ForceStart = 1, - Pause = 2 + + [FieldOption(Label = "Stopped")] + Stop = 2 } } From 240a0339bee7d407e564df6b6575a2ade6ac03cd Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 25 Oct 2024 09:42:55 +0300 Subject: [PATCH 604/762] Fixed: Changing series to another root folder without moving files --- .../PathExtensionFixture.cs | 20 +++++++++++++++++++ .../Extensions/PathExtensions.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index f0c5c3f98..d06d2d9a8 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -414,5 +414,25 @@ namespace NzbDrone.Common.Test { path.GetCleanPath().Should().Be(cleanPath); } + + [TestCase(@"C:\Test\", @"C:\Test\Series Title", "Series Title")] + [TestCase(@"C:\Test\", @"C:\Test\Collection\Series Title", @"Collection\Series Title")] + [TestCase(@"C:\Test\mydir\", @"C:\Test\mydir\Collection\Series Title", @"Collection\Series Title")] + [TestCase(@"\\server\share", @"\\server\share\Series Title", "Series Title")] + [TestCase(@"\\server\share\mydir\", @"\\server\share\mydir\/Collection\Series Title", @"Collection\Series Title")] + public void windows_path_should_return_relative_path(string parentPath, string childPath, string relativePath) + { + parentPath.GetRelativePath(childPath).Should().Be(relativePath); + } + + [TestCase(@"/test", "/test/Series Title", "Series Title")] + [TestCase(@"/test/", "/test/Collection/Series Title", "Collection/Series Title")] + [TestCase(@"/test/mydir", "/test/mydir/Series Title", "Series Title")] + [TestCase(@"/test/mydir/", "/test/mydir/Collection/Series Title", "Collection/Series Title")] + [TestCase(@"/test/mydir/", @"/test/mydir/\Collection/Series Title", "Collection/Series Title")] + public void unix_path_should_return_relative_path(string parentPath, string childPath, string relativePath) + { + parentPath.GetRelativePath(childPath).Should().Be(relativePath); + } } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 1dd7952d3..3245ca580 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -85,7 +85,7 @@ namespace NzbDrone.Common.Extensions throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); } - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); + return childPath.Substring(parentPath.Length).Trim('\\', '/'); } public static string GetParentPath(this string childPath) From 8a558b379a8b7262f04bfb86bcb466d8600cae1a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 12 Oct 2024 16:14:22 -0700 Subject: [PATCH 605/762] New: Maintain '...' in naming format Closes #7290 --- .../FileNameBuilderTests/FileNameBuilderFixture.cs | 9 +++++++++ src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 710dfe30a..f20c39d37 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -1027,6 +1027,15 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(string.Empty); } + [Test] + public void should_maintain_ellipsis_in_naming_format() + { + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}.E{episode:00}...{Episode.CleanTitle}"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.S15.E06...City.Sushi"); + } + private void GivenMediaInfoModel(string videoCodec = "h264", string audioCodec = "dts", int audioChannels = 6, diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 2ee358bef..08ec527ba 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -187,6 +187,7 @@ namespace NzbDrone.Core.Organizer splitPattern = AddSeasonEpisodeNumberingTokens(splitPattern, tokenHandlers, episodes, namingConfig); splitPattern = AddAbsoluteNumberingTokens(splitPattern, tokenHandlers, series, episodes, namingConfig); + splitPattern = splitPattern.Replace("...", "{{ellipsis}}"); UpdateMediaInfoIfNeeded(splitPattern, episodeFile, series); From 41ddacc395e783b361b7766cd812f7114b36ac46 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 12 Oct 2024 17:11:33 -0700 Subject: [PATCH 606/762] New: Improve parsing absolute followed by standard numbering Closes #7246 --- .../AbsoluteEpisodeNumberParserFixture.cs | 21 +++++++++++++++++++ src/NzbDrone.Core/Parser/Parser.cs | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 411ad213e..13fd2f60d 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -137,6 +137,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series_Title_2_[01]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 0, 0)] [TestCase("[SubsPlease] Series Title - 100 Years Quest - 01 (1080p) [1107F3A9].mkv", "Series Title - 100 Years Quest", 1, 0, 0)] [TestCase("[SubsPlease] Series Title 100 Years Quest - 01 (1080p) [1107F3A9].mkv", "Series Title 100 Years Quest", 1, 0, 0)] + [TestCase("[Dae-P9] Anime Series - 05 - S01E05 - Marrying by Contesting (BD 1080p HEVC FLAC AAC) [Dual Audio] [5BCD56B8]", "Anime Series", 5, 1, 5)] + [TestCase("[Kaleido-subs] Animation - 12 (S01E12) - (WEB 1080p HEVC x265 10-bit E-AC3 2.0) [1ADD8F6D]", "Animation", 12, 1, 12)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) @@ -235,5 +237,24 @@ namespace NzbDrone.Core.Test.ParserTests result.SeriesTitle.Should().Be(title); result.FullSeason.Should().BeFalse(); } + + [TestCase("[Dae-P9] Anime Series - 05.5 - S00E01 - Marrying by Contesting (BD 1080p HEVC FLAC AAC) [Dual Audio] [5BCD56B8]", "Anime Series", 5.5, 0, 1)] + [TestCase("[Thighs] Anime Reincarnation - 17.5 (S00E01) (BD 1080p FLAC AAC) [Dual-Audio] [A6E2110E]", "Anime Reincarnation", 17.5, 0, 1)] + [TestCase("[sam] Anime - 15.5 (S00E01) [BD 1080p FLAC] [3E8D676D]", "Anime", 15.5, 0, 1)] + [TestCase("[Kaleido-subs] Sky Series - 07.5 (S00E01) - (BD 1080p HEVC x265 10-bit Opus 2.0) [A548C980].mkv", "Sky Series", 7.5, 0, 1)] + public void should_parse_absolute_followed_by_standard_as_standard(string releaseName, string title, decimal specialEpisodeNumber, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(releaseName); + + result.Should().NotBeNull(); + result.EpisodeNumbers.Should().HaveCount(1); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.First().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title); + result.SpecialAbsoluteEpisodeNumbers.Should().HaveCount(1); + result.SpecialAbsoluteEpisodeNumbers.First().Should().Be(specialEpisodeNumber); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index d219fc884..12d481c0d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -88,6 +88,14 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - [SubGroup] Title Absolute (Season+Episode) + new Regex(@"^(?:\[(?.+?)\](?:_|-|\s|\.)?)(?.+?)[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ])+\((?:S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+))))(?:v\d+)?(?:\)(?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + // Anime - [SubGroup] Title Absolute - Season+Episode + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ](?<![()\[!]))+(?:S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+))))(?:v\d+)?(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", + 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+)))+)(?:v\d+)?(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 03b9c957b81f83559d095f5ab915f5d357f3b4a6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 14:20:55 -0700 Subject: [PATCH 607/762] New: Episode mappings in .plexmatch metadata files Closes #5784 --- src/NzbDrone.Core/Extras/ExtraService.cs | 31 ++++++++++++++++- .../Extras/Files/ExtraFileManager.cs | 2 ++ .../Consumers/Kometa/KometaMetadata.cs | 2 +- .../Metadata/Consumers/Plex/PlexMetadata.cs | 31 ++++++++++++++++- .../Consumers/Plex/PlexMetadataSettings.cs | 3 ++ .../Consumers/Roksbox/RoksboxMetadata.cs | 2 +- .../Metadata/Consumers/Wdtv/WdtvMetadata.cs | 2 +- .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 7 +++- .../Extras/Metadata/IMetadata.cs | 11 +++++-- .../Extras/Metadata/MetadataBase.cs | 2 +- .../Extras/Metadata/MetadataService.cs | 33 ++++++++++++++++--- .../Extras/Others/OtherExtraService.cs | 5 +++ .../Extras/Subtitles/SubtitleService.cs | 5 +++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 14 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index b29fa6d17..5742b7610 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -5,6 +5,7 @@ using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; @@ -25,13 +26,15 @@ namespace NzbDrone.Core.Extras IHandle<MediaCoversUpdatedEvent>, IHandle<EpisodeFolderCreatedEvent>, IHandle<SeriesScannedEvent>, - IHandle<SeriesRenamedEvent> + IHandle<SeriesRenamedEvent>, + IHandle<DownloadsProcessedEvent> { private readonly IMediaFileService _mediaFileService; private readonly IEpisodeService _episodeService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List<IManageExtraFiles> _extraFileManagers; + private readonly Dictionary<int, Series> _seriesWithImportedFiles; public ExtraService(IMediaFileService mediaFileService, IEpisodeService episodeService, @@ -45,6 +48,7 @@ namespace NzbDrone.Core.Extras _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); + _seriesWithImportedFiles = new Dictionary<int, Series>(); } public void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) @@ -100,6 +104,11 @@ namespace NzbDrone.Core.Extras private void CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { + lock (_seriesWithImportedFiles) + { + _seriesWithImportedFiles.TryAdd(series.Id, series); + } + foreach (var extraFileManager in _extraFileManagers) { extraFileManager.CreateAfterEpisodeImport(series, episodeFile); @@ -161,6 +170,26 @@ namespace NzbDrone.Core.Extras } } + public void Handle(DownloadsProcessedEvent message) + { + var allSeries = new List<Series>(); + + lock (_seriesWithImportedFiles) + { + allSeries.AddRange(_seriesWithImportedFiles.Values); + + _seriesWithImportedFiles.Clear(); + } + + foreach (var series in allSeries) + { + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterEpisodesImported(series); + } + } + } + private List<EpisodeFile> GetEpisodeFiles(int seriesId) { var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index 4ea272f67..1b9179689 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Extras.Files int Order { get; } IEnumerable<ExtraFile> CreateAfterMediaCoverUpdate(Series series); IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); + IEnumerable<ExtraFile> CreateAfterEpisodesImported(Series series); IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); IEnumerable<ExtraFile> CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); @@ -46,6 +47,7 @@ namespace NzbDrone.Core.Extras.Files public abstract int Order { get; } public abstract IEnumerable<ExtraFile> CreateAfterMediaCoverUpdate(Series series); public abstract IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); + public abstract IEnumerable<ExtraFile> CreateAfterEpisodesImported(Series series); public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); public abstract IEnumerable<ExtraFile> CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs index 4a0bd6c2f..7085b3ddf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -92,7 +92,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { return null; } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs index bfe8e7220..df56a72af 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; @@ -10,6 +11,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex { public class PlexMetadata : MetadataBase<PlexMetadataSettings> { + private readonly IEpisodeService _episodeService; + private readonly IMediaFileService _mediaFileService; + + public PlexMetadata(IEpisodeService episodeService, IMediaFileService mediaFileService) + { + _episodeService = episodeService; + _mediaFileService = mediaFileService; + } + public override string Name => "Plex"; public override MetadataFile FindMetadataFile(Series series, string path) @@ -37,7 +47,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { if (!Settings.SeriesPlexMatchFile) { @@ -51,6 +61,25 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex content.AppendLine($"TvdbId: {series.TvdbId}"); content.AppendLine($"ImdbId: {series.ImdbId}"); + if (Settings.EpisodeMappings) + { + var episodes = _episodeService.GetEpisodeBySeries(series.Id); + var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); + + foreach (var episodeFile in episodeFiles) + { + var episodesInFile = episodes.Where(e => e.EpisodeFileId == episodeFile.Id); + var episodeFormat = $"S{episodeFile.SeasonNumber:00}{string.Join("-", episodesInFile.Select(e => $"E{e.EpisodeNumber:00}"))}"; + + if (episodeFile.SeasonNumber == 0) + { + episodeFormat = $"SP{episodesInFile.First():00}"; + } + + content.Append($"Episode: {episodeFormat}: {episodeFile.RelativePath}"); + } + } + return new MetadataFileResult(".plexmatch", content.ToString()); } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs index de94f5153..3558290c5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs @@ -21,6 +21,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex [FieldDefinition(0, Label = "MetadataPlexSettingsSeriesPlexMatchFile", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataPlexSettingsSeriesPlexMatchFileHelpText")] public bool SeriesPlexMatchFile { get; set; } + [FieldDefinition(0, Label = "MetadataPlexSettingsEpisodeMappings", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataPlexSettingsEpisodeMappingsHelpText")] + public bool EpisodeMappings { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs index 095225fd6..ee98606a9 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { // Series metadata is not supported return null; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs index 3888d15f3..429bb808b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { // Series metadata is not supported return null; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index a65168bdf..5450a16f3 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -137,8 +137,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { + if (reason == SeriesMetadataReason.EpisodesImported) + { + return null; + } + var xmlResult = string.Empty; if (Settings.SeriesMetadata) diff --git a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs index b631425e6..b9232fa6e 100644 --- a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.ThingiProvider; @@ -10,10 +10,17 @@ namespace NzbDrone.Core.Extras.Metadata { string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); MetadataFile FindMetadataFile(Series series, string path); - MetadataFileResult SeriesMetadata(Series series); + MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason); MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); List<ImageFileResult> SeriesImages(Series series); List<ImageFileResult> SeasonImages(Series series, Season season); List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile); } + + public enum SeriesMetadataReason + { + Scan, + EpisodeFolderCreated, + EpisodesImported + } } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs index ecfa7b855..369c9fe76 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Extras.Metadata public abstract MetadataFile FindMetadataFile(Series series, string path); - public abstract MetadataFileResult SeriesMetadata(Series series); + public abstract MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason); public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); public abstract List<ImageFileResult> SeriesImages(Series series); public abstract List<ImageFileResult> SeasonImages(Series series, Season season); diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 1cf253e81..5b08aa384 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Extras.Metadata { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.Scan)); files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); @@ -115,6 +115,31 @@ namespace NzbDrone.Core.Extras.Metadata return files; } + public override IEnumerable<ExtraFile> CreateAfterEpisodesImported(Series series) + { + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + _cleanMetadataService.Clean(series); + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Info("Series folder does not exist, skipping metadata creation"); + return Enumerable.Empty<MetadataFile>(); + } + + var files = new List<MetadataFile>(); + + foreach (var consumer in _metadataFactory.Enabled()) + { + var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); + + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.EpisodesImported)); + } + + _metadataFileService.Upsert(files); + + return files; + } + public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { var files = new List<MetadataFile>(); @@ -147,7 +172,7 @@ namespace NzbDrone.Core.Extras.Metadata if (seriesFolder.IsNotNullOrWhiteSpace()) { - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.EpisodeFolderCreated)); files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); } @@ -218,9 +243,9 @@ namespace NzbDrone.Core.Extras.Metadata return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } - private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) + private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles, SeriesMetadataReason reason) { - var seriesMetadata = consumer.SeriesMetadata(series); + var seriesMetadata = consumer.SeriesMetadata(series, reason); if (seriesMetadata == null) { diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index 1a1d167c9..4567dbe08 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -46,6 +46,11 @@ namespace NzbDrone.Core.Extras.Others return Enumerable.Empty<ExtraFile>(); } + public override IEnumerable<ExtraFile> CreateAfterEpisodesImported(Series series) + { + return Enumerable.Empty<ExtraFile>(); + } + public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { return Enumerable.Empty<ExtraFile>(); diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs index 465783f16..18e4cbdca 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -53,6 +53,11 @@ namespace NzbDrone.Core.Extras.Subtitles return Enumerable.Empty<SubtitleFile>(); } + public override IEnumerable<ExtraFile> CreateAfterEpisodesImported(Series series) + { + return Enumerable.Empty<SubtitleFile>(); + } + public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { return Enumerable.Empty<SubtitleFile>(); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f9d6383e4..5c8616c83 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1151,6 +1151,8 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataPlexSettingsEpisodeMappings": "Episode Mappings", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file", "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File", "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Creates a .plexmatch file in the series folder", "MetadataProvidedBy": "Metadata is provided by {provider}", From de69d8ec7ec8f1bcb4d4e5d47959ba01380286d1 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 25 Oct 2024 16:54:07 -0700 Subject: [PATCH 608/762] Update JetBrains logos --- Logo/Jetbrains/dottrace.svg | 33 ------------------ Logo/Jetbrains/jetbrains.svg | 66 ------------------------------------ Logo/Jetbrains/resharper.svg | 50 --------------------------- Logo/Jetbrains/teamcity.svg | 64 ---------------------------------- README.md | 22 +++++++----- 5 files changed, 13 insertions(+), 222 deletions(-) delete mode 100644 Logo/Jetbrains/dottrace.svg delete mode 100644 Logo/Jetbrains/jetbrains.svg delete mode 100644 Logo/Jetbrains/resharper.svg delete mode 100644 Logo/Jetbrains/teamcity.svg diff --git a/Logo/Jetbrains/dottrace.svg b/Logo/Jetbrains/dottrace.svg deleted file mode 100644 index b879517cd..000000000 --- a/Logo/Jetbrains/dottrace.svg +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve"> -<g> - <g> - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967"> - <stop offset="0.1237" style="stop-color:#7866FF"/> - <stop offset="0.5376" style="stop-color:#FE2EB6"/> - <stop offset="0.8548" style="stop-color:#FD0486"/> - </linearGradient> - <polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/> - <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989"> - <stop offset="0.1237" style="stop-color:#FF0080"/> - <stop offset="0.2587" style="stop-color:#FE0385"/> - <stop offset="0.4109" style="stop-color:#FA0C92"/> - <stop offset="0.5713" style="stop-color:#F41BA9"/> - <stop offset="0.7363" style="stop-color:#EB2FC8"/> - <stop offset="0.8656" style="stop-color:#E343E6"/> - </linearGradient> - <polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/> - </g> - <g> - <rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/> - <rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/> - <g> - <path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3 - c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/> - <polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/> - </g> - </g> -</g> -</svg> diff --git a/Logo/Jetbrains/jetbrains.svg b/Logo/Jetbrains/jetbrains.svg deleted file mode 100644 index 75d4d2177..000000000 --- a/Logo/Jetbrains/jetbrains.svg +++ /dev/null @@ -1,66 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve" - > -<g> - <linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24"> - <stop offset="0" style="stop-color:#FCEE39"/> - <stop offset="1" style="stop-color:#F37B3D"/> - </linearGradient> - <path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9 - c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0 - c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/> - <linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546"> - <stop offset="0" style="stop-color:#EF5A6B"/> - <stop offset="0.57" style="stop-color:#F26F4E"/> - <stop offset="1" style="stop-color:#F37B3D"/> - </linearGradient> - <path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0 - c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3 - c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/> - <linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562"> - <stop offset="0" style="stop-color:#7C59A4"/> - <stop offset="0.3852" style="stop-color:#AF4C92"/> - <stop offset="0.7654" style="stop-color:#DC4183"/> - <stop offset="0.957" style="stop-color:#ED3D7D"/> - </linearGradient> - <path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9 - c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0 - c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/> - <linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971"> - <stop offset="0" style="stop-color:#EF5A6B"/> - <stop offset="0.364" style="stop-color:#EE4E72"/> - <stop offset="1" style="stop-color:#ED3D7D"/> - </linearGradient> - <path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0 - l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0 - c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/> - <g id="XMLID_3008_"> - <rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/> - <rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/> - <g id="XMLID_3009_"> - <path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0 - l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/> - <path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0 - L45.3,43.8z"/> - <path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/> - <path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0 - c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5 - l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/> - <path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0 - c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1 - l-1.5,0v2H50.6z"/> - <path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z - M58.8,59l-0.9-2.3L57,59L58.8,59z"/> - <path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/> - <path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z" - /> - <path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0 - c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6 - c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7 - C76.1,62.5,74.7,62,73.7,61.1z"/> - </g> - </g> -</g> -</svg> diff --git a/Logo/Jetbrains/resharper.svg b/Logo/Jetbrains/resharper.svg deleted file mode 100644 index 24c987a78..000000000 --- a/Logo/Jetbrains/resharper.svg +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve"> -<g> - <g> - <g> - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415"> - <stop offset="1.612903e-002" style="stop-color:#B35BA3"/> - <stop offset="0.4044" style="stop-color:#C41E57"/> - <stop offset="0.4677" style="stop-color:#C41E57"/> - <stop offset="0.6505" style="stop-color:#EB8523"/> - <stop offset="0.9516" style="stop-color:#FEBD11"/> - </linearGradient> - <polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/> - <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519"> - <stop offset="1.612903e-002" style="stop-color:#B35BA3"/> - <stop offset="0.4044" style="stop-color:#C41E57"/> - <stop offset="0.4677" style="stop-color:#C41E57"/> - <stop offset="0.7043" style="stop-color:#EB8523"/> - </linearGradient> - <polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/> - </g> - <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255"> - <stop offset="1.612903e-002" style="stop-color:#B35BA3"/> - <stop offset="0.6613" style="stop-color:#C41E57"/> - </linearGradient> - <polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/> - <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352"> - <stop offset="0.5" style="stop-color:#C41E57"/> - <stop offset="0.6668" style="stop-color:#D13F48"/> - <stop offset="0.7952" style="stop-color:#D94F39"/> - <stop offset="0.8656" style="stop-color:#DD5433"/> - </linearGradient> - <polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/> - </g> - <g> - - <rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/> - - <rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/> - <path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5 - c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7 - c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/> - <path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7 - l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z - M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/> - </g> -</g> -</svg> diff --git a/Logo/Jetbrains/teamcity.svg b/Logo/Jetbrains/teamcity.svg deleted file mode 100644 index ca14b3dc1..000000000 --- a/Logo/Jetbrains/teamcity.svg +++ /dev/null @@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve"> -<g> - <g> - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.7738" y1="31.2729" x2="40.1662" y2="31.2729"> - <stop offset="0" style="stop-color:#905CFB"/> - <stop offset="6.772543e-002" style="stop-color:#776CF9"/> - <stop offset="0.1729" style="stop-color:#5681F7"/> - <stop offset="0.2865" style="stop-color:#3B92F5"/> - <stop offset="0.4097" style="stop-color:#269FF4"/> - <stop offset="0.5474" style="stop-color:#17A9F3"/> - <stop offset="0.7111" style="stop-color:#0FAEF2"/> - <stop offset="0.9677" style="stop-color:#0CB0F2"/> - </linearGradient> - <path style="fill:url(#SVGID_1_);" d="M39.7,47.9l-6.1-34c-0.4-2.4-1.2-4.8-2.7-7.1c-2-3.2-5.2-5.4-8.8-6.3 - C7.9-2.9-2.6,11.3,3.6,23.9c0,0,0,0,0,0l14.8,31.7c0.4,1,1,2,1.7,2.9c1.2,1.6,2.8,2.8,4.7,3.4C34.4,64.9,42.1,56.4,39.7,47.9z"/> - <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5.3113" y1="9.6691" x2="69.2278" y2="43.8664"> - <stop offset="0" style="stop-color:#905CFB"/> - <stop offset="6.772543e-002" style="stop-color:#776CF9"/> - <stop offset="0.1729" style="stop-color:#5681F7"/> - <stop offset="0.2865" style="stop-color:#3B92F5"/> - <stop offset="0.4097" style="stop-color:#269FF4"/> - <stop offset="0.5474" style="stop-color:#17A9F3"/> - <stop offset="0.7111" style="stop-color:#0FAEF2"/> - <stop offset="0.9677" style="stop-color:#0CB0F2"/> - </linearGradient> - <path style="fill:url(#SVGID_2_);" d="M67.4,26.5c-1.4-2.2-3.4-3.9-5.7-4.9L25.5,1.7l0,0c-1-0.5-2.1-1-3.3-1.3 - C6.7-3.2-4.4,13.8,5.5,27c1.5,2,3.6,3.6,6,4.5L48,47.9c0.8,0.5,1.6,0.8,2.5,1.1C64.5,53.4,75.1,38.6,67.4,26.5z"/> - <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-19.2836" y1="70.8198" x2="55.9833" y2="33.1863"> - <stop offset="0" style="stop-color:#3BEA62"/> - <stop offset="0.117" style="stop-color:#31DE80"/> - <stop offset="0.3025" style="stop-color:#24CEA8"/> - <stop offset="0.4844" style="stop-color:#1AC1C9"/> - <stop offset="0.6592" style="stop-color:#12B7DF"/> - <stop offset="0.8238" style="stop-color:#0EB2ED"/> - <stop offset="0.9677" style="stop-color:#0CB0F2"/> - </linearGradient> - <path style="fill:url(#SVGID_3_);" d="M67.4,26.5c-1.8-2.8-4.6-4.8-7.9-5.6c-3.5-0.8-6.8-0.5-9.6,0.7L11.4,36.1 - c0,0-0.2,0.1-0.6,0.4C0.9,40.4-4,53.3,4,64c1.8,2.4,4.3,4.2,7.1,5c5.3,1.6,10.1,1,14-1.1c0,0,0.1,0,0.1,0l37.6-20.1 - c0,0,0,0,0.1-0.1C69.5,43.9,72.6,34.6,67.4,26.5z"/> - <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="38.9439" y1="5.8503" x2="5.4232" y2="77.5093"> - <stop offset="0" style="stop-color:#3BEA62"/> - <stop offset="9.397750e-002" style="stop-color:#2FDB87"/> - <stop offset="0.196" style="stop-color:#24CEA8"/> - <stop offset="0.3063" style="stop-color:#1BC3C3"/> - <stop offset="0.4259" style="stop-color:#14BAD8"/> - <stop offset="0.5596" style="stop-color:#10B5E7"/> - <stop offset="0.7185" style="stop-color:#0DB1EF"/> - <stop offset="0.9677" style="stop-color:#0CB0F2"/> - </linearGradient> - <path style="fill:url(#SVGID_4_);" d="M50.3,12.8c1.2-2.7,1.1-6-0.9-9c-1.1-1.8-2.9-3-4.9-3.5c-4.5-1.1-8.3,1-10.1,4.2L3.5,42 - c0,0,0,0,0,0.1C-0.9,47.9-1.6,56.5,4,64c1.8,2.4,4.3,4.2,7.1,5c10.5,3.3,19.3-2.5,22.1-10.8L50.3,12.8z"/> - </g> - <g> - <rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/> - <rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/> - <polygon style="fill:#FFFFFF;" points="22.9,22.7 17.5,22.7 17.5,19.1 32.3,19.1 32.3,22.7 26.8,22.7 26.8,37 22.9,37 "/> - <path style="fill:#FFFFFF;" d="M32.5,28.1L32.5,28.1c0-5.1,3.8-9.3,9.3-9.3c3.4,0,5.4,1.1,7.1,2.8l-2.5,2.9c-1.4-1.3-2.8-2-4.6-2 - c-3,0-5.2,2.5-5.2,5.6V28c0,3.1,2.1,5.6,5.2,5.6c2,0,3.3-0.8,4.7-2.1l2.5,2.5c-1.8,2-3.9,3.2-7.3,3.2 - C36.4,37.3,32.5,33.2,32.5,28.1"/> - </g> -</g> -</svg> \ No newline at end of file diff --git a/README.md b/README.md index ef3c2ecea..5636cb739 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr +# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr [![Translated](https://translate.servarr.com/widgets/servarr/-/sonarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/) [![Backers on Open Collective](https://opencollective.com/Sonarr/backers/badge.svg)](#backers) @@ -33,7 +33,7 @@ Note: GitHub Issues are for Bugs and Feature Requests Only - Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. - Automatically detects new episodes - Can scan your existing library and download any missing episodes -- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray* +- Can watch for better quality of the episodes you already have and do an automatic upgrade. _eg. from DVD to Blu-Ray_ - Automatic failed download handling will try another release if one fails - Manual search so you can pick any release or to see why a release was not downloaded automatically - Fully configurable episode renaming @@ -52,7 +52,7 @@ This project exists thanks to all the people who contribute. [Contribute](CONTRI ### Supporters -This project would not be possible without the support of our users and software providers. +This project would not be possible without the support of our users and software providers. [**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out! #### Mega Sponsors @@ -69,13 +69,17 @@ This project would not be possible without the support of our users and software #### JetBrains -Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools +Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools -* [<img src="/Logo/Jetbrains/teamcity.svg" alt="TeamCity" width="32"> TeamCity](http://www.jetbrains.com/teamcity/) -* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/) -* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/) +[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/TeamCity.png" alt="TeamCity" width="64">](http://www.jetbrains.com/teamcity/) + +[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper.png" alt="ReSharper" width="64">](http://www.jetbrains.com/resharper/) + +[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace.png" alt="dotTrace" width="64">](http://www.jetbrains.com/dottrace/) + +[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider.png" alt="Rider" width="64">](http://www.jetbrains.com/rider/) ### Licenses -- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Copyright 2010-2023 +- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) +- Copyright 2010-2024 From 33139d4b53c1adad769c7e2b0510e8990c66b84a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 27 Oct 2024 00:22:16 +0300 Subject: [PATCH 609/762] Fixed: Status check for completed directories in Deluge --- .../DelugeTests/DelugeFixture.cs | 24 +++++++++++++++---- .../Download/Clients/Deluge/Deluge.cs | 13 ++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs index c07a72966..98c78abb0 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs @@ -312,11 +312,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests [Test] public void should_return_status_with_outputdirs() { - var configItems = new Dictionary<string, object>(); - - configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic()); - configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic()); - configItems.Add("move_completed", true); + var configItems = new Dictionary<string, object> + { + { "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() }, + { "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() }, + { "move_completed", true } + }; Mocker.GetMock<IDelugeProxy>() .Setup(v => v.GetConfig(It.IsAny<DelugeSettings>())) @@ -328,5 +329,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests result.OutputRootFolders.Should().NotBeNull(); result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic()); } + + [Test] + public void should_return_status_with_outputdirs_for_directories_in_settings() + { + Subject.Definition.Settings.As<DelugeSettings>().DownloadDirectory = @"D:\Downloads\Downloading\deluge".AsOsAgnostic(); + Subject.Definition.Settings.As<DelugeSettings>().CompletedDirectory = @"D:\Downloads\Finished\deluge".AsOsAgnostic(); + + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"D:\Downloads\Finished\deluge".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 35701d3de..a8d03166e 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -216,9 +216,18 @@ namespace NzbDrone.Core.Download.Clients.Deluge { var config = _proxy.GetConfig(Settings); var label = _proxy.GetLabelOptions(Settings); + OsPath destDir; - if (label != null && label.ApplyMoveCompleted && label.MoveCompleted) + if (Settings.CompletedDirectory.IsNotNullOrWhiteSpace()) + { + destDir = new OsPath(Settings.CompletedDirectory); + } + else if (Settings.DownloadDirectory.IsNotNullOrWhiteSpace()) + { + destDir = new OsPath(Settings.DownloadDirectory); + } + else if (label is { ApplyMoveCompleted: true, MoveCompleted: true }) { // if label exists and a label completed path exists and is enabled use it instead of global destDir = new OsPath(label.MoveCompletedPath); @@ -234,7 +243,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge var status = new DownloadClientInfo { - IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + IsLocalhost = Settings.Host is "127.0.0.1" or "localhost" }; if (!destDir.IsEmpty) From f8a879f4c1a1ef1fb4df915d8308af07114f0422 Mon Sep 17 00:00:00 2001 From: BarbUk <julien.virey@gmail.com> Date: Sat, 26 Oct 2024 10:31:51 +0200 Subject: [PATCH 610/762] Update System.Text.Json to version 6.0.10 --- src/NzbDrone.Common/Sonarr.Common.csproj | 2 +- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 4994793cf..e7acd472a 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -15,7 +15,7 @@ <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> - <PackageReference Include="System.Text.Json" Version="6.0.9" /> + <PackageReference Include="System.Text.Json" Version="6.0.10" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" /> diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index dd25a7184..b62133278 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -24,7 +24,7 @@ <PackageReference Include="NLog" Version="5.3.2" /> <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.9" /> + <PackageReference Include="System.Text.Json" Version="6.0.10" /> <PackageReference Include="Npgsql" Version="7.0.7" /> </ItemGroup> <ItemGroup> From c114e2ddb78e6b6d95ba98bfe2b5c5f2cb4b3ef8 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 26 Oct 2024 21:19:30 +0000 Subject: [PATCH 611/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 1 - src/NzbDrone.Core/Localization/Core/cs.json | 1 - src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 1 - src/NzbDrone.Core/Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 1 - src/NzbDrone.Core/Localization/Core/tr.json | 1 - src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 10 files changed, 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index c6f2c7158..2e2aab539 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -29,7 +29,6 @@ "AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per a prioritzar les funcions i les correccions d'errors.", "AuthenticationRequiredHelpText": "Canvia per a quines sol·licituds cal autenticar. No canvieu tret que entengueu els riscos.", "BypassDelayIfAboveCustomFormatScoreHelpText": "Habiliteu l'omissió quan la versió tingui una puntuació superior a la puntuació mínima per al format personalitzat", - "FailedToUpdateSettings": "No s'ha pogut actualitzar la configuració", "Ended": "Acabat", "Language": "Idioma", "NextAiring": "Propera emissió", diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 6d8e08d2b..fe6aafb59 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -298,7 +298,6 @@ "FormatAgeMinute": "minuta", "FormatAgeMinutes": "minut", "FailedToFetchUpdates": "Nepodařilo se načíst aktualizace", - "FailedToUpdateSettings": "Nepodařilo se aktualizovat nastavení", "EnableProfile": "Povolit profil", "FormatAgeDays": "dnů", "FormatAgeHour": "hodina", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 22212a6b0..e7371281e 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -419,7 +419,6 @@ "FormatTimeSpanDays": "{days}d {time}", "EditConditionImplementation": "Editar Condición - {implementationName}", "FailedToFetchUpdates": "Fallo al buscar las actualizaciones", - "FailedToUpdateSettings": "Fallo al actualizar los ajustes", "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver el historial de commits de Github para más detalles", "CreateEmptySeriesFoldersHelpText": "Crea carpetas de series faltantes durante el análisis del disco", "DefaultCase": "Caso predeterminado", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index b31874b2e..d30fded92 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -898,7 +898,6 @@ "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 \"{title}\"?", - "FailedToUpdateSettings": "Asetusten päivitys epäonnistui", "Forums": "Keskustelualue", "ErrorLoadingPage": "Virhe ladattaessa sivua", "FormatRuntimeHours": "{hours} t", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index a0e6175ea..0d1312936 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1472,7 +1472,6 @@ "FailedToLoadSonarr": "Échec du chargement de {appName}", "FailedToLoadSystemStatusFromApi": "Échec du chargement de l'état du système à partir de l'API", "FailedToLoadUiSettingsFromApi": "Échec du chargement des paramètres de l'interface utilisateur à partir de l'API", - "FailedToUpdateSettings": "Échec de la mise à jour des paramètres", "FeatureRequests": "Requêtes de nouvelles fonctionnalités", "File": "Fichier", "NoImportListsFound": "Aucune liste d'importation trouvée", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 7bfb4163d..a8a417fec 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -779,7 +779,6 @@ "FailedToLoadSystemStatusFromApi": "Nem sikerült betölteni a rendszerállapotot az API-ból", "FailedToLoadTagsFromApi": "Nem sikerült betölteni a címkéket az API-ból", "FailedToLoadTranslationsFromApi": "Nem sikerült betölteni a fordításokat az API-ból", - "FailedToUpdateSettings": "Nem sikerült frissíteni a beállításokat", "FilterDoesNotEndWith": "nem ér véget", "Level": "Szint", "LibraryImportTipsQualityInEpisodeFilename": "Győződjön meg arról, hogy a fájlok fájlnevében szerepel a minőség. például. `episode.s02e15.bluray.mkv`", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index ad7bfb364..228078976 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -230,7 +230,6 @@ "Exception": "Exceção", "ExternalUpdater": "O {appName} está configurado para usar um mecanismo de atualização externo", "FailedToFetchUpdates": "Falha ao buscar atualizações", - "FailedToUpdateSettings": "Falha ao atualizar as configurações", "FeatureRequests": "Solicitações de recursos", "Filename": "Nome do arquivo", "Fixed": "Corrigido", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 69df16596..297a8dfba 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1277,7 +1277,6 @@ "FailedToLoadSeriesFromApi": "Не удалось загрузить сериалы из API", "FileNames": "Имена файлов", "FailedToLoadSonarr": "Не удалось загрузить {appName}", - "FailedToUpdateSettings": "Не удалось обновить настройки", "FilterGreaterThanOrEqual": "больше или равно", "Filter": "Фильтр", "FilterIs": "является", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index bca885725..11fbf777f 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -466,7 +466,6 @@ "No": "Hayır", "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord kanalı webhook URL'si", "NotificationsEmailSettingsBccAddressHelpText": "E-posta bcc alıcılarının virgülle ayrılmış listesi", - "FailedToUpdateSettings": "Ayarlar güncellenemedi", "False": "Pasif", "HistoryLoadError": "Geçmiş yüklenemiyor", "NotificationsEmailSettingsRecipientAddressHelpText": "E-posta alıcılarının virgülle ayrılmış listesi", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index b6a00c0c8..edf704752 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -245,7 +245,6 @@ "Health": "健康度", "HomePage": "主页", "Indexers": "索引器", - "FailedToUpdateSettings": "更新配置失败", "FeatureRequests": "功能建议", "Filename": "文件名", "Fixed": "已修复", From 682d2b4e1b8acb74ab6e75b85e2a73aa431b5332 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 14:54:23 -0700 Subject: [PATCH 612/762] Convert Form Components to TypeScript --- frontend/.eslintrc.js | 1 - frontend/src/App/State/AppState.ts | 6 + frontend/src/App/State/CaptchaAppState.ts | 11 + frontend/src/App/State/OAuthAppState.ts | 9 + .../src/App/State/ProviderOptionsAppState.ts | 22 + .../FileBrowser/FileBrowserModalContent.tsx | 4 +- .../Filter/Builder/FilterBuilderRowValue.js | 2 +- .../Builder/FilterBuilderRowValueTag.js | 2 +- .../src/Components/Form/AutoCompleteInput.js | 98 --- .../src/Components/Form/AutoCompleteInput.tsx | 81 +++ .../src/Components/Form/AutoSuggestInput.js | 257 -------- .../src/Components/Form/AutoSuggestInput.tsx | 259 ++++++++ frontend/src/Components/Form/CaptchaInput.js | 84 --- frontend/src/Components/Form/CaptchaInput.tsx | 118 ++++ .../Components/Form/CaptchaInputConnector.js | 98 --- frontend/src/Components/Form/CheckInput.js | 191 ------ frontend/src/Components/Form/CheckInput.tsx | 141 ++++ frontend/src/Components/Form/DeviceInput.js | 106 --- .../Components/Form/DeviceInputConnector.js | 104 --- .../DownloadClientSelectInputConnector.js | 102 --- .../Components/Form/EnhancedSelectInput.js | 614 ----------------- .../Form/EnhancedSelectInputConnector.js | 162 ----- .../Form/EnhancedSelectInputOption.js | 113 ---- .../Form/EnhancedSelectInputSelectedValue.js | 35 - frontend/src/Components/Form/Form.js | 66 -- frontend/src/Components/Form/Form.tsx | 45 ++ frontend/src/Components/Form/FormGroup.js | 56 -- frontend/src/Components/Form/FormGroup.tsx | 43 ++ .../src/Components/Form/FormInputGroup.js | 311 --------- .../src/Components/Form/FormInputGroup.tsx | 292 ++++++++ .../src/Components/Form/FormInputHelpText.js | 74 --- .../src/Components/Form/FormInputHelpText.tsx | 55 ++ frontend/src/Components/Form/FormLabel.js | 52 -- frontend/src/Components/Form/FormLabel.tsx | 42 ++ .../Form/HintedSelectInputOption.js | 66 -- .../Form/HintedSelectInputSelectedValue.js | 68 -- .../Form/IndexerSelectInputConnector.js | 97 --- .../src/Components/Form/KeyValueListInput.css | 21 - .../Form/KeyValueListInput.css.d.ts | 10 - .../src/Components/Form/KeyValueListInput.js | 156 ----- .../Components/Form/KeyValueListInputItem.css | 28 - .../Form/KeyValueListInputItem.css.d.ts | 12 - .../Components/Form/KeyValueListInputItem.js | 124 ---- .../Form/LanguageSelectInputConnector.js | 52 -- .../Form/MonitorEpisodesSelectInput.js | 55 -- .../Form/MonitorNewItemsSelectInput.js | 50 -- frontend/src/Components/Form/NumberInput.js | 126 ---- frontend/src/Components/Form/NumberInput.tsx | 108 +++ frontend/src/Components/Form/OAuthInput.js | 39 -- frontend/src/Components/Form/OAuthInput.tsx | 72 ++ .../Components/Form/OAuthInputConnector.js | 89 --- frontend/src/Components/Form/PasswordInput.js | 24 - .../src/Components/Form/PasswordInput.tsx | 14 + frontend/src/Components/Form/PathInput.js | 195 ------ frontend/src/Components/Form/PathInput.tsx | 252 +++++++ .../src/Components/Form/PathInputConnector.js | 81 --- .../QualityProfileSelectInputConnector.js | 105 --- .../Components/Form/RootFolderSelectInput.js | 109 --- .../Form/RootFolderSelectInputConnector.js | 175 ----- .../Form/RootFolderSelectInputOption.js | 77 --- .../RootFolderSelectInputSelectedValue.js | 62 -- .../Form/Select/DownloadClientSelectInput.tsx | 88 +++ .../Form/{ => Select}/EnhancedSelectInput.css | 6 + .../{ => Select}/EnhancedSelectInput.css.d.ts | 1 + .../Form/Select/EnhancedSelectInput.tsx | 622 ++++++++++++++++++ .../EnhancedSelectInputOption.css | 4 +- .../EnhancedSelectInputOption.css.d.ts | 0 .../Form/Select/EnhancedSelectInputOption.tsx | 84 +++ .../EnhancedSelectInputSelectedValue.css | 0 .../EnhancedSelectInputSelectedValue.css.d.ts | 0 .../EnhancedSelectInputSelectedValue.tsx | 23 + .../{ => Select}/HintedSelectInputOption.css | 0 .../HintedSelectInputOption.css.d.ts | 0 .../Form/Select/HintedSelectInputOption.tsx | 52 ++ .../HintedSelectInputSelectedValue.css | 0 .../HintedSelectInputSelectedValue.css.d.ts | 0 .../Select/HintedSelectInputSelectedValue.tsx | 55 ++ .../{ => Select}/IndexerFlagsSelectInput.tsx | 28 +- .../Form/Select/IndexerSelectInput.tsx | 81 +++ .../Form/Select/LanguageSelectInput.tsx | 43 ++ .../Select/MonitorEpisodesSelectInput.tsx | 50 ++ .../Select/MonitorNewItemsSelectInput.tsx | 48 ++ .../Form/Select/ProviderOptionSelectInput.tsx | 164 +++++ .../Form/Select/QualityProfileSelectInput.tsx | 126 ++++ .../Form/Select/RootFolderSelectInput.tsx | 215 ++++++ .../RootFolderSelectInputOption.css | 0 .../RootFolderSelectInputOption.css.d.ts | 0 .../Select/RootFolderSelectInputOption.tsx | 67 ++ .../RootFolderSelectInputSelectedValue.css | 0 ...ootFolderSelectInputSelectedValue.css.d.ts | 0 .../RootFolderSelectInputSelectedValue.tsx | 60 ++ .../{ => Select}/SeriesTypeSelectInput.tsx | 48 +- .../SeriesTypeSelectInputOption.css | 0 .../SeriesTypeSelectInputOption.css.d.ts | 0 .../SeriesTypeSelectInputOption.tsx | 13 +- .../SeriesTypeSelectInputSelectedValue.tsx | 26 + .../Form/{ => Select}/UMaskInput.css | 106 +-- .../Form/{ => Select}/UMaskInput.css.d.ts | 0 .../src/Components/Form/Select/UMaskInput.tsx | 142 ++++ frontend/src/Components/Form/SelectInput.js | 95 --- frontend/src/Components/Form/SelectInput.tsx | 76 +++ .../src/Components/Form/SeriesTagInput.tsx | 53 -- .../SeriesTypeSelectInputSelectedValue.css | 20 - ...eriesTypeSelectInputSelectedValue.css.d.ts | 9 - .../SeriesTypeSelectInputSelectedValue.tsx | 27 - .../Components/Form/{ => Tag}/DeviceInput.css | 2 +- .../Form/{ => Tag}/DeviceInput.css.d.ts | 0 .../src/Components/Form/Tag/DeviceInput.tsx | 149 +++++ .../Components/Form/Tag/SeriesTagInput.tsx | 145 ++++ .../Components/Form/{ => Tag}/TagInput.css | 5 +- .../Form/{ => Tag}/TagInput.css.d.ts | 0 frontend/src/Components/Form/Tag/TagInput.tsx | 371 +++++++++++ .../Form/{ => Tag}/TagInputInput.css | 0 .../Form/{ => Tag}/TagInputInput.css.d.ts | 0 .../src/Components/Form/Tag/TagInputInput.tsx | 71 ++ .../Components/Form/{ => Tag}/TagInputTag.css | 0 .../Form/{ => Tag}/TagInputTag.css.d.ts | 0 .../src/Components/Form/Tag/TagInputTag.tsx | 79 +++ .../Components/Form/Tag/TagSelectInput.tsx | 97 +++ .../src/Components/Form/Tag/TextTagInput.tsx | 109 +++ frontend/src/Components/Form/TagInput.js | 301 --------- .../src/Components/Form/TagInputConnector.js | 157 ----- frontend/src/Components/Form/TagInputInput.js | 84 --- frontend/src/Components/Form/TagInputTag.js | 101 --- .../Form/TagSelectInputConnector.js | 102 --- frontend/src/Components/Form/TextArea.js | 172 ----- frontend/src/Components/Form/TextArea.tsx | 143 ++++ frontend/src/Components/Form/TextInput.js | 205 ------ frontend/src/Components/Form/TextInput.tsx | 177 +++++ .../Components/Form/TextTagInputConnector.js | 120 ---- frontend/src/Components/Form/UMaskInput.js | 144 ---- frontend/src/Components/Scroller/Scroller.tsx | 2 + .../Helpers/Hooks/useDebouncedCallback.tsx | 16 + .../Props/{inputTypes.js => inputTypes.ts} | 35 +- .../Episode/SelectEpisodeModalContent.tsx | 4 +- ...eractiveImportSelectFolderModalContent.tsx | 5 +- .../Series/SelectSeriesModalContent.tsx | 3 +- frontend/src/Parse/Parse.tsx | 3 +- frontend/src/Parse/ParseModalContent.tsx | 3 +- .../Index/Select/SeriesIndexPosterSelect.tsx | 5 +- .../MediaManagement/Naming/NamingModal.tsx | 2 +- .../EditReleaseProfileModalContent.css | 2 +- .../Profiles/Release/ReleaseProfileRow.tsx | 1 - .../Store/Actions/providerOptionActions.js | 17 +- .../Selectors/createSortedSectionSelector.ts | 3 +- .../src/Store/Selectors/selectSettings.js | 5 + frontend/src/typings/DownloadClient.ts | 13 +- frontend/src/typings/Field.ts | 21 + frontend/src/typings/Helpers/ArrayElement.ts | 3 + frontend/src/typings/ImportList.ts | 11 +- frontend/src/typings/Indexer.ts | 11 +- frontend/src/typings/Notification.ts | 11 +- frontend/src/typings/inputs.ts | 15 +- frontend/src/typings/pending.ts | 2 + frontend/typings/MiddleTruncate.d.ts | 16 + frontend/typings/jdu.d.ts | 3 + package.json | 2 + yarn.lock | 34 +- 158 files changed, 5225 insertions(+), 6112 deletions(-) create mode 100644 frontend/src/App/State/CaptchaAppState.ts create mode 100644 frontend/src/App/State/OAuthAppState.ts create mode 100644 frontend/src/App/State/ProviderOptionsAppState.ts delete mode 100644 frontend/src/Components/Form/AutoCompleteInput.js create mode 100644 frontend/src/Components/Form/AutoCompleteInput.tsx delete mode 100644 frontend/src/Components/Form/AutoSuggestInput.js create mode 100644 frontend/src/Components/Form/AutoSuggestInput.tsx delete mode 100644 frontend/src/Components/Form/CaptchaInput.js create mode 100644 frontend/src/Components/Form/CaptchaInput.tsx delete mode 100644 frontend/src/Components/Form/CaptchaInputConnector.js delete mode 100644 frontend/src/Components/Form/CheckInput.js create mode 100644 frontend/src/Components/Form/CheckInput.tsx delete mode 100644 frontend/src/Components/Form/DeviceInput.js delete mode 100644 frontend/src/Components/Form/DeviceInputConnector.js delete mode 100644 frontend/src/Components/Form/DownloadClientSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/EnhancedSelectInput.js delete mode 100644 frontend/src/Components/Form/EnhancedSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/EnhancedSelectInputOption.js delete mode 100644 frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js delete mode 100644 frontend/src/Components/Form/Form.js create mode 100644 frontend/src/Components/Form/Form.tsx delete mode 100644 frontend/src/Components/Form/FormGroup.js create mode 100644 frontend/src/Components/Form/FormGroup.tsx delete mode 100644 frontend/src/Components/Form/FormInputGroup.js create mode 100644 frontend/src/Components/Form/FormInputGroup.tsx delete mode 100644 frontend/src/Components/Form/FormInputHelpText.js create mode 100644 frontend/src/Components/Form/FormInputHelpText.tsx delete mode 100644 frontend/src/Components/Form/FormLabel.js create mode 100644 frontend/src/Components/Form/FormLabel.tsx delete mode 100644 frontend/src/Components/Form/HintedSelectInputOption.js delete mode 100644 frontend/src/Components/Form/HintedSelectInputSelectedValue.js delete mode 100644 frontend/src/Components/Form/IndexerSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/KeyValueListInput.css delete mode 100644 frontend/src/Components/Form/KeyValueListInput.css.d.ts delete mode 100644 frontend/src/Components/Form/KeyValueListInput.js delete mode 100644 frontend/src/Components/Form/KeyValueListInputItem.css delete mode 100644 frontend/src/Components/Form/KeyValueListInputItem.css.d.ts delete mode 100644 frontend/src/Components/Form/KeyValueListInputItem.js delete mode 100644 frontend/src/Components/Form/LanguageSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/MonitorEpisodesSelectInput.js delete mode 100644 frontend/src/Components/Form/MonitorNewItemsSelectInput.js delete mode 100644 frontend/src/Components/Form/NumberInput.js create mode 100644 frontend/src/Components/Form/NumberInput.tsx delete mode 100644 frontend/src/Components/Form/OAuthInput.js create mode 100644 frontend/src/Components/Form/OAuthInput.tsx delete mode 100644 frontend/src/Components/Form/OAuthInputConnector.js delete mode 100644 frontend/src/Components/Form/PasswordInput.js create mode 100644 frontend/src/Components/Form/PasswordInput.tsx delete mode 100644 frontend/src/Components/Form/PathInput.js create mode 100644 frontend/src/Components/Form/PathInput.tsx delete mode 100644 frontend/src/Components/Form/PathInputConnector.js delete mode 100644 frontend/src/Components/Form/QualityProfileSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/RootFolderSelectInput.js delete mode 100644 frontend/src/Components/Form/RootFolderSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/RootFolderSelectInputOption.js delete mode 100644 frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js create mode 100644 frontend/src/Components/Form/Select/DownloadClientSelectInput.tsx rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInput.css (94%) rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInput.css.d.ts (94%) create mode 100644 frontend/src/Components/Form/Select/EnhancedSelectInput.tsx rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInputOption.css (87%) rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInputOption.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/EnhancedSelectInputOption.tsx rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInputSelectedValue.css (100%) rename frontend/src/Components/Form/{ => Select}/EnhancedSelectInputSelectedValue.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.tsx rename frontend/src/Components/Form/{ => Select}/HintedSelectInputOption.css (100%) rename frontend/src/Components/Form/{ => Select}/HintedSelectInputOption.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/HintedSelectInputOption.tsx rename frontend/src/Components/Form/{ => Select}/HintedSelectInputSelectedValue.css (100%) rename frontend/src/Components/Form/{ => Select}/HintedSelectInputSelectedValue.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.tsx rename frontend/src/Components/Form/{ => Select}/IndexerFlagsSelectInput.tsx (68%) create mode 100644 frontend/src/Components/Form/Select/IndexerSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/LanguageSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/MonitorEpisodesSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/MonitorNewItemsSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/ProviderOptionSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/QualityProfileSelectInput.tsx create mode 100644 frontend/src/Components/Form/Select/RootFolderSelectInput.tsx rename frontend/src/Components/Form/{ => Select}/RootFolderSelectInputOption.css (100%) rename frontend/src/Components/Form/{ => Select}/RootFolderSelectInputOption.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/RootFolderSelectInputOption.tsx rename frontend/src/Components/Form/{ => Select}/RootFolderSelectInputSelectedValue.css (100%) rename frontend/src/Components/Form/{ => Select}/RootFolderSelectInputSelectedValue.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.tsx rename frontend/src/Components/Form/{ => Select}/SeriesTypeSelectInput.tsx (64%) rename frontend/src/Components/Form/{ => Select}/SeriesTypeSelectInputOption.css (100%) rename frontend/src/Components/Form/{ => Select}/SeriesTypeSelectInputOption.css.d.ts (100%) rename frontend/src/Components/Form/{ => Select}/SeriesTypeSelectInputOption.tsx (63%) create mode 100644 frontend/src/Components/Form/Select/SeriesTypeSelectInputSelectedValue.tsx rename frontend/src/Components/Form/{ => Select}/UMaskInput.css (93%) rename frontend/src/Components/Form/{ => Select}/UMaskInput.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Select/UMaskInput.tsx delete mode 100644 frontend/src/Components/Form/SelectInput.js create mode 100644 frontend/src/Components/Form/SelectInput.tsx delete mode 100644 frontend/src/Components/Form/SeriesTagInput.tsx delete mode 100644 frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css delete mode 100644 frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css.d.ts delete mode 100644 frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.tsx rename frontend/src/Components/Form/{ => Tag}/DeviceInput.css (64%) rename frontend/src/Components/Form/{ => Tag}/DeviceInput.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Tag/DeviceInput.tsx create mode 100644 frontend/src/Components/Form/Tag/SeriesTagInput.tsx rename frontend/src/Components/Form/{ => Tag}/TagInput.css (74%) rename frontend/src/Components/Form/{ => Tag}/TagInput.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Tag/TagInput.tsx rename frontend/src/Components/Form/{ => Tag}/TagInputInput.css (100%) rename frontend/src/Components/Form/{ => Tag}/TagInputInput.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Tag/TagInputInput.tsx rename frontend/src/Components/Form/{ => Tag}/TagInputTag.css (100%) rename frontend/src/Components/Form/{ => Tag}/TagInputTag.css.d.ts (100%) create mode 100644 frontend/src/Components/Form/Tag/TagInputTag.tsx create mode 100644 frontend/src/Components/Form/Tag/TagSelectInput.tsx create mode 100644 frontend/src/Components/Form/Tag/TextTagInput.tsx delete mode 100644 frontend/src/Components/Form/TagInput.js delete mode 100644 frontend/src/Components/Form/TagInputConnector.js delete mode 100644 frontend/src/Components/Form/TagInputInput.js delete mode 100644 frontend/src/Components/Form/TagInputTag.js delete mode 100644 frontend/src/Components/Form/TagSelectInputConnector.js delete mode 100644 frontend/src/Components/Form/TextArea.js create mode 100644 frontend/src/Components/Form/TextArea.tsx delete mode 100644 frontend/src/Components/Form/TextInput.js create mode 100644 frontend/src/Components/Form/TextInput.tsx delete mode 100644 frontend/src/Components/Form/TextTagInputConnector.js delete mode 100644 frontend/src/Components/Form/UMaskInput.js create mode 100644 frontend/src/Helpers/Hooks/useDebouncedCallback.tsx rename frontend/src/Helpers/Props/{inputTypes.js => inputTypes.ts} (74%) create mode 100644 frontend/src/typings/Field.ts create mode 100644 frontend/src/typings/Helpers/ArrayElement.ts create mode 100644 frontend/typings/MiddleTruncate.d.ts create mode 100644 frontend/typings/jdu.d.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index ddc7300fd..77b933a8f 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -210,7 +210,6 @@ module.exports = { 'no-undef-init': 'off', 'no-undefined': 'off', 'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }], - 'no-use-before-define': 'error', // Node.js and CommonJS diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 33638f91f..8dfecab9e 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,12 +1,15 @@ import BlocklistAppState from './BlocklistAppState'; import CalendarAppState from './CalendarAppState'; +import CaptchaAppState from './CaptchaAppState'; import CommandAppState from './CommandAppState'; import EpisodeFilesAppState from './EpisodeFilesAppState'; import EpisodesAppState from './EpisodesAppState'; import HistoryAppState from './HistoryAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; +import OAuthAppState from './OAuthAppState'; import ParseAppState from './ParseAppState'; import PathsAppState from './PathsAppState'; +import ProviderOptionsAppState from './ProviderOptionsAppState'; import QueueAppState from './QueueAppState'; import ReleasesAppState from './ReleasesAppState'; import RootFolderAppState from './RootFolderAppState'; @@ -64,14 +67,17 @@ interface AppState { app: AppSectionState; blocklist: BlocklistAppState; calendar: CalendarAppState; + captcha: CaptchaAppState; commands: CommandAppState; episodeFiles: EpisodeFilesAppState; episodes: EpisodesAppState; episodesSelection: EpisodesAppState; history: HistoryAppState; interactiveImport: InteractiveImportAppState; + oAuth: OAuthAppState; parse: ParseAppState; paths: PathsAppState; + providerOptions: ProviderOptionsAppState; queue: QueueAppState; releases: ReleasesAppState; rootFolders: RootFolderAppState; diff --git a/frontend/src/App/State/CaptchaAppState.ts b/frontend/src/App/State/CaptchaAppState.ts new file mode 100644 index 000000000..7252937eb --- /dev/null +++ b/frontend/src/App/State/CaptchaAppState.ts @@ -0,0 +1,11 @@ +interface CaptchaAppState { + refreshing: false; + token: string; + siteKey: unknown; + secretToken: unknown; + ray: unknown; + stoken: unknown; + responseUrl: unknown; +} + +export default CaptchaAppState; diff --git a/frontend/src/App/State/OAuthAppState.ts b/frontend/src/App/State/OAuthAppState.ts new file mode 100644 index 000000000..619767929 --- /dev/null +++ b/frontend/src/App/State/OAuthAppState.ts @@ -0,0 +1,9 @@ +import { Error } from './AppSectionState'; + +interface OAuthAppState { + authorizing: boolean; + result: Record<string, unknown> | null; + error: Error; +} + +export default OAuthAppState; diff --git a/frontend/src/App/State/ProviderOptionsAppState.ts b/frontend/src/App/State/ProviderOptionsAppState.ts new file mode 100644 index 000000000..7fb5df02b --- /dev/null +++ b/frontend/src/App/State/ProviderOptionsAppState.ts @@ -0,0 +1,22 @@ +import AppSectionState from 'App/State/AppSectionState'; +import Field, { FieldSelectOption } from 'typings/Field'; + +export interface ProviderOptions { + fields?: Field[]; +} + +interface ProviderOptionsDevice { + id: string; + name: string; +} + +interface ProviderOptionsAppState { + devices: AppSectionState<ProviderOptionsDevice>; + servers: AppSectionState<FieldSelectOption<unknown>>; + newznabCategories: AppSectionState<FieldSelectOption<unknown>>; + getProfiles: AppSectionState<FieldSelectOption<unknown>>; + getTags: AppSectionState<FieldSelectOption<unknown>>; + getRootFolders: AppSectionState<FieldSelectOption<unknown>>; +} + +export default ProviderOptionsAppState; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx index 53589551f..41338cb39 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Alert from 'Components/Alert'; -import PathInput from 'Components/Form/PathInput'; +import { PathInputInternal } from 'Components/Form/PathInput'; import Button from 'Components/Link/Button'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; @@ -151,7 +151,7 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) { </Alert> ) : null} - <PathInput + <PathInputInternal className={styles.pathInput} placeholder={translate('FileBrowserPlaceholderText')} hasFileBrowser={false} diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js index 68fa5c557..217626c90 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import TagInput from 'Components/Form/TagInput'; +import TagInput from 'Components/Form/Tag/TagInput'; import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props'; import tagShape from 'Helpers/Props/Shapes/tagShape'; import convertToBytes from 'Utilities/Number/convertToBytes'; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js index 7b6d6313a..063a97346 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import TagInputTag from 'Components/Form/TagInputTag'; +import TagInputTag from 'Components/Form/Tag/TagInputTag'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './FilterBuilderRowValueTag.css'; diff --git a/frontend/src/Components/Form/AutoCompleteInput.js b/frontend/src/Components/Form/AutoCompleteInput.js deleted file mode 100644 index d35969c4c..000000000 --- a/frontend/src/Components/Form/AutoCompleteInput.js +++ /dev/null @@ -1,98 +0,0 @@ -import jdu from 'jdu'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import AutoSuggestInput from './AutoSuggestInput'; - -class AutoCompleteInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - suggestions: [] - }; - } - - // - // Control - - getSuggestionValue(item) { - return item; - } - - renderSuggestion(item) { - return item; - } - - // - // Listeners - - onInputChange = (event, { newValue }) => { - this.props.onChange({ - name: this.props.name, - value: newValue - }); - }; - - onInputBlur = () => { - this.setState({ suggestions: [] }); - }; - - onSuggestionsFetchRequested = ({ value }) => { - const { values } = this.props; - const lowerCaseValue = jdu.replace(value).toLowerCase(); - - const filteredValues = values.filter((v) => { - return jdu.replace(v).toLowerCase().contains(lowerCaseValue); - }); - - this.setState({ suggestions: filteredValues }); - }; - - onSuggestionsClearRequested = () => { - this.setState({ suggestions: [] }); - }; - - // - // Render - - render() { - const { - name, - value, - ...otherProps - } = this.props; - - const { suggestions } = this.state; - - return ( - <AutoSuggestInput - {...otherProps} - name={name} - value={value} - suggestions={suggestions} - getSuggestionValue={this.getSuggestionValue} - renderSuggestion={this.renderSuggestion} - onInputBlur={this.onInputBlur} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - /> - ); - } -} - -AutoCompleteInput.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string, - values: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired -}; - -AutoCompleteInput.defaultProps = { - value: '' -}; - -export default AutoCompleteInput; diff --git a/frontend/src/Components/Form/AutoCompleteInput.tsx b/frontend/src/Components/Form/AutoCompleteInput.tsx new file mode 100644 index 000000000..7ba114125 --- /dev/null +++ b/frontend/src/Components/Form/AutoCompleteInput.tsx @@ -0,0 +1,81 @@ +import jdu from 'jdu'; +import React, { SyntheticEvent, useCallback, useState } from 'react'; +import { + ChangeEvent, + SuggestionsFetchRequestedParams, +} from 'react-autosuggest'; +import { InputChanged } from 'typings/inputs'; +import AutoSuggestInput from './AutoSuggestInput'; + +interface AutoCompleteInputProps { + name: string; + value?: string; + values: string[]; + onChange: (change: InputChanged<string>) => unknown; +} + +function AutoCompleteInput({ + name, + value = '', + values, + onChange, + ...otherProps +}: AutoCompleteInputProps) { + const [suggestions, setSuggestions] = useState<string[]>([]); + + const getSuggestionValue = useCallback((item: string) => { + return item; + }, []); + + const renderSuggestion = useCallback((item: string) => { + return item; + }, []); + + const handleInputChange = useCallback( + (_event: SyntheticEvent, { newValue }: ChangeEvent) => { + onChange({ + name, + value: newValue, + }); + }, + [name, onChange] + ); + + const handleInputBlur = useCallback(() => { + setSuggestions([]); + }, [setSuggestions]); + + const handleSuggestionsFetchRequested = useCallback( + ({ value: newValue }: SuggestionsFetchRequestedParams) => { + const lowerCaseValue = jdu.replace(newValue).toLowerCase(); + + const filteredValues = values.filter((v) => { + return jdu.replace(v).toLowerCase().includes(lowerCaseValue); + }); + + setSuggestions(filteredValues); + }, + [values, setSuggestions] + ); + + const handleSuggestionsClearRequested = useCallback(() => { + setSuggestions([]); + }, [setSuggestions]); + + return ( + <AutoSuggestInput + {...otherProps} + name={name} + value={value} + suggestions={suggestions} + getSuggestionValue={getSuggestionValue} + renderSuggestion={renderSuggestion} + onInputChange={handleInputChange} + onInputBlur={handleInputBlur} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={handleSuggestionsClearRequested} + /> + ); +} + +export default AutoCompleteInput; diff --git a/frontend/src/Components/Form/AutoSuggestInput.js b/frontend/src/Components/Form/AutoSuggestInput.js deleted file mode 100644 index 34ec7530b..000000000 --- a/frontend/src/Components/Form/AutoSuggestInput.js +++ /dev/null @@ -1,257 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Autosuggest from 'react-autosuggest'; -import { Manager, Popper, Reference } from 'react-popper'; -import Portal from 'Components/Portal'; -import styles from './AutoSuggestInput.css'; - -class AutoSuggestInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scheduleUpdate = null; - } - - componentDidUpdate(prevProps) { - if ( - this._scheduleUpdate && - prevProps.suggestions !== this.props.suggestions - ) { - this._scheduleUpdate(); - } - } - - // - // Control - - renderInputComponent = (inputProps) => { - const { renderInputComponent } = this.props; - - return ( - <Reference> - {({ ref }) => { - if (renderInputComponent) { - return renderInputComponent(inputProps, ref); - } - - return ( - <div ref={ref}> - <input - {...inputProps} - /> - </div> - ); - }} - </Reference> - ); - }; - - renderSuggestionsContainer = ({ containerProps, children }) => { - return ( - <Portal> - <Popper - placement='bottom-start' - modifiers={{ - computeMaxHeight: { - order: 851, - enabled: true, - fn: this.onComputeMaxHeight - }, - flip: { - padding: this.props.minHeight - } - }} - > - {({ ref: popperRef, style, scheduleUpdate }) => { - this._scheduleUpdate = scheduleUpdate; - - return ( - <div - ref={popperRef} - style={style} - className={children ? styles.suggestionsContainerOpen : undefined} - > - <div - {...containerProps} - style={{ - maxHeight: style.maxHeight - }} - > - {children} - </div> - </div> - ); - }} - </Popper> - </Portal> - ); - }; - - // - // Listeners - - onComputeMaxHeight = (data) => { - const { - top, - bottom, - width - } = data.offsets.reference; - - const windowHeight = window.innerHeight; - - if ((/^botton/).test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom; - } else { - data.styles.maxHeight = top; - } - - data.styles.width = width; - - return data; - }; - - onInputChange = (event, { newValue }) => { - this.props.onChange({ - name: this.props.name, - value: newValue - }); - }; - - onInputKeyDown = (event) => { - const { - name, - value, - suggestions, - onChange - } = this.props; - - if ( - event.key === 'Tab' && - suggestions.length && - suggestions[0] !== this.props.value - ) { - event.preventDefault(); - - if (value) { - onChange({ - name, - value: suggestions[0] - }); - } - } - }; - - // - // Render - - render() { - const { - forwardedRef, - className, - inputContainerClassName, - name, - value, - placeholder, - suggestions, - hasError, - hasWarning, - getSuggestionValue, - renderSuggestion, - onInputChange, - onInputKeyDown, - onInputFocus, - onInputBlur, - onSuggestionsFetchRequested, - onSuggestionsClearRequested, - onSuggestionSelected, - ...otherProps - } = this.props; - - const inputProps = { - className: classNames( - className, - hasError && styles.hasError, - hasWarning && styles.hasWarning - ), - name, - value, - placeholder, - autoComplete: 'off', - spellCheck: false, - onChange: onInputChange || this.onInputChange, - onKeyDown: onInputKeyDown || this.onInputKeyDown, - onFocus: onInputFocus, - onBlur: onInputBlur - }; - - const theme = { - container: inputContainerClassName, - containerOpen: styles.suggestionsContainerOpen, - suggestionsContainer: styles.suggestionsContainer, - suggestionsList: styles.suggestionsList, - suggestion: styles.suggestion, - suggestionHighlighted: styles.suggestionHighlighted - }; - - return ( - <Manager> - <Autosuggest - {...otherProps} - ref={forwardedRef} - id={name} - inputProps={inputProps} - theme={theme} - suggestions={suggestions} - getSuggestionValue={getSuggestionValue} - renderInputComponent={this.renderInputComponent} - renderSuggestionsContainer={this.renderSuggestionsContainer} - renderSuggestion={renderSuggestion} - onSuggestionSelected={onSuggestionSelected} - onSuggestionsFetchRequested={onSuggestionsFetchRequested} - onSuggestionsClearRequested={onSuggestionsClearRequested} - /> - </Manager> - ); - } -} - -AutoSuggestInput.propTypes = { - forwardedRef: PropTypes.func, - className: PropTypes.string.isRequired, - inputContainerClassName: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - placeholder: PropTypes.string, - suggestions: PropTypes.array.isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - enforceMaxHeight: PropTypes.bool.isRequired, - minHeight: PropTypes.number.isRequired, - maxHeight: PropTypes.number.isRequired, - getSuggestionValue: PropTypes.func.isRequired, - renderInputComponent: PropTypes.elementType, - renderSuggestion: PropTypes.func.isRequired, - onInputChange: PropTypes.func, - onInputKeyDown: PropTypes.func, - onInputFocus: PropTypes.func, - onInputBlur: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func, - onChange: PropTypes.func.isRequired -}; - -AutoSuggestInput.defaultProps = { - className: styles.input, - inputContainerClassName: styles.inputContainer, - enforceMaxHeight: true, - minHeight: 50, - maxHeight: 200 -}; - -export default AutoSuggestInput; diff --git a/frontend/src/Components/Form/AutoSuggestInput.tsx b/frontend/src/Components/Form/AutoSuggestInput.tsx new file mode 100644 index 000000000..b3a7c31b0 --- /dev/null +++ b/frontend/src/Components/Form/AutoSuggestInput.tsx @@ -0,0 +1,259 @@ +import classNames from 'classnames'; +import React, { + FocusEvent, + FormEvent, + KeyboardEvent, + KeyboardEventHandler, + MutableRefObject, + ReactNode, + Ref, + SyntheticEvent, + useCallback, + useEffect, + useRef, +} from 'react'; +import Autosuggest, { + AutosuggestPropsBase, + BlurEvent, + ChangeEvent, + RenderInputComponentProps, + RenderSuggestionsContainerParams, +} from 'react-autosuggest'; +import { Manager, Popper, Reference } from 'react-popper'; +import Portal from 'Components/Portal'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { InputChanged } from 'typings/inputs'; +import styles from './AutoSuggestInput.css'; + +interface AutoSuggestInputProps<T> + extends Omit<AutosuggestPropsBase<T>, 'renderInputComponent' | 'inputProps'> { + forwardedRef?: MutableRefObject<Autosuggest<T> | null>; + className?: string; + inputContainerClassName?: string; + name: string; + value?: string; + placeholder?: string; + suggestions: T[]; + hasError?: boolean; + hasWarning?: boolean; + enforceMaxHeight?: boolean; + minHeight?: number; + maxHeight?: number; + renderInputComponent?: ( + inputProps: RenderInputComponentProps, + ref: Ref<HTMLDivElement> + ) => ReactNode; + onInputChange: ( + event: FormEvent<HTMLElement>, + params: ChangeEvent + ) => unknown; + onInputKeyDown?: KeyboardEventHandler<HTMLElement>; + onInputFocus?: (event: SyntheticEvent) => unknown; + onInputBlur: ( + event: FocusEvent<HTMLElement>, + params?: BlurEvent<T> + ) => unknown; + onChange?: (change: InputChanged<T>) => unknown; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) { + const { + // TODO: forwaredRef should be replaces with React.forwardRef + forwardedRef, + className = styles.input, + inputContainerClassName = styles.inputContainer, + name, + value = '', + placeholder, + suggestions, + enforceMaxHeight = true, + hasError, + hasWarning, + minHeight = 50, + maxHeight = 200, + getSuggestionValue, + renderSuggestion, + renderInputComponent, + onInputChange, + onInputKeyDown, + onInputFocus, + onInputBlur, + onSuggestionsFetchRequested, + onSuggestionsClearRequested, + onSuggestionSelected, + onChange, + ...otherProps + } = props; + + const updater = useRef<(() => void) | null>(null); + const previousSuggestions = usePrevious(suggestions); + + const handleComputeMaxHeight = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data: any) => { + const { top, bottom, width } = data.offsets.reference; + + if (enforceMaxHeight) { + data.styles.maxHeight = maxHeight; + } else { + const windowHeight = window.innerHeight; + + if (/^botton/.test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } + } + + data.styles.width = width; + + return data; + }, + [enforceMaxHeight, maxHeight] + ); + + const createRenderInputComponent = useCallback( + (inputProps: RenderInputComponentProps) => { + return ( + <Reference> + {({ ref }) => { + if (renderInputComponent) { + return renderInputComponent(inputProps, ref); + } + + return ( + <div ref={ref}> + <input {...inputProps} /> + </div> + ); + }} + </Reference> + ); + }, + [renderInputComponent] + ); + + const renderSuggestionsContainer = useCallback( + ({ containerProps, children }: RenderSuggestionsContainerParams) => { + return ( + <Portal> + <Popper + placement="bottom-start" + modifiers={{ + computeMaxHeight: { + order: 851, + enabled: true, + fn: handleComputeMaxHeight, + }, + flip: { + padding: minHeight, + }, + }} + > + {({ ref: popperRef, style, scheduleUpdate }) => { + updater.current = scheduleUpdate; + + return ( + <div + ref={popperRef} + style={style} + className={ + children ? styles.suggestionsContainerOpen : undefined + } + > + <div + {...containerProps} + style={{ + maxHeight: style.maxHeight, + }} + > + {children} + </div> + </div> + ); + }} + </Popper> + </Portal> + ); + }, + [minHeight, handleComputeMaxHeight] + ); + + const handleInputKeyDown = useCallback( + (event: KeyboardEvent<HTMLElement>) => { + if ( + event.key === 'Tab' && + suggestions.length && + suggestions[0] !== value + ) { + event.preventDefault(); + + if (value) { + onSuggestionSelected?.(event, { + suggestion: suggestions[0], + suggestionValue: value, + suggestionIndex: 0, + sectionIndex: null, + method: 'enter', + }); + } + } + }, + [value, suggestions, onSuggestionSelected] + ); + + const inputProps = { + className: classNames( + className, + hasError && styles.hasError, + hasWarning && styles.hasWarning + ), + name, + value, + placeholder, + autoComplete: 'off', + spellCheck: false, + onChange: onInputChange, + onKeyDown: onInputKeyDown || handleInputKeyDown, + onFocus: onInputFocus, + onBlur: onInputBlur, + }; + + const theme = { + container: inputContainerClassName, + containerOpen: styles.suggestionsContainerOpen, + suggestionsContainer: styles.suggestionsContainer, + suggestionsList: styles.suggestionsList, + suggestion: styles.suggestion, + suggestionHighlighted: styles.suggestionHighlighted, + }; + + useEffect(() => { + if (updater.current && suggestions !== previousSuggestions) { + updater.current(); + } + }, [suggestions, previousSuggestions]); + + return ( + <Manager> + <Autosuggest + {...otherProps} + ref={forwardedRef} + id={name} + inputProps={inputProps} + theme={theme} + suggestions={suggestions} + getSuggestionValue={getSuggestionValue} + renderInputComponent={createRenderInputComponent} + renderSuggestionsContainer={renderSuggestionsContainer} + renderSuggestion={renderSuggestion} + onSuggestionSelected={onSuggestionSelected} + onSuggestionsFetchRequested={onSuggestionsFetchRequested} + onSuggestionsClearRequested={onSuggestionsClearRequested} + /> + </Manager> + ); +} + +export default AutoSuggestInput; diff --git a/frontend/src/Components/Form/CaptchaInput.js b/frontend/src/Components/Form/CaptchaInput.js deleted file mode 100644 index b422198b5..000000000 --- a/frontend/src/Components/Form/CaptchaInput.js +++ /dev/null @@ -1,84 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ReCAPTCHA from 'react-google-recaptcha'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import FormInputButton from './FormInputButton'; -import TextInput from './TextInput'; -import styles from './CaptchaInput.css'; - -function CaptchaInput(props) { - const { - className, - name, - value, - hasError, - hasWarning, - refreshing, - siteKey, - secretToken, - onChange, - onRefreshPress, - onCaptchaChange - } = props; - - return ( - <div> - <div className={styles.captchaInputWrapper}> - <TextInput - className={classNames( - className, - styles.hasButton, - hasError && styles.hasError, - hasWarning && styles.hasWarning - )} - name={name} - value={value} - onChange={onChange} - /> - - <FormInputButton - onPress={onRefreshPress} - > - <Icon - name={icons.REFRESH} - isSpinning={refreshing} - /> - </FormInputButton> - </div> - - { - !!siteKey && !!secretToken && - <div className={styles.recaptchaWrapper}> - <ReCAPTCHA - sitekey={siteKey} - stoken={secretToken} - onChange={onCaptchaChange} - /> - </div> - } - </div> - ); -} - -CaptchaInput.propTypes = { - className: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - refreshing: PropTypes.bool.isRequired, - siteKey: PropTypes.string, - secretToken: PropTypes.string, - onChange: PropTypes.func.isRequired, - onRefreshPress: PropTypes.func.isRequired, - onCaptchaChange: PropTypes.func.isRequired -}; - -CaptchaInput.defaultProps = { - className: styles.input, - value: '' -}; - -export default CaptchaInput; diff --git a/frontend/src/Components/Form/CaptchaInput.tsx b/frontend/src/Components/Form/CaptchaInput.tsx new file mode 100644 index 000000000..d5a3f11f7 --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInput.tsx @@ -0,0 +1,118 @@ +import classNames from 'classnames'; +import React, { useCallback, useEffect } from 'react'; +import ReCAPTCHA from 'react-google-recaptcha'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Icon from 'Components/Icon'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { + getCaptchaCookie, + refreshCaptcha, + resetCaptcha, +} from 'Store/Actions/captchaActions'; +import { InputChanged } from 'typings/inputs'; +import FormInputButton from './FormInputButton'; +import TextInput from './TextInput'; +import styles from './CaptchaInput.css'; + +interface CaptchaInputProps { + className?: string; + name: string; + value?: string; + provider: string; + providerData: object; + hasError?: boolean; + hasWarning?: boolean; + refreshing: boolean; + siteKey?: string; + secretToken?: string; + onChange: (change: InputChanged<string>) => unknown; +} + +function CaptchaInput({ + className = styles.input, + name, + value = '', + provider, + providerData, + hasError, + hasWarning, + refreshing, + siteKey, + secretToken, + onChange, +}: CaptchaInputProps) { + const { token } = useSelector((state: AppState) => state.captcha); + const dispatch = useDispatch(); + const previousToken = usePrevious(token); + + const handleCaptchaChange = useCallback( + (token: string | null) => { + // If the captcha has expired `captchaResponse` will be null. + // In the event it's null don't try to get the captchaCookie. + // TODO: Should we clear the cookie? or reset the captcha? + + if (!token) { + return; + } + + dispatch( + getCaptchaCookie({ + provider, + providerData, + captchaResponse: token, + }) + ); + }, + [provider, providerData, dispatch] + ); + + const handleRefreshPress = useCallback(() => { + dispatch(refreshCaptcha({ provider, providerData })); + }, [provider, providerData, dispatch]); + + useEffect(() => { + if (token && token !== previousToken) { + onChange({ name, value: token }); + } + }, [name, token, previousToken, onChange]); + + useEffect(() => { + dispatch(resetCaptcha()); + }, [dispatch]); + + return ( + <div> + <div className={styles.captchaInputWrapper}> + <TextInput + className={classNames( + className, + styles.hasButton, + hasError && styles.hasError, + hasWarning && styles.hasWarning + )} + name={name} + value={value} + onChange={onChange} + /> + + <FormInputButton onPress={handleRefreshPress}> + <Icon name={icons.REFRESH} isSpinning={refreshing} /> + </FormInputButton> + </div> + + {siteKey && secretToken ? ( + <div className={styles.recaptchaWrapper}> + <ReCAPTCHA + sitekey={siteKey} + stoken={secretToken} + onChange={handleCaptchaChange} + /> + </div> + ) : null} + </div> + ); +} + +export default CaptchaInput; diff --git a/frontend/src/Components/Form/CaptchaInputConnector.js b/frontend/src/Components/Form/CaptchaInputConnector.js deleted file mode 100644 index ad83bf02f..000000000 --- a/frontend/src/Components/Form/CaptchaInputConnector.js +++ /dev/null @@ -1,98 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { getCaptchaCookie, refreshCaptcha, resetCaptcha } from 'Store/Actions/captchaActions'; -import CaptchaInput from './CaptchaInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.captcha, - (captcha) => { - return captcha; - } - ); -} - -const mapDispatchToProps = { - refreshCaptcha, - getCaptchaCookie, - resetCaptcha -}; - -class CaptchaInputConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - const { - name, - token, - onChange - } = this.props; - - if (token && token !== prevProps.token) { - onChange({ name, value: token }); - } - } - - componentWillUnmount = () => { - this.props.resetCaptcha(); - }; - - // - // Listeners - - onRefreshPress = () => { - const { - provider, - providerData - } = this.props; - - this.props.refreshCaptcha({ provider, providerData }); - }; - - onCaptchaChange = (captchaResponse) => { - // If the captcha has expired `captchaResponse` will be null. - // In the event it's null don't try to get the captchaCookie. - // TODO: Should we clear the cookie? or reset the captcha? - - if (!captchaResponse) { - return; - } - - const { - provider, - providerData - } = this.props; - - this.props.getCaptchaCookie({ provider, providerData, captchaResponse }); - }; - - // - // Render - - render() { - return ( - <CaptchaInput - {...this.props} - onRefreshPress={this.onRefreshPress} - onCaptchaChange={this.onCaptchaChange} - /> - ); - } -} - -CaptchaInputConnector.propTypes = { - provider: PropTypes.string.isRequired, - providerData: PropTypes.object.isRequired, - name: PropTypes.string.isRequired, - token: PropTypes.string, - onChange: PropTypes.func.isRequired, - refreshCaptcha: PropTypes.func.isRequired, - getCaptchaCookie: PropTypes.func.isRequired, - resetCaptcha: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CaptchaInputConnector); diff --git a/frontend/src/Components/Form/CheckInput.js b/frontend/src/Components/Form/CheckInput.js deleted file mode 100644 index 26d915880..000000000 --- a/frontend/src/Components/Form/CheckInput.js +++ /dev/null @@ -1,191 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; -import FormInputHelpText from './FormInputHelpText'; -import styles from './CheckInput.css'; - -class CheckInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._checkbox = null; - } - - componentDidMount() { - this.setIndeterminate(); - } - - componentDidUpdate() { - this.setIndeterminate(); - } - - // - // Control - - setIndeterminate() { - if (!this._checkbox) { - return; - } - - const { - value, - uncheckedValue, - checkedValue - } = this.props; - - this._checkbox.indeterminate = value !== uncheckedValue && value !== checkedValue; - } - - toggleChecked = (checked, shiftKey) => { - const { - name, - value, - checkedValue, - uncheckedValue - } = this.props; - - const newValue = checked ? checkedValue : uncheckedValue; - - if (value !== newValue) { - this.props.onChange({ - name, - value: newValue, - shiftKey - }); - } - }; - - // - // Listeners - - setRef = (ref) => { - this._checkbox = ref; - }; - - onClick = (event) => { - if (this.props.isDisabled) { - return; - } - - const shiftKey = event.nativeEvent.shiftKey; - const checked = !this._checkbox.checked; - - event.preventDefault(); - this.toggleChecked(checked, shiftKey); - }; - - onChange = (event) => { - const checked = event.target.checked; - const shiftKey = event.nativeEvent.shiftKey; - - this.toggleChecked(checked, shiftKey); - }; - - // - // Render - - render() { - const { - className, - containerClassName, - name, - value, - checkedValue, - uncheckedValue, - helpText, - helpTextWarning, - isDisabled, - kind - } = this.props; - - const isChecked = value === checkedValue; - const isUnchecked = value === uncheckedValue; - const isIndeterminate = !isChecked && !isUnchecked; - const isCheckClass = `${kind}IsChecked`; - - return ( - <div className={containerClassName}> - <label - className={styles.label} - onClick={this.onClick} - > - <input - ref={this.setRef} - className={styles.checkbox} - type="checkbox" - name={name} - checked={isChecked} - disabled={isDisabled} - onChange={this.onChange} - /> - - <div - className={classNames( - className, - isChecked ? styles[isCheckClass] : styles.isNotChecked, - isIndeterminate && styles.isIndeterminate, - isDisabled && styles.isDisabled - )} - > - { - isChecked && - <Icon name={icons.CHECK} /> - } - - { - isIndeterminate && - <Icon name={icons.CHECK_INDETERMINATE} /> - } - </div> - - { - helpText && - <FormInputHelpText - className={styles.helpText} - text={helpText} - /> - } - - { - !helpText && helpTextWarning && - <FormInputHelpText - className={styles.helpText} - text={helpTextWarning} - isWarning={true} - /> - } - </label> - </div> - ); - } -} - -CheckInput.propTypes = { - className: PropTypes.string.isRequired, - containerClassName: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - checkedValue: PropTypes.bool, - uncheckedValue: PropTypes.bool, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - helpText: PropTypes.string, - helpTextWarning: PropTypes.string, - isDisabled: PropTypes.bool, - kind: PropTypes.oneOf(kinds.all).isRequired, - onChange: PropTypes.func.isRequired -}; - -CheckInput.defaultProps = { - className: styles.input, - containerClassName: styles.container, - checkedValue: true, - uncheckedValue: false, - kind: kinds.PRIMARY -}; - -export default CheckInput; diff --git a/frontend/src/Components/Form/CheckInput.tsx b/frontend/src/Components/Form/CheckInput.tsx new file mode 100644 index 000000000..b7080cfdd --- /dev/null +++ b/frontend/src/Components/Form/CheckInput.tsx @@ -0,0 +1,141 @@ +import classNames from 'classnames'; +import React, { SyntheticEvent, useCallback, useEffect, useRef } from 'react'; +import Icon from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { CheckInputChanged } from 'typings/inputs'; +import FormInputHelpText from './FormInputHelpText'; +import styles from './CheckInput.css'; + +interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> { + target: EventTarget & T; +} + +interface CheckInputProps { + className?: string; + containerClassName?: string; + name: string; + checkedValue?: boolean; + uncheckedValue?: boolean; + value?: string | boolean; + helpText?: string; + helpTextWarning?: string; + isDisabled?: boolean; + kind?: Extract<Kind, keyof typeof styles>; + onChange: (changes: CheckInputChanged) => void; +} + +function CheckInput(props: CheckInputProps) { + const { + className = styles.input, + containerClassName = styles.container, + name, + value, + checkedValue = true, + uncheckedValue = false, + helpText, + helpTextWarning, + isDisabled, + kind = 'primary', + onChange, + } = props; + + const inputRef = useRef<HTMLInputElement>(null); + + const isChecked = value === checkedValue; + const isUnchecked = value === uncheckedValue; + const isIndeterminate = !isChecked && !isUnchecked; + const isCheckClass: keyof typeof styles = `${kind}IsChecked`; + + const toggleChecked = useCallback( + (checked: boolean, shiftKey: boolean) => { + const newValue = checked ? checkedValue : uncheckedValue; + + if (value !== newValue) { + onChange({ + name, + value: newValue, + shiftKey, + }); + } + }, + [name, value, checkedValue, uncheckedValue, onChange] + ); + + const handleClick = useCallback( + (event: SyntheticEvent<HTMLElement, MouseEvent>) => { + if (isDisabled) { + return; + } + + const shiftKey = event.nativeEvent.shiftKey; + const checked = !(inputRef.current?.checked ?? false); + + event.preventDefault(); + toggleChecked(checked, shiftKey); + }, + [isDisabled, toggleChecked] + ); + + const handleChange = useCallback( + (event: ChangeEvent<HTMLInputElement>) => { + const checked = event.target.checked; + const shiftKey = event.nativeEvent.shiftKey; + + toggleChecked(checked, shiftKey); + }, + [toggleChecked] + ); + + useEffect(() => { + if (!inputRef.current) { + return; + } + + inputRef.current.indeterminate = + value !== uncheckedValue && value !== checkedValue; + }, [value, uncheckedValue, checkedValue]); + + return ( + <div className={containerClassName}> + <label className={styles.label} onClick={handleClick}> + <input + ref={inputRef} + className={styles.checkbox} + type="checkbox" + name={name} + checked={isChecked} + disabled={isDisabled} + onChange={handleChange} + /> + + <div + className={classNames( + className, + isChecked ? styles[isCheckClass] : styles.isNotChecked, + isIndeterminate && styles.isIndeterminate, + isDisabled && styles.isDisabled + )} + > + {isChecked ? <Icon name={icons.CHECK} /> : null} + + {isIndeterminate ? <Icon name={icons.CHECK_INDETERMINATE} /> : null} + </div> + + {helpText ? ( + <FormInputHelpText className={styles.helpText} text={helpText} /> + ) : null} + + {!helpText && helpTextWarning ? ( + <FormInputHelpText + className={styles.helpText} + text={helpTextWarning} + isWarning={true} + /> + ) : null} + </label> + </div> + ); +} + +export default CheckInput; diff --git a/frontend/src/Components/Form/DeviceInput.js b/frontend/src/Components/Form/DeviceInput.js deleted file mode 100644 index 55c239cb8..000000000 --- a/frontend/src/Components/Form/DeviceInput.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import tagShape from 'Helpers/Props/Shapes/tagShape'; -import FormInputButton from './FormInputButton'; -import TagInput from './TagInput'; -import styles from './DeviceInput.css'; - -class DeviceInput extends Component { - - onTagAdd = (device) => { - const { - name, - value, - onChange - } = this.props; - - // New tags won't have an ID, only a name. - const deviceId = device.id || device.name; - - onChange({ - name, - value: [...value, deviceId] - }); - }; - - onTagDelete = ({ index }) => { - const { - name, - value, - onChange - } = this.props; - - const newValue = value.slice(); - newValue.splice(index, 1); - - onChange({ - name, - value: newValue - }); - }; - - // - // Render - - render() { - const { - className, - name, - items, - selectedDevices, - hasError, - hasWarning, - isFetching, - onRefreshPress - } = this.props; - - return ( - <div className={className}> - <TagInput - inputContainerClassName={styles.input} - name={name} - tags={selectedDevices} - tagList={items} - allowNew={true} - minQueryLength={0} - hasError={hasError} - hasWarning={hasWarning} - onTagAdd={this.onTagAdd} - onTagDelete={this.onTagDelete} - /> - - <FormInputButton - onPress={onRefreshPress} - > - <Icon - name={icons.REFRESH} - isSpinning={isFetching} - /> - </FormInputButton> - </div> - ); - } -} - -DeviceInput.propTypes = { - className: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired, - items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, - selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onRefreshPress: PropTypes.func.isRequired -}; - -DeviceInput.defaultProps = { - className: styles.deviceInputWrapper, - inputClassName: styles.input -}; - -export default DeviceInput; diff --git a/frontend/src/Components/Form/DeviceInputConnector.js b/frontend/src/Components/Form/DeviceInputConnector.js deleted file mode 100644 index 2af9a79f6..000000000 --- a/frontend/src/Components/Form/DeviceInputConnector.js +++ /dev/null @@ -1,104 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions'; -import DeviceInput from './DeviceInput'; - -function createMapStateToProps() { - return createSelector( - (state, { value }) => value, - (state) => state.providerOptions.devices || defaultState, - (value, devices) => { - - return { - ...devices, - selectedDevices: value.map((valueDevice) => { - // Disable equality ESLint rule so we don't need to worry about - // a type mismatch between the value items and the device ID. - // eslint-disable-next-line eqeqeq - const device = devices.items.find((d) => d.id == valueDevice); - - if (device) { - return { - id: device.id, - name: `${device.name} (${device.id})` - }; - } - - return { - id: valueDevice, - name: `Unknown (${valueDevice})` - }; - }) - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchOptions: fetchOptions, - dispatchClearOptions: clearOptions -}; - -class DeviceInputConnector extends Component { - - // - // Lifecycle - - componentDidMount = () => { - this._populate(); - }; - - componentWillUnmount = () => { - this.props.dispatchClearOptions({ section: 'devices' }); - }; - - // - // Control - - _populate() { - const { - provider, - providerData, - dispatchFetchOptions - } = this.props; - - dispatchFetchOptions({ - section: 'devices', - action: 'getDevices', - provider, - providerData - }); - } - - // - // Listeners - - onRefreshPress = () => { - this._populate(); - }; - - // - // Render - - render() { - return ( - <DeviceInput - {...this.props} - onRefreshPress={this.onRefreshPress} - /> - ); - } -} - -DeviceInputConnector.propTypes = { - provider: PropTypes.string.isRequired, - providerData: PropTypes.object.isRequired, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - dispatchFetchOptions: PropTypes.func.isRequired, - dispatchClearOptions: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector); diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js deleted file mode 100644 index c21f0ded6..000000000 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ /dev/null @@ -1,102 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.downloadClients, - (state, { includeAny }) => includeAny, - (state, { protocol }) => protocol, - (downloadClients, includeAny, protocolFilter) => { - const { - isFetching, - isPopulated, - error, - items - } = downloadClients; - - const filteredItems = items.filter((item) => item.protocol === protocolFilter); - - const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => { - return { - key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` - }; - }); - - if (includeAny) { - values.unshift({ - key: 0, - value: `(${translate('Any')})` - }); - } - - return { - isFetching, - isPopulated, - error, - values - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchDownloadClients: fetchDownloadClients -}; - -class DownloadClientSelectInputConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.dispatchFetchDownloadClients(); - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - this.props.onChange({ name, value: parseInt(value) }); - }; - - // - // Render - - render() { - return ( - <EnhancedSelectInput - {...this.props} - onChange={this.onChange} - /> - ); - } -} - -DownloadClientSelectInputConnector.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - includeAny: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired -}; - -DownloadClientSelectInputConnector.defaultProps = { - includeAny: false, - protocol: 'torrent' -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector); diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js deleted file mode 100644 index 38b5e6ab5..000000000 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ /dev/null @@ -1,614 +0,0 @@ -import classNames from 'classnames'; -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Manager, Popper, Reference } from 'react-popper'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Measure from 'Components/Measure'; -import Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import Portal from 'Components/Portal'; -import Scroller from 'Components/Scroller/Scroller'; -import { icons, scrollDirections, sizes } from 'Helpers/Props'; -import { isMobile as isMobileUtil } from 'Utilities/browser'; -import * as keyCodes from 'Utilities/Constants/keyCodes'; -import getUniqueElememtId from 'Utilities/getUniqueElementId'; -import HintedSelectInputOption from './HintedSelectInputOption'; -import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; -import TextInput from './TextInput'; -import styles from './EnhancedSelectInput.css'; - -function isArrowKey(keyCode) { - return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; -} - -function getSelectedOption(selectedIndex, values) { - return values[selectedIndex]; -} - -function findIndex(startingIndex, direction, values) { - let indexToTest = startingIndex + direction; - - while (indexToTest !== startingIndex) { - if (indexToTest < 0) { - indexToTest = values.length - 1; - } else if (indexToTest >= values.length) { - indexToTest = 0; - } - - if (getSelectedOption(indexToTest, values).isDisabled) { - indexToTest = indexToTest + direction; - } else { - return indexToTest; - } - } -} - -function previousIndex(selectedIndex, values) { - return findIndex(selectedIndex, -1, values); -} - -function nextIndex(selectedIndex, values) { - return findIndex(selectedIndex, 1, values); -} - -function getSelectedIndex(props) { - const { - value, - values - } = props; - - if (Array.isArray(value)) { - return values.findIndex((v) => { - return value.size && v.key === value[0]; - }); - } - - return values.findIndex((v) => { - return v.key === value; - }); -} - -function isSelectedItem(index, props) { - const { - value, - values - } = props; - - if (Array.isArray(value)) { - return value.includes(values[index].key); - } - - return values[index].key === value; -} - -function getKey(selectedIndex, values) { - return values[selectedIndex].key; -} - -class EnhancedSelectInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scheduleUpdate = null; - this._buttonId = getUniqueElememtId(); - this._optionsId = getUniqueElememtId(); - - this.state = { - isOpen: false, - selectedIndex: getSelectedIndex(props), - width: 0, - isMobile: isMobileUtil() - }; - } - - componentDidUpdate(prevProps) { - if (this._scheduleUpdate) { - this._scheduleUpdate(); - } - - if (!Array.isArray(this.props.value)) { - if (prevProps.value !== this.props.value || prevProps.values !== this.props.values) { - this.setState({ - selectedIndex: getSelectedIndex(this.props) - }); - } - } - } - - // - // Control - - _addListener() { - window.addEventListener('click', this.onWindowClick); - } - - _removeListener() { - window.removeEventListener('click', this.onWindowClick); - } - - // - // Listeners - - onComputeMaxHeight = (data) => { - const { - top, - bottom - } = data.offsets.reference; - - const windowHeight = window.innerHeight; - - if ((/^botton/).test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom; - } else { - data.styles.maxHeight = top; - } - - return data; - }; - - onWindowClick = (event) => { - const button = document.getElementById(this._buttonId); - const options = document.getElementById(this._optionsId); - - if (!button || !event.target.isConnected || this.state.isMobile) { - return; - } - - if ( - !button.contains(event.target) && - options && - !options.contains(event.target) && - this.state.isOpen - ) { - this.setState({ isOpen: false }); - this._removeListener(); - } - }; - - onFocus = () => { - if (this.state.isOpen) { - this._removeListener(); - this.setState({ isOpen: false }); - } - }; - - onBlur = () => { - if (!this.props.isEditable) { - // Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox) - const origIndex = getSelectedIndex(this.props); - - if (origIndex !== this.state.selectedIndex) { - this.setState({ selectedIndex: origIndex }); - } - } - }; - - onKeyDown = (event) => { - const { - values - } = this.props; - - const { - isOpen, - selectedIndex - } = this.state; - - const keyCode = event.keyCode; - const newState = {}; - - if (!isOpen) { - if (isArrowKey(keyCode)) { - event.preventDefault(); - newState.isOpen = true; - } - - if ( - selectedIndex == null || selectedIndex === -1 || - getSelectedOption(selectedIndex, values).isDisabled - ) { - if (keyCode === keyCodes.UP_ARROW) { - newState.selectedIndex = previousIndex(0, values); - } else if (keyCode === keyCodes.DOWN_ARROW) { - newState.selectedIndex = nextIndex(values.length - 1, values); - } - } - - this.setState(newState); - return; - } - - if (keyCode === keyCodes.UP_ARROW) { - event.preventDefault(); - newState.selectedIndex = previousIndex(selectedIndex, values); - } - - if (keyCode === keyCodes.DOWN_ARROW) { - event.preventDefault(); - newState.selectedIndex = nextIndex(selectedIndex, values); - } - - if (keyCode === keyCodes.ENTER) { - event.preventDefault(); - newState.isOpen = false; - this.onSelect(getKey(selectedIndex, values)); - } - - if (keyCode === keyCodes.TAB) { - newState.isOpen = false; - this.onSelect(getKey(selectedIndex, values)); - } - - if (keyCode === keyCodes.ESCAPE) { - event.preventDefault(); - event.stopPropagation(); - newState.isOpen = false; - newState.selectedIndex = getSelectedIndex(this.props); - } - - if (!_.isEmpty(newState)) { - this.setState(newState); - } - }; - - onPress = () => { - if (this.state.isOpen) { - this._removeListener(); - } else { - this._addListener(); - } - - if (!this.state.isOpen && this.props.onOpen) { - this.props.onOpen(); - } - - this.setState({ isOpen: !this.state.isOpen }); - }; - - onSelect = (newValue) => { - const { name, value, values, onChange } = this.props; - const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties; - - if (Array.isArray(value)) { - let arrayValue = null; - const index = value.indexOf(newValue); - - if (index === -1) { - arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); - } else { - arrayValue = [...value]; - arrayValue.splice(index, 1); - } - onChange({ - name, - value: arrayValue, - additionalProperties - }); - } else { - this.setState({ isOpen: false }); - - onChange({ - name, - value: newValue, - additionalProperties - }); - } - }; - - onMeasure = ({ width }) => { - this.setState({ width }); - }; - - onOptionsModalClose = () => { - this.setState({ isOpen: false }); - }; - - // - // Render - - render() { - const { - className, - disabledClassName, - name, - value, - values, - isDisabled, - isEditable, - isFetching, - hasError, - hasWarning, - valueOptions, - selectedValueOptions, - selectedValueComponent: SelectedValueComponent, - optionComponent: OptionComponent, - onChange - } = this.props; - - const { - selectedIndex, - width, - isOpen, - isMobile - } = this.state; - - const isMultiSelect = Array.isArray(value); - const selectedOption = getSelectedOption(selectedIndex, values); - let selectedValue = value; - - if (!values.length) { - selectedValue = isMultiSelect ? [] : ''; - } - - return ( - <div> - <Manager> - <Reference> - {({ ref }) => ( - <div - ref={ref} - id={this._buttonId} - > - <Measure - whitelist={['width']} - onMeasure={this.onMeasure} - > - { - isEditable ? - <div - className={styles.editableContainer} - > - <TextInput - className={className} - name={name} - value={value} - readOnly={isDisabled} - hasError={hasError} - hasWarning={hasWarning} - onFocus={this.onFocus} - onBlur={this.onBlur} - onChange={onChange} - /> - <Link - className={classNames( - styles.dropdownArrowContainerEditable, - isDisabled ? - styles.dropdownArrowContainerDisabled : - styles.dropdownArrowContainer) - } - onPress={this.onPress} - > - { - isFetching ? - <LoadingIndicator - className={styles.loading} - size={20} - /> : - null - } - - { - isFetching ? - null : - <Icon - name={icons.CARET_DOWN} - /> - } - </Link> - </div> : - <Link - className={classNames( - className, - hasError && styles.hasError, - hasWarning && styles.hasWarning, - isDisabled && disabledClassName - )} - isDisabled={isDisabled} - onBlur={this.onBlur} - onKeyDown={this.onKeyDown} - onPress={this.onPress} - > - <SelectedValueComponent - value={selectedValue} - values={values} - {...selectedValueOptions} - {...selectedOption} - isDisabled={isDisabled} - isMultiSelect={isMultiSelect} - > - {selectedOption ? selectedOption.value : null} - </SelectedValueComponent> - - <div - className={isDisabled ? - styles.dropdownArrowContainerDisabled : - styles.dropdownArrowContainer - } - > - - { - isFetching ? - <LoadingIndicator - className={styles.loading} - size={20} - /> : - null - } - - { - isFetching ? - null : - <Icon - name={icons.CARET_DOWN} - /> - } - </div> - </Link> - } - </Measure> - </div> - )} - </Reference> - <Portal> - <Popper - placement="bottom-start" - modifiers={{ - computeMaxHeight: { - order: 851, - enabled: true, - fn: this.onComputeMaxHeight - } - }} - > - {({ ref, style, scheduleUpdate }) => { - this._scheduleUpdate = scheduleUpdate; - - return ( - <div - ref={ref} - id={this._optionsId} - className={styles.optionsContainer} - style={{ - ...style, - minWidth: width - }} - > - { - isOpen && !isMobile ? - <Scroller - className={styles.options} - style={{ - maxHeight: style.maxHeight - }} - > - { - values.map((v, index) => { - const hasParent = v.parentKey !== undefined; - const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey); - return ( - <OptionComponent - key={v.key} - id={v.key} - depth={depth} - isSelected={isSelectedItem(index, this.props)} - isDisabled={parentSelected} - isMultiSelect={isMultiSelect} - {...valueOptions} - {...v} - isMobile={false} - onSelect={this.onSelect} - > - {v.value} - </OptionComponent> - ); - }) - } - </Scroller> : - null - } - </div> - ); - } - } - </Popper> - </Portal> - </Manager> - - { - isMobile ? - <Modal - className={styles.optionsModal} - size={sizes.EXTRA_SMALL} - isOpen={isOpen} - onModalClose={this.onOptionsModalClose} - > - <ModalBody - className={styles.optionsModalBody} - innerClassName={styles.optionsInnerModalBody} - scrollDirection={scrollDirections.NONE} - > - <Scroller className={styles.optionsModalScroller}> - <div className={styles.mobileCloseButtonContainer}> - <Link - className={styles.mobileCloseButton} - onPress={this.onOptionsModalClose} - > - <Icon - name={icons.CLOSE} - size={18} - /> - </Link> - </div> - - { - values.map((v, index) => { - const hasParent = v.parentKey !== undefined; - const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && value.includes(v.parentKey); - return ( - <OptionComponent - key={v.key} - id={v.key} - depth={depth} - isSelected={isSelectedItem(index, this.props)} - isMultiSelect={isMultiSelect} - isDisabled={parentSelected} - {...valueOptions} - {...v} - isMobile={true} - onSelect={this.onSelect} - > - {v.value} - </OptionComponent> - ); - }) - } - </Scroller> - </ModalBody> - </Modal> : - null - } - </div> - ); - } -} - -EnhancedSelectInput.propTypes = { - className: PropTypes.string, - disabledClassName: PropTypes.string, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - isDisabled: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - isEditable: PropTypes.bool.isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - valueOptions: PropTypes.object.isRequired, - selectedValueOptions: PropTypes.object.isRequired, - selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, - optionComponent: PropTypes.elementType, - onOpen: PropTypes.func, - onChange: PropTypes.func.isRequired -}; - -EnhancedSelectInput.defaultProps = { - className: styles.enhancedSelect, - disabledClassName: styles.isDisabled, - isDisabled: false, - isFetching: false, - isEditable: false, - valueOptions: {}, - selectedValueOptions: {}, - selectedValueComponent: HintedSelectInputSelectedValue, - optionComponent: HintedSelectInputOption -}; - -export default EnhancedSelectInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInputConnector.js b/frontend/src/Components/Form/EnhancedSelectInputConnector.js deleted file mode 100644 index cfbe9484f..000000000 --- a/frontend/src/Components/Form/EnhancedSelectInputConnector.js +++ /dev/null @@ -1,162 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -const importantFieldNames = [ - 'baseUrl', - 'apiPath', - 'apiKey', - 'authToken' -]; - -function getProviderDataKey(providerData) { - if (!providerData || !providerData.fields) { - return null; - } - - const fields = providerData.fields - .filter((f) => importantFieldNames.includes(f.name)) - .map((f) => f.value); - - return fields; -} - -function getSelectOptions(items) { - if (!items) { - return []; - } - - return items.map((option) => { - return { - key: option.value, - value: option.name, - hint: option.hint, - parentKey: option.parentValue, - isDisabled: option.isDisabled, - additionalProperties: option.additionalProperties - }; - }); -} - -function createMapStateToProps() { - return createSelector( - (state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState, - (options) => { - if (options) { - return { - isFetching: options.isFetching, - values: getSelectOptions(options.items) - }; - } - } - ); -} - -const mapDispatchToProps = { - dispatchFetchOptions: fetchOptions, - dispatchClearOptions: clearOptions -}; - -class EnhancedSelectInputConnector extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - refetchRequired: false - }; - } - - componentDidMount = () => { - this._populate(); - }; - - componentDidUpdate = (prevProps) => { - const prevKey = getProviderDataKey(prevProps.providerData); - const nextKey = getProviderDataKey(this.props.providerData); - - if (!_.isEqual(prevKey, nextKey)) { - this.setState({ refetchRequired: true }); - } - }; - - componentWillUnmount = () => { - this._cleanup(); - }; - - // - // Listeners - - onOpen = () => { - if (this.state.refetchRequired) { - this._populate(); - } - }; - - // - // Control - - _populate() { - const { - provider, - providerData, - selectOptionsProviderAction, - dispatchFetchOptions - } = this.props; - - if (selectOptionsProviderAction) { - this.setState({ refetchRequired: false }); - dispatchFetchOptions({ - section: selectOptionsProviderAction, - action: selectOptionsProviderAction, - provider, - providerData - }); - } - } - - _cleanup() { - const { - selectOptionsProviderAction, - dispatchClearOptions - } = this.props; - - if (selectOptionsProviderAction) { - dispatchClearOptions({ section: selectOptionsProviderAction }); - } - } - - // - // Render - - render() { - return ( - <EnhancedSelectInput - {...this.props} - onOpen={this.onOpen} - /> - ); - } -} - -EnhancedSelectInputConnector.propTypes = { - provider: PropTypes.string.isRequired, - providerData: PropTypes.object.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - selectOptionsProviderAction: PropTypes.string, - onChange: PropTypes.func.isRequired, - isFetching: PropTypes.bool.isRequired, - dispatchFetchOptions: PropTypes.func.isRequired, - dispatchClearOptions: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector); diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.js b/frontend/src/Components/Form/EnhancedSelectInputOption.js deleted file mode 100644 index b2783dbaa..000000000 --- a/frontend/src/Components/Form/EnhancedSelectInputOption.js +++ /dev/null @@ -1,113 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons } from 'Helpers/Props'; -import CheckInput from './CheckInput'; -import styles from './EnhancedSelectInputOption.css'; - -class EnhancedSelectInputOption extends Component { - - // - // Listeners - - onPress = (e) => { - e.preventDefault(); - - const { - id, - onSelect - } = this.props; - - onSelect(id); - }; - - onCheckPress = () => { - // CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation. - }; - - // - // Render - - render() { - const { - className, - id, - depth, - isSelected, - isDisabled, - isHidden, - isMultiSelect, - isMobile, - children - } = this.props; - - return ( - <Link - className={classNames( - className, - isSelected && !isMultiSelect && styles.isSelected, - isDisabled && !isMultiSelect && styles.isDisabled, - isHidden && styles.isHidden, - isMobile && styles.isMobile - )} - component="div" - isDisabled={isDisabled} - onPress={this.onPress} - > - - { - depth !== 0 && - <div style={{ width: `${depth * 20}px` }} /> - } - - { - isMultiSelect && - <CheckInput - className={styles.optionCheckInput} - containerClassName={styles.optionCheck} - name={`select-${id}`} - value={isSelected} - isDisabled={isDisabled} - onChange={this.onCheckPress} - /> - } - - {children} - - { - isMobile && - <div className={styles.iconContainer}> - <Icon - name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE} - /> - </div> - } - </Link> - ); - } -} - -EnhancedSelectInputOption.propTypes = { - className: PropTypes.string.isRequired, - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - depth: PropTypes.number.isRequired, - isSelected: PropTypes.bool.isRequired, - isDisabled: PropTypes.bool.isRequired, - isHidden: PropTypes.bool.isRequired, - isMultiSelect: PropTypes.bool.isRequired, - isMobile: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, - onSelect: PropTypes.func.isRequired -}; - -EnhancedSelectInputOption.defaultProps = { - className: styles.option, - depth: 0, - isDisabled: false, - isHidden: false, - isMultiSelect: false -}; - -export default EnhancedSelectInputOption; diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js deleted file mode 100644 index 21ddebb02..000000000 --- a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js +++ /dev/null @@ -1,35 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './EnhancedSelectInputSelectedValue.css'; - -function EnhancedSelectInputSelectedValue(props) { - const { - className, - children, - isDisabled - } = props; - - return ( - <div className={classNames( - className, - isDisabled && styles.isDisabled - )} - > - {children} - </div> - ); -} - -EnhancedSelectInputSelectedValue.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.node, - isDisabled: PropTypes.bool.isRequired -}; - -EnhancedSelectInputSelectedValue.defaultProps = { - className: styles.selectedValue, - isDisabled: false -}; - -export default EnhancedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/Form.js b/frontend/src/Components/Form/Form.js deleted file mode 100644 index 79ad3fe8a..000000000 --- a/frontend/src/Components/Form/Form.js +++ /dev/null @@ -1,66 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import { kinds } from 'Helpers/Props'; -import styles from './Form.css'; - -function Form(props) { - const { - children, - validationErrors, - validationWarnings, - // eslint-disable-next-line no-unused-vars - ...otherProps - } = props; - - return ( - <div> - { - validationErrors.length || validationWarnings.length ? - <div className={styles.validationFailures}> - { - validationErrors.map((error, index) => { - return ( - <Alert - key={index} - kind={kinds.DANGER} - > - {error.errorMessage} - </Alert> - ); - }) - } - - { - validationWarnings.map((warning, index) => { - return ( - <Alert - key={index} - kind={kinds.WARNING} - > - {warning.errorMessage} - </Alert> - ); - }) - } - </div> : - null - } - - {children} - </div> - ); -} - -Form.propTypes = { - children: PropTypes.node.isRequired, - validationErrors: PropTypes.arrayOf(PropTypes.object).isRequired, - validationWarnings: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -Form.defaultProps = { - validationErrors: [], - validationWarnings: [] -}; - -export default Form; diff --git a/frontend/src/Components/Form/Form.tsx b/frontend/src/Components/Form/Form.tsx new file mode 100644 index 000000000..d522019e7 --- /dev/null +++ b/frontend/src/Components/Form/Form.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react'; +import Alert from 'Components/Alert'; +import { kinds } from 'Helpers/Props'; +import { ValidationError, ValidationWarning } from 'typings/pending'; +import styles from './Form.css'; + +export interface FormProps { + children: ReactNode; + validationErrors?: ValidationError[]; + validationWarnings?: ValidationWarning[]; +} + +function Form({ + children, + validationErrors = [], + validationWarnings = [], +}: FormProps) { + return ( + <div> + {validationErrors.length || validationWarnings.length ? ( + <div className={styles.validationFailures}> + {validationErrors.map((error, index) => { + return ( + <Alert key={index} kind={kinds.DANGER}> + {error.errorMessage} + </Alert> + ); + })} + + {validationWarnings.map((warning, index) => { + return ( + <Alert key={index} kind={kinds.WARNING}> + {warning.errorMessage} + </Alert> + ); + })} + </div> + ) : null} + + {children} + </div> + ); +} + +export default Form; diff --git a/frontend/src/Components/Form/FormGroup.js b/frontend/src/Components/Form/FormGroup.js deleted file mode 100644 index f538daa2f..000000000 --- a/frontend/src/Components/Form/FormGroup.js +++ /dev/null @@ -1,56 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { map } from 'Helpers/elementChildren'; -import { sizes } from 'Helpers/Props'; -import styles from './FormGroup.css'; - -function FormGroup(props) { - const { - className, - children, - size, - advancedSettings, - isAdvanced, - ...otherProps - } = props; - - if (!advancedSettings && isAdvanced) { - return null; - } - - const childProps = isAdvanced ? { isAdvanced } : {}; - - return ( - <div - className={classNames( - className, - styles[size] - )} - {...otherProps} - > - { - map(children, (child) => { - return React.cloneElement(child, childProps); - }) - } - </div> - ); -} - -FormGroup.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - size: PropTypes.oneOf(sizes.all).isRequired, - advancedSettings: PropTypes.bool.isRequired, - isAdvanced: PropTypes.bool.isRequired -}; - -FormGroup.defaultProps = { - className: styles.group, - size: sizes.SMALL, - advancedSettings: false, - isAdvanced: false -}; - -export default FormGroup; diff --git a/frontend/src/Components/Form/FormGroup.tsx b/frontend/src/Components/Form/FormGroup.tsx new file mode 100644 index 000000000..1dd879897 --- /dev/null +++ b/frontend/src/Components/Form/FormGroup.tsx @@ -0,0 +1,43 @@ +import classNames from 'classnames'; +import React, { Children, ComponentPropsWithoutRef, ReactNode } from 'react'; +import { Size } from 'Helpers/Props/sizes'; +import styles from './FormGroup.css'; + +interface FormGroupProps extends ComponentPropsWithoutRef<'div'> { + className?: string; + children: ReactNode; + size?: Extract<Size, keyof typeof styles>; + advancedSettings?: boolean; + isAdvanced?: boolean; +} + +function FormGroup(props: FormGroupProps) { + const { + className = styles.group, + children, + size = 'small', + advancedSettings = false, + isAdvanced = false, + ...otherProps + } = props; + + if (!advancedSettings && isAdvanced) { + return null; + } + + const childProps = isAdvanced ? { isAdvanced } : {}; + + return ( + <div className={classNames(className, styles[size])} {...otherProps}> + {Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + return React.cloneElement(child, childProps); + })} + </div> + ); +} + +export default FormGroup; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js deleted file mode 100644 index e3bccaf7c..000000000 --- a/frontend/src/Components/Form/FormInputGroup.js +++ /dev/null @@ -1,311 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Link from 'Components/Link/Link'; -import { inputTypes, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AutoCompleteInput from './AutoCompleteInput'; -import CaptchaInputConnector from './CaptchaInputConnector'; -import CheckInput from './CheckInput'; -import DeviceInputConnector from './DeviceInputConnector'; -import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector'; -import EnhancedSelectInput from './EnhancedSelectInput'; -import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; -import FormInputHelpText from './FormInputHelpText'; -import IndexerFlagsSelectInput from './IndexerFlagsSelectInput'; -import IndexerSelectInputConnector from './IndexerSelectInputConnector'; -import KeyValueListInput from './KeyValueListInput'; -import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput'; -import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput'; -import NumberInput from './NumberInput'; -import OAuthInputConnector from './OAuthInputConnector'; -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'; -import TextArea from './TextArea'; -import TextInput from './TextInput'; -import TextTagInputConnector from './TextTagInputConnector'; -import UMaskInput from './UMaskInput'; -import styles from './FormInputGroup.css'; - -function getComponent(type) { - switch (type) { - case inputTypes.AUTO_COMPLETE: - return AutoCompleteInput; - - case inputTypes.CAPTCHA: - return CaptchaInputConnector; - - case inputTypes.CHECK: - return CheckInput; - - case inputTypes.DEVICE: - return DeviceInputConnector; - - case inputTypes.KEY_VALUE_LIST: - return KeyValueListInput; - - case inputTypes.MONITOR_EPISODES_SELECT: - return MonitorEpisodesSelectInput; - - case inputTypes.MONITOR_NEW_ITEMS_SELECT: - return MonitorNewItemsSelectInput; - - case inputTypes.NUMBER: - return NumberInput; - - case inputTypes.OAUTH: - return OAuthInputConnector; - - case inputTypes.PASSWORD: - return PasswordInput; - - case inputTypes.PATH: - return PathInputConnector; - - case inputTypes.QUALITY_PROFILE_SELECT: - return QualityProfileSelectInputConnector; - - case inputTypes.INDEXER_SELECT: - return IndexerSelectInputConnector; - - case inputTypes.INDEXER_FLAGS_SELECT: - return IndexerFlagsSelectInput; - - case inputTypes.DOWNLOAD_CLIENT_SELECT: - return DownloadClientSelectInputConnector; - - case inputTypes.ROOT_FOLDER_SELECT: - return RootFolderSelectInputConnector; - - case inputTypes.SELECT: - return EnhancedSelectInput; - - case inputTypes.DYNAMIC_SELECT: - return EnhancedSelectInputConnector; - - case inputTypes.SERIES_TAG: - return SeriesTagInput; - - case inputTypes.SERIES_TYPE_SELECT: - return SeriesTypeSelectInput; - - case inputTypes.TAG: - return TagInputConnector; - - case inputTypes.TEXT_AREA: - return TextArea; - - case inputTypes.TEXT_TAG: - return TextTagInputConnector; - - case inputTypes.TAG_SELECT: - return TagSelectInputConnector; - - case inputTypes.UMASK: - return UMaskInput; - - default: - return TextInput; - } -} - -function FormInputGroup(props) { - const { - className, - containerClassName, - inputClassName, - type, - unit, - buttons, - helpText, - helpTexts, - helpTextWarning, - helpLink, - pending, - errors, - warnings, - ...otherProps - } = props; - - const InputComponent = getComponent(type); - const checkInput = type === inputTypes.CHECK; - const hasError = !!errors.length; - const hasWarning = !hasError && !!warnings.length; - const buttonsArray = React.Children.toArray(buttons); - const lastButtonIndex = buttonsArray.length - 1; - const hasButton = !!buttonsArray.length; - - return ( - <div className={containerClassName}> - <div className={className}> - <div className={styles.inputContainer}> - <InputComponent - className={inputClassName} - helpText={helpText} - helpTextWarning={helpTextWarning} - hasError={hasError} - hasWarning={hasWarning} - hasButton={hasButton} - {...otherProps} - /> - - { - unit && - <div - className={ - type === inputTypes.NUMBER ? - styles.inputUnitNumber : - styles.inputUnit - } - > - {unit} - </div> - } - </div> - - { - buttonsArray.map((button, index) => { - return React.cloneElement( - button, - { - isLastButton: index === lastButtonIndex - } - ); - }) - } - - {/* <div className={styles.pendingChangesContainer}> - { - pending && - <Icon - name={icons.UNSAVED_SETTING} - className={styles.pendingChangesIcon} - title="Change has not been saved yet" - /> - } - </div> */} - </div> - - { - !checkInput && helpText && - <FormInputHelpText - text={helpText} - /> - } - - { - !checkInput && helpTexts && - <div> - { - helpTexts.map((text, index) => { - return ( - <FormInputHelpText - key={index} - text={text} - isCheckInput={checkInput} - /> - ); - }) - } - </div> - } - - { - (!checkInput || helpText) && helpTextWarning && - <FormInputHelpText - text={helpTextWarning} - isWarning={true} - /> - } - - { - helpLink && - <Link - to={helpLink} - > - {translate('MoreInfo')} - </Link> - } - - { - errors.map((error, index) => { - return ( - <FormInputHelpText - key={index} - text={error.message} - link={error.link} - tooltip={error.detailedMessage} - isError={true} - isCheckInput={checkInput} - /> - ); - }) - } - - { - warnings.map((warning, index) => { - return ( - <FormInputHelpText - key={index} - text={warning.message} - link={warning.link} - tooltip={warning.detailedMessage} - isWarning={true} - isCheckInput={checkInput} - /> - ); - }) - } - </div> - ); -} - -FormInputGroup.propTypes = { - className: PropTypes.string.isRequired, - containerClassName: PropTypes.string.isRequired, - inputClassName: PropTypes.string, - name: PropTypes.string.isRequired, - value: PropTypes.any, - values: PropTypes.arrayOf(PropTypes.any), - placeholder: PropTypes.string, - delimiters: PropTypes.arrayOf(PropTypes.string), - isDisabled: PropTypes.bool, - type: PropTypes.string.isRequired, - kind: PropTypes.oneOf(kinds.all), - min: PropTypes.number, - max: PropTypes.number, - unit: PropTypes.string, - buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), - helpText: PropTypes.string, - helpTexts: PropTypes.arrayOf(PropTypes.string), - helpTextWarning: PropTypes.string, - helpLink: PropTypes.string, - autoFocus: PropTypes.bool, - canEdit: PropTypes.bool, - includeNoChange: PropTypes.bool, - includeNoChangeDisabled: PropTypes.bool, - includeAny: PropTypes.bool, - selectedValueOptions: PropTypes.object, - indexerFlags: PropTypes.number, - pending: PropTypes.bool, - errors: PropTypes.arrayOf(PropTypes.object), - warnings: PropTypes.arrayOf(PropTypes.object), - onChange: PropTypes.func.isRequired -}; - -FormInputGroup.defaultProps = { - className: styles.inputGroup, - containerClassName: styles.inputGroupContainer, - type: inputTypes.TEXT, - buttons: [], - helpTexts: [], - errors: [], - warnings: [] -}; - -export default FormInputGroup; diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx new file mode 100644 index 000000000..897f19bbd --- /dev/null +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -0,0 +1,292 @@ +import React, { ReactNode } from 'react'; +import Link from 'Components/Link/Link'; +import { inputTypes } from 'Helpers/Props'; +import { InputType } from 'Helpers/Props/inputTypes'; +import { Kind } from 'Helpers/Props/kinds'; +import { ValidationError, ValidationWarning } from 'typings/pending'; +import translate from 'Utilities/String/translate'; +import AutoCompleteInput from './AutoCompleteInput'; +import CaptchaInput from './CaptchaInput'; +import CheckInput from './CheckInput'; +import { FormInputButtonProps } from './FormInputButton'; +import FormInputHelpText from './FormInputHelpText'; +import NumberInput from './NumberInput'; +import OAuthInput from './OAuthInput'; +import PasswordInput from './PasswordInput'; +import PathInput from './PathInput'; +import DownloadClientSelectInput from './Select/DownloadClientSelectInput'; +import EnhancedSelectInput from './Select/EnhancedSelectInput'; +import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput'; +import IndexerSelectInput from './Select/IndexerSelectInput'; +import MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput'; +import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput'; +import ProviderDataSelectInput from './Select/ProviderOptionSelectInput'; +import QualityProfileSelectInput from './Select/QualityProfileSelectInput'; +import RootFolderSelectInput from './Select/RootFolderSelectInput'; +import SeriesTypeSelectInput from './Select/SeriesTypeSelectInput'; +import UMaskInput from './Select/UMaskInput'; +import DeviceInput from './Tag/DeviceInput'; +import SeriesTagInput from './Tag/SeriesTagInput'; +import TagSelectInput from './Tag/TagSelectInput'; +import TextTagInput from './Tag/TextTagInput'; +import TextArea from './TextArea'; +import TextInput from './TextInput'; +import styles from './FormInputGroup.css'; + +function getComponent(type: InputType) { + switch (type) { + case inputTypes.AUTO_COMPLETE: + return AutoCompleteInput; + + case inputTypes.CAPTCHA: + return CaptchaInput; + + case inputTypes.CHECK: + return CheckInput; + + case inputTypes.DEVICE: + return DeviceInput; + + case inputTypes.MONITOR_EPISODES_SELECT: + return MonitorEpisodesSelectInput; + + case inputTypes.MONITOR_NEW_ITEMS_SELECT: + return MonitorNewItemsSelectInput; + + case inputTypes.NUMBER: + return NumberInput; + + case inputTypes.OAUTH: + return OAuthInput; + + case inputTypes.PASSWORD: + return PasswordInput; + + case inputTypes.PATH: + return PathInput; + + case inputTypes.QUALITY_PROFILE_SELECT: + return QualityProfileSelectInput; + + case inputTypes.INDEXER_SELECT: + return IndexerSelectInput; + + case inputTypes.INDEXER_FLAGS_SELECT: + return IndexerFlagsSelectInput; + + case inputTypes.DOWNLOAD_CLIENT_SELECT: + return DownloadClientSelectInput; + + case inputTypes.ROOT_FOLDER_SELECT: + return RootFolderSelectInput; + + case inputTypes.SELECT: + return EnhancedSelectInput; + + case inputTypes.DYNAMIC_SELECT: + return ProviderDataSelectInput; + + case inputTypes.TAG: + case inputTypes.SERIES_TAG: + return SeriesTagInput; + + case inputTypes.SERIES_TYPE_SELECT: + return SeriesTypeSelectInput; + + case inputTypes.TEXT_AREA: + return TextArea; + + case inputTypes.TEXT_TAG: + return TextTagInput; + + case inputTypes.TAG_SELECT: + return TagSelectInput; + + case inputTypes.UMASK: + return UMaskInput; + + default: + return TextInput; + } +} + +// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type +interface ValidationMessage { + message: string; +} + +interface FormInputGroupProps<T> { + className?: string; + containerClassName?: string; + inputClassName?: string; + name: string; + value?: unknown; + values?: unknown[]; + isDisabled?: boolean; + type?: InputType; + kind?: Kind; + min?: number; + max?: number; + unit?: string; + buttons?: ReactNode | ReactNode[]; + helpText?: string; + helpTexts?: string[]; + helpTextWarning?: string; + helpLink?: string; + placeholder?: string; + autoFocus?: boolean; + includeNoChange?: boolean; + includeNoChangeDisabled?: boolean; + selectedValueOptions?: object; + indexerFlags?: number; + pending?: boolean; + canEdit?: boolean; + includeAny?: boolean; + delimiters?: string[]; + errors?: (ValidationMessage | ValidationError)[]; + warnings?: (ValidationMessage | ValidationWarning)[]; + onChange: (args: T) => void; +} + +function FormInputGroup<T>(props: FormInputGroupProps<T>) { + const { + className = styles.inputGroup, + containerClassName = styles.inputGroupContainer, + inputClassName, + type = 'text', + unit, + buttons = [], + helpText, + helpTexts = [], + helpTextWarning, + helpLink, + pending, + errors = [], + warnings = [], + ...otherProps + } = props; + + const InputComponent = getComponent(type); + const checkInput = type === inputTypes.CHECK; + const hasError = !!errors.length; + const hasWarning = !hasError && !!warnings.length; + const buttonsArray = React.Children.toArray(buttons); + const lastButtonIndex = buttonsArray.length - 1; + const hasButton = !!buttonsArray.length; + + return ( + <div className={containerClassName}> + <div className={className}> + <div className={styles.inputContainer}> + {/* @ts-expect-error - need to pass through all the expected options */} + <InputComponent + className={inputClassName} + helpText={helpText} + helpTextWarning={helpTextWarning} + hasError={hasError} + hasWarning={hasWarning} + hasButton={hasButton} + {...otherProps} + /> + + {unit && ( + <div + className={ + type === inputTypes.NUMBER + ? styles.inputUnitNumber + : styles.inputUnit + } + > + {unit} + </div> + )} + </div> + + {buttonsArray.map((button, index) => { + if (!React.isValidElement<FormInputButtonProps>(button)) { + return button; + } + + return React.cloneElement(button, { + isLastButton: index === lastButtonIndex, + }); + })} + + {/* <div className={styles.pendingChangesContainer}> + { + pending && + <Icon + name={icons.UNSAVED_SETTING} + className={styles.pendingChangesIcon} + title="Change has not been saved yet" + /> + } + </div> */} + </div> + + {!checkInput && helpText ? <FormInputHelpText text={helpText} /> : null} + + {!checkInput && helpTexts ? ( + <div> + {helpTexts.map((text, index) => { + return ( + <FormInputHelpText + key={index} + text={text} + isCheckInput={checkInput} + /> + ); + })} + </div> + ) : null} + + {(!checkInput || helpText) && helpTextWarning ? ( + <FormInputHelpText text={helpTextWarning} isWarning={true} /> + ) : null} + + {helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null} + + {errors.map((error, index) => { + return 'message' in error ? ( + <FormInputHelpText + key={index} + text={error.message} + isError={true} + isCheckInput={checkInput} + /> + ) : ( + <FormInputHelpText + key={index} + text={error.errorMessage} + link={error.infoLink} + tooltip={error.detailedDescription} + isError={true} + isCheckInput={checkInput} + /> + ); + })} + + {warnings.map((warning, index) => { + return 'message' in warning ? ( + <FormInputHelpText + key={index} + text={warning.message} + isWarning={true} + isCheckInput={checkInput} + /> + ) : ( + <FormInputHelpText + key={index} + text={warning.errorMessage} + link={warning.infoLink} + tooltip={warning.detailedDescription} + isWarning={true} + isCheckInput={checkInput} + /> + ); + })} + </div> + ); +} + +export default FormInputGroup; diff --git a/frontend/src/Components/Form/FormInputHelpText.js b/frontend/src/Components/Form/FormInputHelpText.js deleted file mode 100644 index 00024684e..000000000 --- a/frontend/src/Components/Form/FormInputHelpText.js +++ /dev/null @@ -1,74 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons } from 'Helpers/Props'; -import styles from './FormInputHelpText.css'; - -function FormInputHelpText(props) { - const { - className, - text, - link, - tooltip, - isError, - isWarning, - isCheckInput - } = props; - - return ( - <div className={classNames( - className, - isError && styles.isError, - isWarning && styles.isWarning, - isCheckInput && styles.isCheckInput - )} - > - {text} - - { - link ? - <Link - className={styles.link} - to={link} - title={tooltip} - > - <Icon - name={icons.EXTERNAL_LINK} - /> - </Link> : - null - } - - { - !link && tooltip ? - <Icon - containerClassName={styles.details} - name={icons.INFO} - title={tooltip} - /> : - null - } - </div> - ); -} - -FormInputHelpText.propTypes = { - className: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - link: PropTypes.string, - tooltip: PropTypes.string, - isError: PropTypes.bool, - isWarning: PropTypes.bool, - isCheckInput: PropTypes.bool -}; - -FormInputHelpText.defaultProps = { - className: styles.helpText, - isError: false, - isWarning: false, - isCheckInput: false -}; - -export default FormInputHelpText; diff --git a/frontend/src/Components/Form/FormInputHelpText.tsx b/frontend/src/Components/Form/FormInputHelpText.tsx new file mode 100644 index 000000000..1531d9585 --- /dev/null +++ b/frontend/src/Components/Form/FormInputHelpText.tsx @@ -0,0 +1,55 @@ +import classNames from 'classnames'; +import React from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import styles from './FormInputHelpText.css'; + +interface FormInputHelpTextProps { + className?: string; + text: string; + link?: string; + tooltip?: string; + isError?: boolean; + isWarning?: boolean; + isCheckInput?: boolean; +} + +function FormInputHelpText({ + className = styles.helpText, + text, + link, + tooltip, + isError = false, + isWarning = false, + isCheckInput = false, +}: FormInputHelpTextProps) { + return ( + <div + className={classNames( + className, + isError && styles.isError, + isWarning && styles.isWarning, + isCheckInput && styles.isCheckInput + )} + > + {text} + + {link ? ( + <Link className={styles.link} to={link} title={tooltip}> + <Icon name={icons.EXTERNAL_LINK} /> + </Link> + ) : null} + + {!link && tooltip ? ( + <Icon + containerClassName={styles.details} + name={icons.INFO} + title={tooltip} + /> + ) : null} + </div> + ); +} + +export default FormInputHelpText; diff --git a/frontend/src/Components/Form/FormLabel.js b/frontend/src/Components/Form/FormLabel.js deleted file mode 100644 index d4a4bcffc..000000000 --- a/frontend/src/Components/Form/FormLabel.js +++ /dev/null @@ -1,52 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { sizes } from 'Helpers/Props'; -import styles from './FormLabel.css'; - -function FormLabel(props) { - const { - children, - className, - errorClassName, - size, - name, - hasError, - isAdvanced, - ...otherProps - } = props; - - return ( - <label - {...otherProps} - className={classNames( - className, - styles[size], - hasError && errorClassName, - isAdvanced && styles.isAdvanced - )} - htmlFor={name} - > - {children} - </label> - ); -} - -FormLabel.propTypes = { - children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired, - className: PropTypes.string, - errorClassName: PropTypes.string, - size: PropTypes.oneOf(sizes.all), - name: PropTypes.string, - hasError: PropTypes.bool, - isAdvanced: PropTypes.bool -}; - -FormLabel.defaultProps = { - className: styles.label, - errorClassName: styles.hasError, - isAdvanced: false, - size: sizes.LARGE -}; - -export default FormLabel; diff --git a/frontend/src/Components/Form/FormLabel.tsx b/frontend/src/Components/Form/FormLabel.tsx new file mode 100644 index 000000000..4f29e6ac6 --- /dev/null +++ b/frontend/src/Components/Form/FormLabel.tsx @@ -0,0 +1,42 @@ +import classNames from 'classnames'; +import React, { ReactNode } from 'react'; +import { Size } from 'Helpers/Props/sizes'; +import styles from './FormLabel.css'; + +interface FormLabelProps { + children: ReactNode; + className?: string; + errorClassName?: string; + size?: Extract<Size, keyof typeof styles>; + name?: string; + hasError?: boolean; + isAdvanced?: boolean; +} + +function FormLabel(props: FormLabelProps) { + const { + children, + className = styles.label, + errorClassName = styles.hasError, + size = 'large', + name, + hasError, + isAdvanced = false, + } = props; + + return ( + <label + className={classNames( + className, + styles[size], + hasError && errorClassName, + isAdvanced && styles.isAdvanced + )} + htmlFor={name} + > + {children} + </label> + ); +} + +export default FormLabel; diff --git a/frontend/src/Components/Form/HintedSelectInputOption.js b/frontend/src/Components/Form/HintedSelectInputOption.js deleted file mode 100644 index 4957ece2a..000000000 --- a/frontend/src/Components/Form/HintedSelectInputOption.js +++ /dev/null @@ -1,66 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import EnhancedSelectInputOption from './EnhancedSelectInputOption'; -import styles from './HintedSelectInputOption.css'; - -function HintedSelectInputOption(props) { - const { - id, - value, - hint, - depth, - isSelected, - isDisabled, - isMultiSelect, - isMobile, - ...otherProps - } = props; - - return ( - <EnhancedSelectInputOption - id={id} - depth={depth} - isSelected={isSelected} - isDisabled={isDisabled} - isHidden={isDisabled} - isMultiSelect={isMultiSelect} - isMobile={isMobile} - {...otherProps} - > - <div className={classNames( - styles.optionText, - isMobile && styles.isMobile - )} - > - <div>{value}</div> - - { - hint != null && - <div className={styles.hintText}> - {hint} - </div> - } - </div> - </EnhancedSelectInputOption> - ); -} - -HintedSelectInputOption.propTypes = { - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - value: PropTypes.string.isRequired, - hint: PropTypes.node, - depth: PropTypes.number, - isSelected: PropTypes.bool.isRequired, - isDisabled: PropTypes.bool.isRequired, - isMultiSelect: PropTypes.bool.isRequired, - isMobile: PropTypes.bool.isRequired -}; - -HintedSelectInputOption.defaultProps = { - isDisabled: false, - isHidden: false, - isMultiSelect: false -}; - -export default HintedSelectInputOption; diff --git a/frontend/src/Components/Form/HintedSelectInputSelectedValue.js b/frontend/src/Components/Form/HintedSelectInputSelectedValue.js deleted file mode 100644 index a3fecf324..000000000 --- a/frontend/src/Components/Form/HintedSelectInputSelectedValue.js +++ /dev/null @@ -1,68 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; -import styles from './HintedSelectInputSelectedValue.css'; - -function HintedSelectInputSelectedValue(props) { - const { - value, - values, - hint, - isMultiSelect, - includeHint, - ...otherProps - } = props; - - const valuesMap = isMultiSelect && _.keyBy(values, 'key'); - - return ( - <EnhancedSelectInputSelectedValue - className={styles.selectedValue} - {...otherProps} - > - <div className={styles.valueText}> - { - isMultiSelect ? - value.map((key, index) => { - const v = valuesMap[key]; - return ( - <Label key={key}> - {v ? v.value : key} - </Label> - ); - }) : - null - } - - { - isMultiSelect ? null : value - } - </div> - - { - hint != null && includeHint ? - <div className={styles.hintText}> - {hint} - </div> : - null - } - </EnhancedSelectInputSelectedValue> - ); -} - -HintedSelectInputSelectedValue.propTypes = { - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - hint: PropTypes.string, - isMultiSelect: PropTypes.bool.isRequired, - includeHint: PropTypes.bool.isRequired -}; - -HintedSelectInputSelectedValue.defaultProps = { - isMultiSelect: false, - includeHint: true -}; - -export default HintedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js deleted file mode 100644 index 5f62becbb..000000000 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ /dev/null @@ -1,97 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchIndexers } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.indexers, - (state, { includeAny }) => includeAny, - (indexers, includeAny) => { - const { - isFetching, - isPopulated, - error, - items - } = indexers; - - const values = _.map(items.sort(sortByProp('name')), (indexer) => { - return { - key: indexer.id, - value: indexer.name - }; - }); - - if (includeAny) { - values.unshift({ - key: 0, - value: `(${translate('Any')})` - }); - } - - return { - isFetching, - isPopulated, - error, - values - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchIndexers: fetchIndexers -}; - -class IndexerSelectInputConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.dispatchFetchIndexers(); - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - this.props.onChange({ name, value: parseInt(value) }); - }; - - // - // Render - - render() { - return ( - <EnhancedSelectInput - {...this.props} - onChange={this.onChange} - /> - ); - } -} - -IndexerSelectInputConnector.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - includeAny: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - dispatchFetchIndexers: PropTypes.func.isRequired -}; - -IndexerSelectInputConnector.defaultProps = { - includeAny: false -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector); diff --git a/frontend/src/Components/Form/KeyValueListInput.css b/frontend/src/Components/Form/KeyValueListInput.css deleted file mode 100644 index d86e6a512..000000000 --- a/frontend/src/Components/Form/KeyValueListInput.css +++ /dev/null @@ -1,21 +0,0 @@ -.inputContainer { - composes: input from '~Components/Form/Input.css'; - - position: relative; - min-height: 35px; - height: auto; - - &.isFocused { - outline: 0; - border-color: var(--inputFocusBorderColor); - box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor); - } -} - -.hasError { - composes: hasError from '~Components/Form/Input.css'; -} - -.hasWarning { - composes: hasWarning from '~Components/Form/Input.css'; -} diff --git a/frontend/src/Components/Form/KeyValueListInput.css.d.ts b/frontend/src/Components/Form/KeyValueListInput.css.d.ts deleted file mode 100644 index 972f108c9..000000000 --- a/frontend/src/Components/Form/KeyValueListInput.css.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'hasError': string; - 'hasWarning': string; - 'inputContainer': string; - 'isFocused': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js deleted file mode 100644 index 3e73d74f3..000000000 --- a/frontend/src/Components/Form/KeyValueListInput.js +++ /dev/null @@ -1,156 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import KeyValueListInputItem from './KeyValueListInputItem'; -import styles from './KeyValueListInput.css'; - -class KeyValueListInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isFocused: false - }; - } - - // - // Listeners - - onItemChange = (index, itemValue) => { - const { - name, - value, - onChange - } = this.props; - - const newValue = [...value]; - - if (index == null) { - newValue.push(itemValue); - } else { - newValue.splice(index, 1, itemValue); - } - - onChange({ - name, - value: newValue - }); - }; - - onRemoveItem = (index) => { - const { - name, - value, - onChange - } = this.props; - - const newValue = [...value]; - newValue.splice(index, 1); - - onChange({ - name, - value: newValue - }); - }; - - onFocus = () => { - this.setState({ - isFocused: true - }); - }; - - onBlur = () => { - this.setState({ - isFocused: false - }); - - const { - name, - value, - onChange - } = this.props; - - const newValue = value.reduce((acc, v) => { - if (v.key || v.value) { - acc.push(v); - } - - return acc; - }, []); - - if (newValue.length !== value.length) { - onChange({ - name, - value: newValue - }); - } - }; - - // - // Render - - render() { - const { - className, - value, - keyPlaceholder, - valuePlaceholder, - hasError, - hasWarning - } = this.props; - - const { isFocused } = this.state; - - return ( - <div className={classNames( - className, - isFocused && styles.isFocused, - hasError && styles.hasError, - hasWarning && styles.hasWarning - )} - > - { - [...value, { key: '', value: '' }].map((v, index) => { - return ( - <KeyValueListInputItem - key={index} - index={index} - keyValue={v.key} - value={v.value} - keyPlaceholder={keyPlaceholder} - valuePlaceholder={valuePlaceholder} - isNew={index === value.length} - onChange={this.onItemChange} - onRemove={this.onRemoveItem} - onFocus={this.onFocus} - onBlur={this.onBlur} - /> - ); - }) - } - </div> - ); - } -} - -KeyValueListInput.propTypes = { - className: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.object).isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - keyPlaceholder: PropTypes.string, - valuePlaceholder: PropTypes.string, - onChange: PropTypes.func.isRequired -}; - -KeyValueListInput.defaultProps = { - className: styles.inputContainer, - value: [] -}; - -export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css deleted file mode 100644 index 75d37b74f..000000000 --- a/frontend/src/Components/Form/KeyValueListInputItem.css +++ /dev/null @@ -1,28 +0,0 @@ -.itemContainer { - display: flex; - margin-bottom: 3px; - border-bottom: 1px solid var(--inputBorderColor); - - &:last-child { - margin-bottom: 0; - } -} - -.keyInputWrapper { - flex: 6 0 0; -} - -.valueInputWrapper { - flex: 1 0 0; - min-width: 40px; -} - -.buttonWrapper { - flex: 0 0 22px; -} - -.keyInput, -.valueInput { - width: 100%; - border: none; -} diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts deleted file mode 100644 index aa0c1be13..000000000 --- a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'buttonWrapper': string; - 'itemContainer': string; - 'keyInput': string; - 'keyInputWrapper': string; - 'valueInput': string; - 'valueInputWrapper': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js deleted file mode 100644 index 9f5abce2f..000000000 --- a/frontend/src/Components/Form/KeyValueListInputItem.js +++ /dev/null @@ -1,124 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import { icons } from 'Helpers/Props'; -import TextInput from './TextInput'; -import styles from './KeyValueListInputItem.css'; - -class KeyValueListInputItem extends Component { - - // - // Listeners - - onKeyChange = ({ value: keyValue }) => { - const { - index, - value, - onChange - } = this.props; - - onChange(index, { key: keyValue, value }); - }; - - onValueChange = ({ value }) => { - // TODO: Validate here or validate at a lower level component - - const { - index, - keyValue, - onChange - } = this.props; - - onChange(index, { key: keyValue, value }); - }; - - onRemovePress = () => { - const { - index, - onRemove - } = this.props; - - onRemove(index); - }; - - onFocus = () => { - this.props.onFocus(); - }; - - onBlur = () => { - this.props.onBlur(); - }; - - // - // Render - - render() { - const { - keyValue, - value, - keyPlaceholder, - valuePlaceholder, - isNew - } = this.props; - - return ( - <div className={styles.itemContainer}> - <div className={styles.keyInputWrapper}> - <TextInput - className={styles.keyInput} - name="key" - value={keyValue} - placeholder={keyPlaceholder} - onChange={this.onKeyChange} - onFocus={this.onFocus} - onBlur={this.onBlur} - /> - </div> - - <div className={styles.valueInputWrapper}> - <TextInput - className={styles.valueInput} - name="value" - value={value} - placeholder={valuePlaceholder} - onChange={this.onValueChange} - onFocus={this.onFocus} - onBlur={this.onBlur} - /> - </div> - - <div className={styles.buttonWrapper}> - { - isNew ? - null : - <IconButton - name={icons.REMOVE} - tabIndex={-1} - onPress={this.onRemovePress} - /> - } - </div> - </div> - ); - } -} - -KeyValueListInputItem.propTypes = { - index: PropTypes.number, - keyValue: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - keyPlaceholder: PropTypes.string.isRequired, - valuePlaceholder: PropTypes.string.isRequired, - isNew: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - onFocus: PropTypes.func.isRequired, - onBlur: PropTypes.func.isRequired -}; - -KeyValueListInputItem.defaultProps = { - keyPlaceholder: 'Key', - valuePlaceholder: 'Value' -}; - -export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/LanguageSelectInputConnector.js b/frontend/src/Components/Form/LanguageSelectInputConnector.js deleted file mode 100644 index dd3a52017..000000000 --- a/frontend/src/Components/Form/LanguageSelectInputConnector.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - (state, { values }) => values, - ( languages ) => { - - const minId = languages.reduce((min, v) => (v.key < 1 ? v.key : min), languages[0].key); - - const values = languages.map(({ key, value }) => { - return { - key, - value, - dividerAfter: minId < 1 ? key === minId : false - }; - }); - - return { - values - }; - } - ); -} - -class LanguageSelectInputConnector extends Component { - - // - // Render - - render() { - - return ( - <EnhancedSelectInput - {...this.props} - onChange={this.props.onChange} - /> - ); - } -} - -LanguageSelectInputConnector.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - onChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps)(LanguageSelectInputConnector); diff --git a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js deleted file mode 100644 index a4ee4fd85..000000000 --- a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import monitorOptions from 'Utilities/Series/monitorOptions'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function MonitorEpisodesSelectInput(props) { - const { - includeNoChange, - includeMixed, - ...otherProps - } = props; - - const values = [...monitorOptions]; - - if (includeNoChange) { - values.unshift({ - key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: true - }); - } - - if (includeMixed) { - values.unshift({ - key: 'mixed', - get value() { - return `(${translate('Mixed')})`; - }, - isDisabled: true - }); - } - - return ( - <EnhancedSelectInput - values={values} - {...otherProps} - /> - ); -} - -MonitorEpisodesSelectInput.propTypes = { - includeNoChange: PropTypes.bool.isRequired, - includeMixed: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired -}; - -MonitorEpisodesSelectInput.defaultProps = { - includeNoChange: false, - includeMixed: false -}; - -export default MonitorEpisodesSelectInput; diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js deleted file mode 100644 index be179c3e5..000000000 --- a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function MonitorNewItemsSelectInput(props) { - const { - includeNoChange, - includeMixed, - ...otherProps - } = props; - - const values = [...monitorNewItemsOptions]; - - if (includeNoChange) { - values.unshift({ - key: 'noChange', - value: 'No Change', - isDisabled: true - }); - } - - if (includeMixed) { - values.unshift({ - key: 'mixed', - value: '(Mixed)', - isDisabled: true - }); - } - - return ( - <EnhancedSelectInput - values={values} - {...otherProps} - /> - ); -} - -MonitorNewItemsSelectInput.propTypes = { - includeNoChange: PropTypes.bool.isRequired, - includeMixed: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired -}; - -MonitorNewItemsSelectInput.defaultProps = { - includeNoChange: false, - includeMixed: false -}; - -export default MonitorNewItemsSelectInput; diff --git a/frontend/src/Components/Form/NumberInput.js b/frontend/src/Components/Form/NumberInput.js deleted file mode 100644 index cac274d95..000000000 --- a/frontend/src/Components/Form/NumberInput.js +++ /dev/null @@ -1,126 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TextInput from './TextInput'; - -function parseValue(props, value) { - const { - isFloat, - min, - max - } = props; - - if (value == null || value === '') { - return null; - } - - let newValue = isFloat ? parseFloat(value) : parseInt(value); - - if (min != null && newValue != null && newValue < min) { - newValue = min; - } else if (max != null && newValue != null && newValue > max) { - newValue = max; - } - - return newValue; -} - -class NumberInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - value: props.value == null ? '' : props.value.toString(), - isFocused: false - }; - } - - componentDidUpdate(prevProps, prevState) { - const { value } = this.props; - - if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) { - this.setState({ - value: value == null ? '' : value.toString() - }); - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - this.setState({ value }); - - this.props.onChange({ - name, - value: parseValue(this.props, value) - }); - - }; - - onFocus = () => { - this.setState({ isFocused: true }); - }; - - onBlur = () => { - const { - name, - onChange - } = this.props; - - const { value } = this.state; - const parsedValue = parseValue(this.props, value); - const stringValue = parsedValue == null ? '' : parsedValue.toString(); - - if (stringValue === value) { - this.setState({ isFocused: false }); - } else { - this.setState({ - value: stringValue, - isFocused: false - }); - } - - onChange({ - name, - value: parsedValue - }); - }; - - // - // Render - - render() { - const value = this.state.value; - - return ( - <TextInput - {...this.props} - type="number" - value={value == null ? '' : value} - onChange={this.onChange} - onBlur={this.onBlur} - onFocus={this.onFocus} - /> - ); - } -} - -NumberInput.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.number, - min: PropTypes.number, - max: PropTypes.number, - isFloat: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired -}; - -NumberInput.defaultProps = { - value: null, - isFloat: false -}; - -export default NumberInput; diff --git a/frontend/src/Components/Form/NumberInput.tsx b/frontend/src/Components/Form/NumberInput.tsx new file mode 100644 index 000000000..a5e1fcb64 --- /dev/null +++ b/frontend/src/Components/Form/NumberInput.tsx @@ -0,0 +1,108 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { InputChanged } from 'typings/inputs'; +import TextInput, { TextInputProps } from './TextInput'; + +function parseValue( + value: string | null | undefined, + isFloat: boolean, + min: number | undefined, + max: number | undefined +) { + if (value == null || value === '') { + return null; + } + + let newValue = isFloat ? parseFloat(value) : parseInt(value); + + if (min != null && newValue != null && newValue < min) { + newValue = min; + } else if (max != null && newValue != null && newValue > max) { + newValue = max; + } + + return newValue; +} + +interface NumberInputProps + extends Omit<TextInputProps<number | null>, 'value'> { + value?: number | null; + min?: number; + max?: number; + isFloat?: boolean; +} + +function NumberInput({ + name, + value: inputValue = null, + isFloat = false, + min, + max, + onChange, + ...otherProps +}: NumberInputProps) { + const [value, setValue] = useState( + inputValue == null ? '' : inputValue.toString() + ); + const isFocused = useRef(false); + const previousValue = usePrevious(inputValue); + + const handleChange = useCallback( + ({ name, value: newValue }: InputChanged<string>) => { + setValue(newValue); + + onChange({ + name, + value: parseValue(newValue, isFloat, min, max), + }); + }, + [isFloat, min, max, onChange, setValue] + ); + + const handleFocus = useCallback(() => { + isFocused.current = true; + }, []); + + const handleBlur = useCallback(() => { + const parsedValue = parseValue(value, isFloat, min, max); + const stringValue = parsedValue == null ? '' : parsedValue.toString(); + + if (stringValue !== value) { + setValue(stringValue); + } + + onChange({ + name, + value: parsedValue, + }); + + isFocused.current = false; + }, [name, value, isFloat, min, max, onChange]); + + useEffect(() => { + if ( + // @ts-expect-error inputValue may be null + !isNaN(inputValue) && + inputValue !== previousValue && + !isFocused.current + ) { + setValue(inputValue == null ? '' : inputValue.toString()); + } + }, [inputValue, previousValue, setValue]); + + return ( + <TextInput + {...otherProps} + name={name} + type="number" + value={value == null ? '' : value} + min={min} + max={max} + onChange={handleChange} + onBlur={handleBlur} + onFocus={handleFocus} + /> + ); +} + +export default NumberInput; diff --git a/frontend/src/Components/Form/OAuthInput.js b/frontend/src/Components/Form/OAuthInput.js deleted file mode 100644 index 4ecd625bc..000000000 --- a/frontend/src/Components/Form/OAuthInput.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import { kinds } from 'Helpers/Props'; - -function OAuthInput(props) { - const { - label, - authorizing, - error, - onPress - } = props; - - return ( - <div> - <SpinnerErrorButton - kind={kinds.PRIMARY} - isSpinning={authorizing} - error={error} - onPress={onPress} - > - {label} - </SpinnerErrorButton> - </div> - ); -} - -OAuthInput.propTypes = { - label: PropTypes.string.isRequired, - authorizing: PropTypes.bool.isRequired, - error: PropTypes.object, - onPress: PropTypes.func.isRequired -}; - -OAuthInput.defaultProps = { - label: 'Start OAuth' -}; - -export default OAuthInput; diff --git a/frontend/src/Components/Form/OAuthInput.tsx b/frontend/src/Components/Form/OAuthInput.tsx new file mode 100644 index 000000000..04d2a0caf --- /dev/null +++ b/frontend/src/Components/Form/OAuthInput.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import { kinds } from 'Helpers/Props'; +import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions'; +import { InputOnChange } from 'typings/inputs'; + +interface OAuthInputProps { + label?: string; + name: string; + provider: string; + providerData: object; + section: string; + onChange: InputOnChange<unknown>; +} + +function OAuthInput({ + label = 'Start OAuth', + name, + provider, + providerData, + section, + onChange, +}: OAuthInputProps) { + const dispatch = useDispatch(); + const { authorizing, error, result } = useSelector( + (state: AppState) => state.oAuth + ); + + const handlePress = useCallback(() => { + dispatch( + startOAuth({ + name, + provider, + providerData, + section, + }) + ); + }, [name, provider, providerData, section, dispatch]); + + useEffect(() => { + if (!result) { + return; + } + + Object.keys(result).forEach((key) => { + onChange({ name: key, value: result[key] }); + }); + }, [result, onChange]); + + useEffect(() => { + return () => { + dispatch(resetOAuth()); + }; + }, [dispatch]); + + return ( + <div> + <SpinnerErrorButton + kind={kinds.PRIMARY} + isSpinning={authorizing} + error={error} + onPress={handlePress} + > + {label} + </SpinnerErrorButton> + </div> + ); +} + +export default OAuthInput; diff --git a/frontend/src/Components/Form/OAuthInputConnector.js b/frontend/src/Components/Form/OAuthInputConnector.js deleted file mode 100644 index 1567c7e6c..000000000 --- a/frontend/src/Components/Form/OAuthInputConnector.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions'; -import OAuthInput from './OAuthInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.oAuth, - (oAuth) => { - return oAuth; - } - ); -} - -const mapDispatchToProps = { - startOAuth, - resetOAuth -}; - -class OAuthInputConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - const { - result, - onChange - } = this.props; - - if (!result || result === prevProps.result) { - return; - } - - Object.keys(result).forEach((key) => { - onChange({ name: key, value: result[key] }); - }); - } - - componentWillUnmount = () => { - this.props.resetOAuth(); - }; - - // - // Listeners - - onPress = () => { - const { - name, - provider, - providerData, - section - } = this.props; - - this.props.startOAuth({ - name, - provider, - providerData, - section - }); - }; - - // - // Render - - render() { - return ( - <OAuthInput - {...this.props} - onPress={this.onPress} - /> - ); - } -} - -OAuthInputConnector.propTypes = { - name: PropTypes.string.isRequired, - result: PropTypes.object, - provider: PropTypes.string.isRequired, - providerData: PropTypes.object.isRequired, - section: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - startOAuth: PropTypes.func.isRequired, - resetOAuth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(OAuthInputConnector); diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js deleted file mode 100644 index dbc4cfdb4..000000000 --- a/frontend/src/Components/Form/PasswordInput.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import TextInput from './TextInput'; - -// Prevent a user from copying (or cutting) the password from the input -function onCopy(e) { - e.preventDefault(); - e.nativeEvent.stopImmediatePropagation(); -} - -function PasswordInput(props) { - return ( - <TextInput - {...props} - type="password" - onCopy={onCopy} - /> - ); -} - -PasswordInput.propTypes = { - ...TextInput.props -}; - -export default PasswordInput; diff --git a/frontend/src/Components/Form/PasswordInput.tsx b/frontend/src/Components/Form/PasswordInput.tsx new file mode 100644 index 000000000..776c2b913 --- /dev/null +++ b/frontend/src/Components/Form/PasswordInput.tsx @@ -0,0 +1,14 @@ +import React, { SyntheticEvent } from 'react'; +import TextInput, { TextInputProps } from './TextInput'; + +// Prevent a user from copying (or cutting) the password from the input +function onCopy(e: SyntheticEvent) { + e.preventDefault(); + e.nativeEvent.stopImmediatePropagation(); +} + +function PasswordInput(props: TextInputProps<string>) { + return <TextInput {...props} type="password" onCopy={onCopy} />; +} + +export default PasswordInput; diff --git a/frontend/src/Components/Form/PathInput.js b/frontend/src/Components/Form/PathInput.js deleted file mode 100644 index 972d8f99f..000000000 --- a/frontend/src/Components/Form/PathInput.js +++ /dev/null @@ -1,195 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import AutoSuggestInput from './AutoSuggestInput'; -import FormInputButton from './FormInputButton'; -import styles from './PathInput.css'; - -class PathInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._node = document.getElementById('portal-root'); - - this.state = { - value: props.value, - isFileBrowserModalOpen: false - }; - } - - componentDidUpdate(prevProps) { - const { value } = this.props; - - if (prevProps.value !== value) { - this.setState({ value }); - } - } - - // - // Control - - getSuggestionValue({ path }) { - return path; - } - - renderSuggestion({ path }, { query }) { - const lastSeparatorIndex = query.lastIndexOf('\\') || query.lastIndexOf('/'); - - if (lastSeparatorIndex === -1) { - return ( - <span>{path}</span> - ); - } - - return ( - <span> - <span className={styles.pathMatch}> - {path.substr(0, lastSeparatorIndex)} - </span> - {path.substr(lastSeparatorIndex)} - </span> - ); - } - - // - // Listeners - - onInputChange = ({ value }) => { - this.setState({ value }); - }; - - onInputKeyDown = (event) => { - if (event.key === 'Tab') { - event.preventDefault(); - const path = this.props.paths[0]; - - if (path) { - this.props.onChange({ - name: this.props.name, - value: path.path - }); - - if (path.type !== 'file') { - this.props.onFetchPaths(path.path); - } - } - } - }; - - onInputBlur = () => { - this.props.onChange({ - name: this.props.name, - value: this.state.value - }); - - this.props.onClearPaths(); - }; - - onSuggestionsFetchRequested = ({ value }) => { - this.props.onFetchPaths(value); - }; - - onSuggestionsClearRequested = () => { - // Required because props aren't always rendered, but no-op - // because we don't want to reset the paths after a path is selected. - }; - - onSuggestionSelected = (event, { suggestionValue }) => { - this.props.onFetchPaths(suggestionValue); - }; - - onFileBrowserOpenPress = () => { - this.setState({ isFileBrowserModalOpen: true }); - }; - - onFileBrowserModalClose = () => { - this.setState({ isFileBrowserModalOpen: false }); - }; - - // - // Render - - render() { - const { - className, - name, - paths, - includeFiles, - hasFileBrowser, - onChange, - ...otherProps - } = this.props; - - const { - value, - isFileBrowserModalOpen - } = this.state; - - return ( - <div className={className}> - <AutoSuggestInput - {...otherProps} - className={hasFileBrowser ? styles.hasFileBrowser : undefined} - name={name} - value={value} - suggestions={paths} - getSuggestionValue={this.getSuggestionValue} - renderSuggestion={this.renderSuggestion} - onInputKeyDown={this.onInputKeyDown} - onInputBlur={this.onInputBlur} - onSuggestionSelected={this.onSuggestionSelected} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onChange={this.onInputChange} - /> - - { - hasFileBrowser && - <div> - <FormInputButton - className={styles.fileBrowserButton} - onPress={this.onFileBrowserOpenPress} - > - <Icon name={icons.FOLDER_OPEN} /> - </FormInputButton> - - <FileBrowserModal - isOpen={isFileBrowserModalOpen} - name={name} - value={value} - includeFiles={includeFiles} - onChange={onChange} - onModalClose={this.onFileBrowserModalClose} - /> - </div> - } - </div> - ); - } -} - -PathInput.propTypes = { - className: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.string, - paths: PropTypes.array.isRequired, - includeFiles: PropTypes.bool.isRequired, - hasFileBrowser: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onFetchPaths: PropTypes.func.isRequired, - onClearPaths: PropTypes.func.isRequired -}; - -PathInput.defaultProps = { - className: styles.inputWrapper, - value: '', - hasFileBrowser: true -}; - -export default PathInput; diff --git a/frontend/src/Components/Form/PathInput.tsx b/frontend/src/Components/Form/PathInput.tsx new file mode 100644 index 000000000..f353f1be4 --- /dev/null +++ b/frontend/src/Components/Form/PathInput.tsx @@ -0,0 +1,252 @@ +import React, { + KeyboardEvent, + SyntheticEvent, + useCallback, + useEffect, + useState, +} from 'react'; +import { + ChangeEvent, + SuggestionsFetchRequestedParams, +} from 'react-autosuggest'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import { Path } from 'App/State/PathsAppState'; +import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; +import Icon from 'Components/Icon'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; +import { InputChanged } from 'typings/inputs'; +import AutoSuggestInput from './AutoSuggestInput'; +import FormInputButton from './FormInputButton'; +import styles from './PathInput.css'; + +interface PathInputProps { + className?: string; + name: string; + value?: string; + placeholder?: string; + includeFiles: boolean; + hasFileBrowser?: boolean; + onChange: (change: InputChanged<string>) => void; +} + +interface PathInputInternalProps extends PathInputProps { + paths: Path[]; + onFetchPaths: (path: string) => void; + onClearPaths: () => void; +} + +function handleSuggestionsClearRequested() { + // Required because props aren't always rendered, but no-op + // because we don't want to reset the paths after a path is selected. +} + +function createPathsSelector() { + return createSelector( + (state: AppState) => state.paths, + (paths) => { + const { currentPath, directories, files } = paths; + + const filteredPaths = [...directories, ...files].filter(({ path }) => { + return path.toLowerCase().startsWith(currentPath.toLowerCase()); + }); + + return filteredPaths; + } + ); +} + +function PathInput(props: PathInputProps) { + const { includeFiles } = props; + + const dispatch = useDispatch(); + + const paths = useSelector(createPathsSelector()); + + const handleFetchPaths = useCallback( + (path: string) => { + dispatch(fetchPaths({ path, includeFiles })); + }, + [includeFiles, dispatch] + ); + + const handleClearPaths = useCallback(() => { + dispatch(clearPaths); + }, [dispatch]); + + return ( + <PathInputInternal + {...props} + paths={paths} + onFetchPaths={handleFetchPaths} + onClearPaths={handleClearPaths} + /> + ); +} + +export default PathInput; + +export function PathInputInternal(props: PathInputInternalProps) { + const { + className = styles.inputWrapper, + name, + value: inputValue = '', + paths, + includeFiles, + hasFileBrowser = true, + onChange, + onFetchPaths, + onClearPaths, + ...otherProps + } = props; + + const [value, setValue] = useState(inputValue); + const [isFileBrowserModalOpen, setIsFileBrowserModalOpen] = useState(false); + const previousInputValue = usePrevious(inputValue); + const dispatch = useDispatch(); + + const handleFetchPaths = useCallback( + (path: string) => { + dispatch(fetchPaths({ path, includeFiles })); + }, + [includeFiles, dispatch] + ); + + const handleInputChange = useCallback( + (_event: SyntheticEvent, { newValue }: ChangeEvent) => { + setValue(newValue); + }, + [setValue] + ); + + const handleInputKeyDown = useCallback( + (event: KeyboardEvent<HTMLElement>) => { + if (event.key === 'Tab') { + event.preventDefault(); + const path = paths[0]; + + if (path) { + onChange({ + name, + value: path.path, + }); + + if (path.type !== 'file') { + handleFetchPaths(path.path); + } + } + } + }, + [name, paths, handleFetchPaths, onChange] + ); + const handleInputBlur = useCallback(() => { + onChange({ + name, + value, + }); + + onClearPaths(); + }, [name, value, onClearPaths, onChange]); + + const handleSuggestionSelected = useCallback( + (_event: SyntheticEvent, { suggestion }: { suggestion: Path }) => { + handleFetchPaths(suggestion.path); + }, + [handleFetchPaths] + ); + + const handleSuggestionsFetchRequested = useCallback( + ({ value: newValue }: SuggestionsFetchRequestedParams) => { + handleFetchPaths(newValue); + }, + [handleFetchPaths] + ); + + const handleFileBrowserOpenPress = useCallback(() => { + setIsFileBrowserModalOpen(true); + }, [setIsFileBrowserModalOpen]); + + const handleFileBrowserModalClose = useCallback(() => { + setIsFileBrowserModalOpen(false); + }, [setIsFileBrowserModalOpen]); + + const handleChange = useCallback( + (change: InputChanged<Path>) => { + onChange({ name, value: change.value.path }); + }, + [name, onChange] + ); + + const getSuggestionValue = useCallback(({ path }: Path) => path, []); + + const renderSuggestion = useCallback( + ({ path }: Path, { query }: { query: string }) => { + const lastSeparatorIndex = + query.lastIndexOf('\\') || query.lastIndexOf('/'); + + if (lastSeparatorIndex === -1) { + return <span>{path}</span>; + } + + return ( + <span> + <span className={styles.pathMatch}> + {path.substring(0, lastSeparatorIndex)} + </span> + {path.substring(lastSeparatorIndex)} + </span> + ); + }, + [] + ); + + useEffect(() => { + if (inputValue !== previousInputValue) { + setValue(inputValue); + } + }, [inputValue, previousInputValue, setValue]); + + return ( + <div className={className}> + <AutoSuggestInput + {...otherProps} + className={hasFileBrowser ? styles.hasFileBrowser : undefined} + name={name} + value={value} + suggestions={paths} + getSuggestionValue={getSuggestionValue} + renderSuggestion={renderSuggestion} + onInputKeyDown={handleInputKeyDown} + onInputChange={handleInputChange} + onInputBlur={handleInputBlur} + onSuggestionSelected={handleSuggestionSelected} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={handleSuggestionsClearRequested} + onChange={handleChange} + /> + + {hasFileBrowser ? ( + <div> + <FormInputButton + className={styles.fileBrowserButton} + onPress={handleFileBrowserOpenPress} + > + <Icon name={icons.FOLDER_OPEN} /> + </FormInputButton> + + <FileBrowserModal + isOpen={isFileBrowserModalOpen} + name={name} + value={value} + includeFiles={includeFiles} + onChange={onChange} + onModalClose={handleFileBrowserModalClose} + /> + </div> + ) : null} + </div> + ); +} diff --git a/frontend/src/Components/Form/PathInputConnector.js b/frontend/src/Components/Form/PathInputConnector.js deleted file mode 100644 index 563437f9a..000000000 --- a/frontend/src/Components/Form/PathInputConnector.js +++ /dev/null @@ -1,81 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; -import PathInput from './PathInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.paths, - (paths) => { - const { - currentPath, - directories, - files - } = paths; - - const filteredPaths = _.filter([...directories, ...files], ({ path }) => { - return path.toLowerCase().startsWith(currentPath.toLowerCase()); - }); - - return { - paths: filteredPaths - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchPaths: fetchPaths, - dispatchClearPaths: clearPaths -}; - -class PathInputConnector extends Component { - - // - // Listeners - - onFetchPaths = (path) => { - const { - includeFiles, - dispatchFetchPaths - } = this.props; - - dispatchFetchPaths({ - path, - includeFiles - }); - }; - - onClearPaths = () => { - this.props.dispatchClearPaths(); - }; - - // - // Render - - render() { - return ( - <PathInput - onFetchPaths={this.onFetchPaths} - onClearPaths={this.onClearPaths} - {...this.props} - /> - ); - } -} - -PathInputConnector.propTypes = { - ...PathInput.props, - includeFiles: PropTypes.bool.isRequired, - dispatchFetchPaths: PropTypes.func.isRequired, - dispatchClearPaths: PropTypes.func.isRequired -}; - -PathInputConnector.defaultProps = { - includeFiles: false -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector); diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js deleted file mode 100644 index 055180f12..000000000 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), - (state, { includeNoChange }) => includeNoChange, - (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, - (state, { includeMixed }) => includeMixed, - (qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => { - const values = _.map(qualityProfiles.items, (qualityProfile) => { - return { - key: qualityProfile.id, - value: qualityProfile.name - }; - }); - - if (includeNoChange) { - values.unshift({ - key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: includeNoChangeDisabled - }); - } - - if (includeMixed) { - values.unshift({ - key: 'mixed', - get value() { - return `(${translate('Mixed')})`; - }, - isDisabled: true - }); - } - - return { - values - }; - } - ); -} - -class QualityProfileSelectInputConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - name, - value, - values - } = this.props; - - if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) { - const firstValue = values.find((option) => !isNaN(parseInt(option.key))); - - if (firstValue) { - this.onChange({ name, value: firstValue.key }); - } - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) }); - }; - - // - // Render - - render() { - return ( - <EnhancedSelectInput - {...this.props} - onChange={this.onChange} - /> - ); - } -} - -QualityProfileSelectInputConnector.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - values: PropTypes.arrayOf(PropTypes.object).isRequired, - includeNoChange: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired -}; - -QualityProfileSelectInputConnector.defaultProps = { - includeNoChange: false -}; - -export default connect(createMapStateToProps)(QualityProfileSelectInputConnector); diff --git a/frontend/src/Components/Form/RootFolderSelectInput.js b/frontend/src/Components/Form/RootFolderSelectInput.js deleted file mode 100644 index 1d76ad946..000000000 --- a/frontend/src/Components/Form/RootFolderSelectInput.js +++ /dev/null @@ -1,109 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; -import EnhancedSelectInput from './EnhancedSelectInput'; -import RootFolderSelectInputOption from './RootFolderSelectInputOption'; -import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue'; - -class RootFolderSelectInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddNewRootFolderModalOpen: false, - newRootFolderPath: '' - }; - } - - componentDidUpdate(prevProps) { - const { - name, - isSaving, - saveError, - onChange - } = this.props; - - const newRootFolderPath = this.state.newRootFolderPath; - - if ( - prevProps.isSaving && - !isSaving && - !saveError && - newRootFolderPath - ) { - onChange({ name, value: newRootFolderPath }); - this.setState({ newRootFolderPath: '' }); - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - if (value === 'addNew') { - this.setState({ isAddNewRootFolderModalOpen: true }); - } else { - this.props.onChange({ name, value }); - } - }; - - onNewRootFolderSelect = ({ value }) => { - this.setState({ newRootFolderPath: value }, () => { - this.props.onNewRootFolderSelect(value); - }); - }; - - onAddRootFolderModalClose = () => { - this.setState({ isAddNewRootFolderModalOpen: false }); - }; - - // - // Render - - render() { - const { - includeNoChange, - onNewRootFolderSelect, - ...otherProps - } = this.props; - - return ( - <div> - <EnhancedSelectInput - {...otherProps} - selectedValueComponent={RootFolderSelectInputSelectedValue} - optionComponent={RootFolderSelectInputOption} - onChange={this.onChange} - /> - - <FileBrowserModal - isOpen={this.state.isAddNewRootFolderModalOpen} - name="rootFolderPath" - value="" - onChange={this.onNewRootFolderSelect} - onModalClose={this.onAddRootFolderModalClose} - /> - </div> - ); - } -} - -RootFolderSelectInput.propTypes = { - name: PropTypes.string.isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - includeNoChange: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onNewRootFolderSelect: PropTypes.func.isRequired -}; - -RootFolderSelectInput.defaultProps = { - includeNoChange: false -}; - -export default RootFolderSelectInput; diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js deleted file mode 100644 index 43581835f..000000000 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ /dev/null @@ -1,175 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { addRootFolder } from 'Store/Actions/rootFolderActions'; -import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; -import translate from 'Utilities/String/translate'; -import RootFolderSelectInput from './RootFolderSelectInput'; - -const ADD_NEW_KEY = 'addNew'; - -function createMapStateToProps() { - return createSelector( - createRootFoldersSelector(), - (state, { value }) => value, - (state, { includeMissingValue }) => includeMissingValue, - (state, { includeNoChange }) => includeNoChange, - (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, - (rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => { - const values = rootFolders.items.map((rootFolder) => { - return { - key: rootFolder.path, - value: rootFolder.path, - freeSpace: rootFolder.freeSpace, - isMissing: false - }; - }); - - if (includeNoChange) { - values.unshift({ - key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: includeNoChangeDisabled, - isMissing: false - }); - } - - if (!values.length) { - values.push({ - key: '', - value: '', - isDisabled: true, - isHidden: true - }); - } - - if (includeMissingValue && !values.find((v) => v.key === value)) { - values.push({ - key: value, - value, - isMissing: true, - isDisabled: true - }); - } - - values.push({ - key: ADD_NEW_KEY, - value: translate('AddANewPath') - }); - - return { - values, - isSaving: rootFolders.isSaving, - saveError: rootFolders.saveError - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchAddRootFolder(path) { - dispatch(addRootFolder({ path })); - } - }; -} - -class RootFolderSelectInputConnector extends Component { - - // - // Lifecycle - - componentWillMount() { - const { - value, - values, - onChange - } = this.props; - - if (value == null && values[0].key === '') { - onChange({ name, value: '' }); - } - } - - componentDidMount() { - const { - name, - value, - values, - onChange - } = this.props; - - if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) { - const defaultValue = values[0]; - - if (defaultValue.key === ADD_NEW_KEY) { - onChange({ name, value: '' }); - } else { - onChange({ name, value: defaultValue.key }); - } - } - } - - componentDidUpdate(prevProps) { - const { - name, - value, - values, - onChange - } = this.props; - - if (prevProps.values === values) { - return; - } - - if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) { - const defaultValue = values[0]; - - if (defaultValue.key !== ADD_NEW_KEY) { - onChange({ name, value: defaultValue.key }); - } - } - } - - // - // Listeners - - onNewRootFolderSelect = (path) => { - this.props.dispatchAddRootFolder(path); - }; - - // - // Render - - render() { - const { - dispatchAddRootFolder, - ...otherProps - } = this.props; - - return ( - <RootFolderSelectInput - {...otherProps} - onNewRootFolderSelect={this.onNewRootFolderSelect} - /> - ); - } -} - -RootFolderSelectInputConnector.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - includeNoChange: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - dispatchAddRootFolder: PropTypes.func.isRequired -}; - -RootFolderSelectInputConnector.defaultProps = { - includeNoChange: false -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(RootFolderSelectInputConnector); diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js deleted file mode 100644 index daac82f34..000000000 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ /dev/null @@ -1,77 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInputOption from './EnhancedSelectInputOption'; -import styles from './RootFolderSelectInputOption.css'; - -function RootFolderSelectInputOption(props) { - const { - id, - value, - freeSpace, - isMissing, - seriesFolder, - isMobile, - isWindows, - ...otherProps - } = props; - - const slashCharacter = isWindows ? '\\' : '/'; - - return ( - <EnhancedSelectInputOption - id={id} - isMobile={isMobile} - {...otherProps} - > - <div className={classNames( - styles.optionText, - isMobile && styles.isMobile - )} - > - <div className={styles.value}> - {value} - - { - seriesFolder && id !== 'addNew' ? - <div className={styles.seriesFolder}> - {slashCharacter} - {seriesFolder} - </div> : - null - } - </div> - - { - freeSpace == null ? - null : - <div className={styles.freeSpace}> - {translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })} - </div> - } - - { - isMissing ? - <div className={styles.isMissing}> - {translate('Missing')} - </div> : - null - } - </div> - </EnhancedSelectInputOption> - ); -} - -RootFolderSelectInputOption.propTypes = { - id: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - freeSpace: PropTypes.number, - isMissing: PropTypes.bool, - seriesFolder: PropTypes.string, - isMobile: PropTypes.bool.isRequired, - isWindows: PropTypes.bool -}; - -export default RootFolderSelectInputOption; diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js deleted file mode 100644 index 1c3a4fc9d..000000000 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; -import styles from './RootFolderSelectInputSelectedValue.css'; - -function RootFolderSelectInputSelectedValue(props) { - const { - value, - freeSpace, - seriesFolder, - includeFreeSpace, - isWindows, - ...otherProps - } = props; - - const slashCharacter = isWindows ? '\\' : '/'; - - return ( - <EnhancedSelectInputSelectedValue - className={styles.selectedValue} - {...otherProps} - > - <div className={styles.pathContainer}> - <div className={styles.path}> - {value} - </div> - - { - seriesFolder ? - <div className={styles.seriesFolder}> - {slashCharacter} - {seriesFolder} - </div> : - null - } - </div> - - { - freeSpace != null && includeFreeSpace && - <div className={styles.freeSpace}> - {translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })} - </div> - } - </EnhancedSelectInputSelectedValue> - ); -} - -RootFolderSelectInputSelectedValue.propTypes = { - value: PropTypes.string, - freeSpace: PropTypes.number, - seriesFolder: PropTypes.string, - isWindows: PropTypes.bool, - includeFreeSpace: PropTypes.bool.isRequired -}; - -RootFolderSelectInputSelectedValue.defaultProps = { - includeFreeSpace: true -}; - -export default RootFolderSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/Select/DownloadClientSelectInput.tsx b/frontend/src/Components/Form/Select/DownloadClientSelectInput.tsx new file mode 100644 index 000000000..4ed3e0952 --- /dev/null +++ b/frontend/src/Components/Form/Select/DownloadClientSelectInput.tsx @@ -0,0 +1,88 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import { fetchDownloadClients } from 'Store/Actions/settingsActions'; +import { Protocol } from 'typings/DownloadClient'; +import { EnhancedSelectInputChanged } from 'typings/inputs'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +function createDownloadClientsSelector( + includeAny: boolean, + protocol: Protocol +) { + return createSelector( + (state: AppState) => state.settings.downloadClients, + (downloadClients) => { + const { isFetching, isPopulated, error, items } = downloadClients; + + const filteredItems = items.filter((item) => item.protocol === protocol); + + const values = filteredItems + .sort(sortByProp('name')) + .map((downloadClient) => { + return { + key: downloadClient.id, + value: downloadClient.name, + hint: `(${downloadClient.id})`, + }; + }); + + if (includeAny) { + values.unshift({ + key: 0, + value: `(${translate('Any')})`, + hint: '', + }); + } + + return { + isFetching, + isPopulated, + error, + values, + }; + } + ); +} + +interface DownloadClientSelectInputProps + extends EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number> { + name: string; + value: number; + includeAny?: boolean; + protocol?: Protocol; + onChange: (change: EnhancedSelectInputChanged<number>) => void; +} + +function DownloadClientSelectInput({ + includeAny = false, + protocol = 'torrent', + ...otherProps +}: DownloadClientSelectInputProps) { + const dispatch = useDispatch(); + const { isFetching, isPopulated, values } = useSelector( + createDownloadClientsSelector(includeAny, protocol) + ); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchDownloadClients()); + } + }, [isPopulated, dispatch]); + + return ( + <EnhancedSelectInput + {...otherProps} + isFetching={isFetching} + values={values} + /> + ); +} + +export default DownloadClientSelectInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/Select/EnhancedSelectInput.css similarity index 94% rename from frontend/src/Components/Form/EnhancedSelectInput.css rename to frontend/src/Components/Form/Select/EnhancedSelectInput.css index defefb18e..735d63573 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.css @@ -73,6 +73,12 @@ padding: 10px 0; } +.optionsInnerModalBody { + composes: innerModalBody from '~Components/Modal/ModalBody.css'; + + padding: 0; +} + .optionsModalScroller { composes: scroller from '~Components/Scroller/Scroller.css'; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css.d.ts b/frontend/src/Components/Form/Select/EnhancedSelectInput.css.d.ts similarity index 94% rename from frontend/src/Components/Form/EnhancedSelectInput.css.d.ts rename to frontend/src/Components/Form/Select/EnhancedSelectInput.css.d.ts index edcf0079b..98167a9b5 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css.d.ts +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.css.d.ts @@ -14,6 +14,7 @@ interface CssExports { 'mobileCloseButtonContainer': string; 'options': string; 'optionsContainer': string; + 'optionsInnerModalBody': string; 'optionsModal': string; 'optionsModalBody': string; 'optionsModalScroller': string; diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx new file mode 100644 index 000000000..b47f8da3d --- /dev/null +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx @@ -0,0 +1,622 @@ +import classNames from 'classnames'; +import React, { + ElementType, + KeyboardEvent, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Manager, Popper, Reference } from 'react-popper'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Measure from 'Components/Measure'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import Portal from 'Components/Portal'; +import Scroller from 'Components/Scroller/Scroller'; +import { icons, scrollDirections, sizes } from 'Helpers/Props'; +import ArrayElement from 'typings/Helpers/ArrayElement'; +import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; +import { isMobile as isMobileUtil } from 'Utilities/browser'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; +import getUniqueElementId from 'Utilities/getUniqueElementId'; +import TextInput from '../TextInput'; +import HintedSelectInputOption from './HintedSelectInputOption'; +import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; +import styles from './EnhancedSelectInput.css'; + +function isArrowKey(keyCode: number) { + return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; +} + +function getSelectedOption<T extends EnhancedSelectInputValue<V>, V>( + selectedIndex: number, + values: T[] +) { + return values[selectedIndex]; +} + +function findIndex<T extends EnhancedSelectInputValue<V>, V>( + startingIndex: number, + direction: 1 | -1, + values: T[] +) { + let indexToTest = startingIndex + direction; + + while (indexToTest !== startingIndex) { + if (indexToTest < 0) { + indexToTest = values.length - 1; + } else if (indexToTest >= values.length) { + indexToTest = 0; + } + + if (getSelectedOption(indexToTest, values).isDisabled) { + indexToTest = indexToTest + direction; + } else { + return indexToTest; + } + } + + return null; +} + +function previousIndex<T extends EnhancedSelectInputValue<V>, V>( + selectedIndex: number, + values: T[] +) { + return findIndex(selectedIndex, -1, values); +} + +function nextIndex<T extends EnhancedSelectInputValue<V>, V>( + selectedIndex: number, + values: T[] +) { + return findIndex(selectedIndex, 1, values); +} + +function getSelectedIndex<T extends EnhancedSelectInputValue<V>, V>( + value: V, + values: T[] +) { + if (Array.isArray(value)) { + return values.findIndex((v) => { + return v.key === value[0]; + }); + } + + return values.findIndex((v) => { + return v.key === value; + }); +} + +function isSelectedItem<T extends EnhancedSelectInputValue<V>, V>( + index: number, + value: V, + values: T[] +) { + if (Array.isArray(value)) { + return value.includes(values[index].key); + } + + return values[index].key === value; +} + +export interface EnhancedSelectInputValue<V> { + key: ArrayElement<V>; + value: string; + hint?: ReactNode; + isDisabled?: boolean; + isHidden?: boolean; + parentKey?: V; + additionalProperties?: object; +} + +export interface EnhancedSelectInputProps< + T extends EnhancedSelectInputValue<V>, + V +> { + className?: string; + disabledClassName?: string; + name: string; + value: V; + values: T[]; + isDisabled?: boolean; + isFetching?: boolean; + isEditable?: boolean; + hasError?: boolean; + hasWarning?: boolean; + valueOptions?: object; + selectedValueOptions?: object; + selectedValueComponent?: string | ElementType; + optionComponent?: ElementType; + onOpen?: () => void; + onChange: (change: EnhancedSelectInputChanged<V>) => void; +} + +function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( + props: EnhancedSelectInputProps<T, V> +) { + const { + className = styles.enhancedSelect, + disabledClassName = styles.isDisabled, + name, + value, + values, + isDisabled = false, + isEditable, + isFetching, + hasError, + hasWarning, + valueOptions, + selectedValueOptions, + selectedValueComponent: + SelectedValueComponent = HintedSelectInputSelectedValue, + optionComponent: OptionComponent = HintedSelectInputOption, + onChange, + onOpen, + } = props; + + const updater = useRef<(() => void) | null>(null); + const buttonId = useMemo(() => getUniqueElementId(), []); + const optionsId = useMemo(() => getUniqueElementId(), []); + const [selectedIndex, setSelectedIndex] = useState( + getSelectedIndex(value, values) + ); + const [width, setWidth] = useState(0); + const [isOpen, setIsOpen] = useState(false); + const isMobile = useMemo(() => isMobileUtil(), []); + + const isMultiSelect = Array.isArray(value); + const selectedOption = getSelectedOption(selectedIndex, values); + + const selectedValue = useMemo(() => { + if (values.length) { + return value; + } + + if (isMultiSelect) { + return []; + } else if (typeof value === 'number') { + return 0; + } + + return ''; + }, [value, values, isMultiSelect]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleComputeMaxHeight = useCallback((data: any) => { + const { top, bottom } = data.offsets.reference; + const windowHeight = window.innerHeight; + + if (/^botton/.test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } + + return data; + }, []); + + const handleWindowClick = useCallback( + (event: MouseEvent) => { + const button = document.getElementById(buttonId); + const options = document.getElementById(optionsId); + const eventTarget = event.target as HTMLElement; + + if (!button || !eventTarget.isConnected || isMobile) { + return; + } + + if ( + !button.contains(eventTarget) && + options && + !options.contains(eventTarget) && + isOpen + ) { + setIsOpen(false); + window.removeEventListener('click', handleWindowClick); + } + }, + [isMobile, isOpen, buttonId, optionsId, setIsOpen] + ); + + const addListener = useCallback(() => { + window.addEventListener('click', handleWindowClick); + }, [handleWindowClick]); + + const removeListener = useCallback(() => { + window.removeEventListener('click', handleWindowClick); + }, [handleWindowClick]); + + const handlePress = useCallback(() => { + if (isOpen) { + removeListener(); + } else { + addListener(); + } + + if (!isOpen && onOpen) { + onOpen(); + } + + setIsOpen(!isOpen); + }, [isOpen, setIsOpen, addListener, removeListener, onOpen]); + + const handleSelect = useCallback( + (newValue: ArrayElement<V>) => { + const additionalProperties = values.find( + (v) => v.key === newValue + )?.additionalProperties; + + if (Array.isArray(value)) { + const index = value.indexOf(newValue); + + if (index === -1) { + const arrayValue = values + .map((v) => v.key) + .filter((v) => v === newValue || value.includes(v)); + + onChange({ + name, + value: arrayValue as V, + additionalProperties, + }); + } else { + const arrayValue = [...value]; + arrayValue.splice(index, 1); + + onChange({ + name, + value: arrayValue as V, + additionalProperties, + }); + } + } else { + setIsOpen(false); + + onChange({ + name, + value: newValue as V, + additionalProperties, + }); + } + }, + [name, value, values, onChange, setIsOpen] + ); + + const handleBlur = useCallback(() => { + if (!isEditable) { + // Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox) + const origIndex = getSelectedIndex(value, values); + + if (origIndex !== selectedIndex) { + setSelectedIndex(origIndex); + } + } + }, [value, values, isEditable, selectedIndex, setSelectedIndex]); + + const handleFocus = useCallback(() => { + if (isOpen) { + removeListener(); + setIsOpen(false); + } + }, [isOpen, setIsOpen, removeListener]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent<HTMLButtonElement>) => { + const keyCode = event.keyCode; + let nextIsOpen: boolean | null = null; + let nextSelectedIndex: number | null = null; + + if (!isOpen) { + if (isArrowKey(keyCode)) { + event.preventDefault(); + nextIsOpen = true; + } + + if ( + selectedIndex == null || + selectedIndex === -1 || + getSelectedOption(selectedIndex, values).isDisabled + ) { + if (keyCode === keyCodes.UP_ARROW) { + nextSelectedIndex = previousIndex(0, values); + } else if (keyCode === keyCodes.DOWN_ARROW) { + nextSelectedIndex = nextIndex(values.length - 1, values); + } + } + + if (nextIsOpen !== null) { + setIsOpen(nextIsOpen); + } + + if (nextSelectedIndex !== null) { + setSelectedIndex(nextSelectedIndex); + } + return; + } + + if (keyCode === keyCodes.UP_ARROW) { + event.preventDefault(); + nextSelectedIndex = previousIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.DOWN_ARROW) { + event.preventDefault(); + nextSelectedIndex = nextIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.ENTER) { + event.preventDefault(); + nextIsOpen = false; + handleSelect(values[selectedIndex].key); + } + + if (keyCode === keyCodes.TAB) { + nextIsOpen = false; + handleSelect(values[selectedIndex].key); + } + + if (keyCode === keyCodes.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + nextIsOpen = false; + nextSelectedIndex = getSelectedIndex(value, values); + } + + if (nextIsOpen !== null) { + setIsOpen(nextIsOpen); + } + + if (nextSelectedIndex !== null) { + setSelectedIndex(nextSelectedIndex); + } + }, + [ + value, + isOpen, + selectedIndex, + values, + setIsOpen, + setSelectedIndex, + handleSelect, + ] + ); + + const handleMeasure = useCallback( + ({ width: newWidth }: { width: number }) => { + setWidth(newWidth); + }, + [setWidth] + ); + + const handleOptionsModalClose = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const handleEditChange = useCallback( + (change: InputChanged<string>) => { + onChange(change as EnhancedSelectInputChanged<V>); + }, + [onChange] + ); + + useEffect(() => { + if (updater.current) { + updater.current(); + } + }); + + return ( + <div> + <Manager> + <Reference> + {({ ref }) => ( + <div ref={ref} id={buttonId}> + <Measure whitelist={['width']} onMeasure={handleMeasure}> + {isEditable && typeof value === 'string' ? ( + <div className={styles.editableContainer}> + <TextInput + className={className} + name={name} + value={value} + readOnly={isDisabled} + hasError={hasError} + hasWarning={hasWarning} + onFocus={handleFocus} + onBlur={handleBlur} + onChange={handleEditChange} + /> + <Link + className={classNames( + styles.dropdownArrowContainerEditable, + isDisabled + ? styles.dropdownArrowContainerDisabled + : styles.dropdownArrowContainer + )} + onPress={handlePress} + > + {isFetching ? ( + <LoadingIndicator + className={styles.loading} + size={20} + /> + ) : null} + + {isFetching ? null : <Icon name={icons.CARET_DOWN} />} + </Link> + </div> + ) : ( + <Link + className={classNames( + className, + hasError && styles.hasError, + hasWarning && styles.hasWarning, + isDisabled && disabledClassName + )} + isDisabled={isDisabled} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + onPress={handlePress} + > + <SelectedValueComponent + values={values} + {...selectedValueOptions} + selectedValue={selectedValue} + isDisabled={isDisabled} + isMultiSelect={isMultiSelect} + > + {selectedOption ? selectedOption.value : selectedValue} + </SelectedValueComponent> + + <div + className={ + isDisabled + ? styles.dropdownArrowContainerDisabled + : styles.dropdownArrowContainer + } + > + {isFetching ? ( + <LoadingIndicator + className={styles.loading} + size={20} + /> + ) : null} + + {isFetching ? null : <Icon name={icons.CARET_DOWN} />} + </div> + </Link> + )} + </Measure> + </div> + )} + </Reference> + <Portal> + <Popper + placement="bottom-start" + modifiers={{ + computeMaxHeight: { + order: 851, + enabled: true, + fn: handleComputeMaxHeight, + }, + }} + > + {({ ref, style, scheduleUpdate }) => { + updater.current = scheduleUpdate; + + return ( + <div + ref={ref} + id={optionsId} + className={styles.optionsContainer} + style={{ + ...style, + minWidth: width, + }} + > + {isOpen && !isMobile ? ( + <Scroller + className={styles.options} + style={{ + maxHeight: style.maxHeight, + }} + > + {values.map((v, index) => { + const hasParent = v.parentKey !== undefined; + const depth = hasParent ? 1 : 0; + const parentSelected = + v.parentKey !== undefined && + Array.isArray(value) && + value.includes(v.parentKey); + + const { key, ...other } = v; + + return ( + <OptionComponent + key={v.key} + id={v.key} + depth={depth} + isSelected={isSelectedItem(index, value, values)} + isDisabled={parentSelected} + isMultiSelect={isMultiSelect} + {...valueOptions} + {...other} + isMobile={false} + onSelect={handleSelect} + > + {v.value} + </OptionComponent> + ); + })} + </Scroller> + ) : null} + </div> + ); + }} + </Popper> + </Portal> + </Manager> + + {isMobile ? ( + <Modal + className={styles.optionsModal} + size={sizes.EXTRA_SMALL} + isOpen={isOpen} + onModalClose={handleOptionsModalClose} + > + <ModalBody + className={styles.optionsModalBody} + innerClassName={styles.optionsInnerModalBody} + scrollDirection={scrollDirections.NONE} + > + <Scroller className={styles.optionsModalScroller}> + <div className={styles.mobileCloseButtonContainer}> + <Link + className={styles.mobileCloseButton} + onPress={handleOptionsModalClose} + > + <Icon name={icons.CLOSE} size={18} /> + </Link> + </div> + + {values.map((v, index) => { + const hasParent = v.parentKey !== undefined; + const depth = hasParent ? 1 : 0; + const parentSelected = + v.parentKey !== undefined && + isMultiSelect && + value.includes(v.parentKey); + + const { key, ...other } = v; + + return ( + <OptionComponent + key={key} + id={key} + depth={depth} + isSelected={isSelectedItem(index, value, values)} + isMultiSelect={isMultiSelect} + isDisabled={parentSelected} + {...valueOptions} + {...other} + isMobile={true} + onSelect={handleSelect} + > + {v.value} + </OptionComponent> + ); + })} + </Scroller> + </ModalBody> + </Modal> + ) : null} + </div> + ); +} + +export default EnhancedSelectInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.css b/frontend/src/Components/Form/Select/EnhancedSelectInputOption.css similarity index 87% rename from frontend/src/Components/Form/EnhancedSelectInputOption.css rename to frontend/src/Components/Form/Select/EnhancedSelectInputOption.css index d7f0e861b..bfdaa9036 100644 --- a/frontend/src/Components/Form/EnhancedSelectInputOption.css +++ b/frontend/src/Components/Form/Select/EnhancedSelectInputOption.css @@ -16,13 +16,13 @@ } .optionCheck { - composes: container from '~./CheckInput.css'; + composes: container from '~Components/Form/CheckInput.css'; flex: 0 0 0; } .optionCheckInput { - composes: input from '~./CheckInput.css'; + composes: input from '~Components/Form/CheckInput.css'; margin-top: 0; } diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.css.d.ts b/frontend/src/Components/Form/Select/EnhancedSelectInputOption.css.d.ts similarity index 100% rename from frontend/src/Components/Form/EnhancedSelectInputOption.css.d.ts rename to frontend/src/Components/Form/Select/EnhancedSelectInputOption.css.d.ts diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInputOption.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInputOption.tsx new file mode 100644 index 000000000..c866a5060 --- /dev/null +++ b/frontend/src/Components/Form/Select/EnhancedSelectInputOption.tsx @@ -0,0 +1,84 @@ +import classNames from 'classnames'; +import React, { SyntheticEvent, useCallback } from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import CheckInput from '../CheckInput'; +import styles from './EnhancedSelectInputOption.css'; + +function handleCheckPress() { + // CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation. +} + +export interface EnhancedSelectInputOptionProps { + className?: string; + id: string | number; + depth?: number; + isSelected: boolean; + isDisabled?: boolean; + isHidden?: boolean; + isMultiSelect?: boolean; + isMobile: boolean; + children: React.ReactNode; + onSelect: (...args: unknown[]) => unknown; +} + +function EnhancedSelectInputOption({ + className = styles.option, + id, + depth = 0, + isSelected, + isDisabled = false, + isHidden = false, + isMultiSelect = false, + isMobile, + children, + onSelect, +}: EnhancedSelectInputOptionProps) { + const handlePress = useCallback( + (event: SyntheticEvent) => { + event.preventDefault(); + + onSelect(id); + }, + [id, onSelect] + ); + + return ( + <Link + className={classNames( + className, + isSelected && !isMultiSelect && styles.isSelected, + isDisabled && !isMultiSelect && styles.isDisabled, + isHidden && styles.isHidden, + isMobile && styles.isMobile + )} + component="div" + isDisabled={isDisabled} + onPress={handlePress} + > + {depth !== 0 && <div style={{ width: `${depth * 20}px` }} />} + + {isMultiSelect && ( + <CheckInput + className={styles.optionCheckInput} + containerClassName={styles.optionCheck} + name={`select-${id}`} + value={isSelected} + isDisabled={isDisabled} + onChange={handleCheckPress} + /> + )} + + {children} + + {isMobile && ( + <div className={styles.iconContainer}> + <Icon name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE} /> + </div> + )} + </Link> + ); +} + +export default EnhancedSelectInputOption; diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css b/frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.css similarity index 100% rename from frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css rename to frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.css diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css.d.ts b/frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.css.d.ts similarity index 100% rename from frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css.d.ts rename to frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.css.d.ts diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.tsx new file mode 100644 index 000000000..88afdb18a --- /dev/null +++ b/frontend/src/Components/Form/Select/EnhancedSelectInputSelectedValue.tsx @@ -0,0 +1,23 @@ +import classNames from 'classnames'; +import React, { ReactNode } from 'react'; +import styles from './EnhancedSelectInputSelectedValue.css'; + +interface EnhancedSelectInputSelectedValueProps { + className?: string; + children: ReactNode; + isDisabled?: boolean; +} + +function EnhancedSelectInputSelectedValue({ + className = styles.selectedValue, + children, + isDisabled = false, +}: EnhancedSelectInputSelectedValueProps) { + return ( + <div className={classNames(className, isDisabled && styles.isDisabled)}> + {children} + </div> + ); +} + +export default EnhancedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/HintedSelectInputOption.css b/frontend/src/Components/Form/Select/HintedSelectInputOption.css similarity index 100% rename from frontend/src/Components/Form/HintedSelectInputOption.css rename to frontend/src/Components/Form/Select/HintedSelectInputOption.css diff --git a/frontend/src/Components/Form/HintedSelectInputOption.css.d.ts b/frontend/src/Components/Form/Select/HintedSelectInputOption.css.d.ts similarity index 100% rename from frontend/src/Components/Form/HintedSelectInputOption.css.d.ts rename to frontend/src/Components/Form/Select/HintedSelectInputOption.css.d.ts diff --git a/frontend/src/Components/Form/Select/HintedSelectInputOption.tsx b/frontend/src/Components/Form/Select/HintedSelectInputOption.tsx new file mode 100644 index 000000000..faa9081c5 --- /dev/null +++ b/frontend/src/Components/Form/Select/HintedSelectInputOption.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import React from 'react'; +import EnhancedSelectInputOption, { + EnhancedSelectInputOptionProps, +} from './EnhancedSelectInputOption'; +import styles from './HintedSelectInputOption.css'; + +interface HintedSelectInputOptionProps extends EnhancedSelectInputOptionProps { + value: string; + hint?: React.ReactNode; +} + +function HintedSelectInputOption(props: HintedSelectInputOptionProps) { + const { + id, + value, + hint, + depth, + isSelected = false, + isDisabled, + isMobile, + ...otherProps + } = props; + + return ( + <EnhancedSelectInputOption + id={id} + depth={depth} + isSelected={isSelected} + isDisabled={isDisabled} + isHidden={isDisabled} + isMobile={isMobile} + {...otherProps} + > + <div + className={classNames(styles.optionText, isMobile && styles.isMobile)} + > + <div>{value}</div> + + {hint != null && <div className={styles.hintText}>{hint}</div>} + </div> + </EnhancedSelectInputOption> + ); +} + +HintedSelectInputOption.defaultProps = { + isDisabled: false, + isHidden: false, + isMultiSelect: false, +}; + +export default HintedSelectInputOption; diff --git a/frontend/src/Components/Form/HintedSelectInputSelectedValue.css b/frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.css similarity index 100% rename from frontend/src/Components/Form/HintedSelectInputSelectedValue.css rename to frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.css diff --git a/frontend/src/Components/Form/HintedSelectInputSelectedValue.css.d.ts b/frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.css.d.ts similarity index 100% rename from frontend/src/Components/Form/HintedSelectInputSelectedValue.css.d.ts rename to frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.css.d.ts diff --git a/frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.tsx b/frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.tsx new file mode 100644 index 000000000..7c4cba115 --- /dev/null +++ b/frontend/src/Components/Form/Select/HintedSelectInputSelectedValue.tsx @@ -0,0 +1,55 @@ +import React, { ReactNode, useMemo } from 'react'; +import Label from 'Components/Label'; +import ArrayElement from 'typings/Helpers/ArrayElement'; +import { EnhancedSelectInputValue } from './EnhancedSelectInput'; +import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; +import styles from './HintedSelectInputSelectedValue.css'; + +interface HintedSelectInputSelectedValueProps<T, V> { + selectedValue: V; + values: T[]; + hint?: ReactNode; + isMultiSelect?: boolean; + includeHint?: boolean; +} + +function HintedSelectInputSelectedValue< + T extends EnhancedSelectInputValue<V>, + V extends number | string +>(props: HintedSelectInputSelectedValueProps<T, V>) { + const { + selectedValue, + values, + hint, + isMultiSelect = false, + includeHint = true, + ...otherProps + } = props; + + const valuesMap = useMemo(() => { + return new Map(values.map((v) => [v.key, v.value])); + }, [values]); + + return ( + <EnhancedSelectInputSelectedValue + className={styles.selectedValue} + {...otherProps} + > + <div className={styles.valueText}> + {isMultiSelect && Array.isArray(selectedValue) + ? selectedValue.map((key) => { + const v = valuesMap.get(key); + + return <Label key={key}>{v ? v : key}</Label>; + }) + : valuesMap.get(selectedValue as ArrayElement<V>)} + </div> + + {hint != null && includeHint ? ( + <div className={styles.hintText}>{hint}</div> + ) : null} + </EnhancedSelectInputSelectedValue> + ); +} + +export default HintedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx similarity index 68% rename from frontend/src/Components/Form/IndexerFlagsSelectInput.tsx rename to frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx index 8dbd27a70..a43044156 100644 --- a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx +++ b/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx @@ -2,6 +2,7 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; +import { EnhancedSelectInputChanged } from 'typings/inputs'; import EnhancedSelectInput from './EnhancedSelectInput'; const selectIndexerFlagsValues = (selectedFlags: number) => @@ -32,29 +33,36 @@ const selectIndexerFlagsValues = (selectedFlags: number) => interface IndexerFlagsSelectInputProps { name: string; indexerFlags: number; - onChange(payload: object): void; + onChange(payload: EnhancedSelectInputChanged<number>): void; } -function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) { - const { indexerFlags, onChange } = props; - +function IndexerFlagsSelectInput({ + name, + indexerFlags, + onChange, + ...otherProps +}: IndexerFlagsSelectInputProps) { const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); - const onChangeWrapper = useCallback( - ({ name, value }: { name: string; value: number[] }) => { - const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0); + const handleChange = useCallback( + (change: EnhancedSelectInputChanged<number[]>) => { + const indexerFlags = change.value.reduce( + (acc, flagId) => acc + flagId, + 0 + ); onChange({ name, value: indexerFlags }); }, - [onChange] + [name, onChange] ); return ( <EnhancedSelectInput - {...props} + {...otherProps} + name={name} value={value} values={values} - onChange={onChangeWrapper} + onChange={handleChange} /> ); } diff --git a/frontend/src/Components/Form/Select/IndexerSelectInput.tsx b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx new file mode 100644 index 000000000..4bb4ff787 --- /dev/null +++ b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import { fetchIndexers } from 'Store/Actions/settingsActions'; +import { EnhancedSelectInputChanged } from 'typings/inputs'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +function createIndexersSelector(includeAny: boolean) { + return createSelector( + (state: AppState) => state.settings.indexers, + (indexers) => { + const { isFetching, isPopulated, error, items } = indexers; + + const values = items.sort(sortByProp('name')).map((indexer) => { + return { + key: indexer.id, + value: indexer.name, + }; + }); + + if (includeAny) { + values.unshift({ + key: 0, + value: `(${translate('Any')})`, + }); + } + + return { + isFetching, + isPopulated, + error, + values, + }; + } + ); +} + +interface IndexerSelectInputConnectorProps { + name: string; + value: number; + includeAny?: boolean; + values: object[]; + onChange: (change: EnhancedSelectInputChanged<number>) => void; +} + +function IndexerSelectInput({ + name, + value, + includeAny = false, + onChange, +}: IndexerSelectInputConnectorProps) { + const dispatch = useDispatch(); + const { isFetching, isPopulated, values } = useSelector( + createIndexersSelector(includeAny) + ); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchIndexers()); + } + }, [isPopulated, dispatch]); + + return ( + <EnhancedSelectInput + name={name} + value={value} + isFetching={isFetching} + values={values} + onChange={onChange} + /> + ); +} + +IndexerSelectInput.defaultProps = { + includeAny: false, +}; + +export default IndexerSelectInput; diff --git a/frontend/src/Components/Form/Select/LanguageSelectInput.tsx b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx new file mode 100644 index 000000000..80efde065 --- /dev/null +++ b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx @@ -0,0 +1,43 @@ +import React, { useMemo } from 'react'; +import { EnhancedSelectInputChanged } from 'typings/inputs'; +import EnhancedSelectInput, { + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +interface LanguageSelectInputProps { + name: string; + value: number; + values: EnhancedSelectInputValue<number>[]; + onChange: (change: EnhancedSelectInputChanged<number>) => void; +} + +function LanguageSelectInput({ + values, + onChange, + ...otherProps +}: LanguageSelectInputProps) { + const mappedValues = useMemo(() => { + const minId = values.reduce( + (min: number, v) => (v.key < 1 ? v.key : min), + values[0].key + ); + + return values.map(({ key, value }) => { + return { + key, + value, + dividerAfter: minId < 1 ? key === minId : false, + }; + }); + }, [values]); + + return ( + <EnhancedSelectInput + {...otherProps} + values={mappedValues} + onChange={onChange} + /> + ); +} + +export default LanguageSelectInput; diff --git a/frontend/src/Components/Form/Select/MonitorEpisodesSelectInput.tsx b/frontend/src/Components/Form/Select/MonitorEpisodesSelectInput.tsx new file mode 100644 index 000000000..59fd08513 --- /dev/null +++ b/frontend/src/Components/Form/Select/MonitorEpisodesSelectInput.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import monitorOptions from 'Utilities/Series/monitorOptions'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +interface MonitorEpisodesSelectInputProps + extends Omit< + EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, + 'values' + > { + includeNoChange: boolean; + includeMixed: boolean; +} + +function MonitorEpisodesSelectInput(props: MonitorEpisodesSelectInputProps) { + const { + includeNoChange = false, + includeMixed = false, + ...otherProps + } = props; + + const values: EnhancedSelectInputValue<string>[] = [...monitorOptions]; + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + get value() { + return translate('NoChange'); + }, + isDisabled: true, + }); + } + + if (includeMixed) { + values.unshift({ + key: 'mixed', + get value() { + return `(${translate('Mixed')})`; + }, + isDisabled: true, + }); + } + + return <EnhancedSelectInput {...otherProps} values={values} />; +} + +export default MonitorEpisodesSelectInput; diff --git a/frontend/src/Components/Form/Select/MonitorNewItemsSelectInput.tsx b/frontend/src/Components/Form/Select/MonitorNewItemsSelectInput.tsx new file mode 100644 index 000000000..ac11f1fca --- /dev/null +++ b/frontend/src/Components/Form/Select/MonitorNewItemsSelectInput.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +interface MonitorNewItemsSelectInputProps + extends Omit< + EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, + 'values' + > { + includeNoChange?: boolean; + includeMixed?: boolean; + onChange: (...args: unknown[]) => unknown; +} + +function MonitorNewItemsSelectInput(props: MonitorNewItemsSelectInputProps) { + const { + includeNoChange = false, + includeMixed = false, + ...otherProps + } = props; + + const values: EnhancedSelectInputValue<string>[] = [ + ...monitorNewItemsOptions, + ]; + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + value: 'No Change', + isDisabled: true, + }); + } + + if (includeMixed) { + values.unshift({ + key: 'mixed', + value: '(Mixed)', + isDisabled: true, + }); + } + + return <EnhancedSelectInput {...otherProps} values={values} />; +} + +export default MonitorNewItemsSelectInput; diff --git a/frontend/src/Components/Form/Select/ProviderOptionSelectInput.tsx b/frontend/src/Components/Form/Select/ProviderOptionSelectInput.tsx new file mode 100644 index 000000000..e4a8003eb --- /dev/null +++ b/frontend/src/Components/Form/Select/ProviderOptionSelectInput.tsx @@ -0,0 +1,164 @@ +import { isEqual } from 'lodash'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import ProviderOptionsAppState, { + ProviderOptions, +} from 'App/State/ProviderOptionsAppState'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { + clearOptions, + fetchOptions, +} from 'Store/Actions/providerOptionActions'; +import { FieldSelectOption } from 'typings/Field'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +const importantFieldNames = ['baseUrl', 'apiPath', 'apiKey', 'authToken']; + +function getProviderDataKey(providerData: ProviderOptions) { + if (!providerData || !providerData.fields) { + return null; + } + + const fields = providerData.fields + .filter((f) => importantFieldNames.includes(f.name)) + .map((f) => f.value); + + return fields; +} + +function getSelectOptions(items: FieldSelectOption<unknown>[]) { + if (!items) { + return []; + } + + return items.map((option) => { + return { + key: option.value, + value: option.name, + hint: option.hint, + parentKey: option.parentValue, + isDisabled: option.isDisabled, + additionalProperties: option.additionalProperties, + }; + }); +} + +function createProviderOptionsSelector( + selectOptionsProviderAction: keyof Omit<ProviderOptionsAppState, 'devices'> +) { + return createSelector( + (state: AppState) => state.providerOptions[selectOptionsProviderAction], + (options) => { + if (!options) { + return { + isFetching: false, + values: [], + }; + } + + return { + isFetching: options.isFetching, + values: getSelectOptions(options.items), + }; + } + ); +} + +interface ProviderOptionSelectInputProps + extends Omit< + EnhancedSelectInputProps<EnhancedSelectInputValue<unknown>, unknown>, + 'values' + > { + provider: string; + providerData: ProviderOptions; + name: string; + value: unknown; + selectOptionsProviderAction: keyof Omit<ProviderOptionsAppState, 'devices'>; +} + +function ProviderOptionSelectInput({ + provider, + providerData, + selectOptionsProviderAction, + ...otherProps +}: ProviderOptionSelectInputProps) { + const dispatch = useDispatch(); + const [isRefetchRequired, setIsRefetchRequired] = useState(false); + const previousProviderData = usePrevious(providerData); + const { isFetching, values } = useSelector( + createProviderOptionsSelector(selectOptionsProviderAction) + ); + + const handleOpen = useCallback(() => { + if (isRefetchRequired && selectOptionsProviderAction) { + setIsRefetchRequired(false); + + dispatch( + fetchOptions({ + section: selectOptionsProviderAction, + action: selectOptionsProviderAction, + provider, + providerData, + }) + ); + } + }, [ + isRefetchRequired, + provider, + providerData, + selectOptionsProviderAction, + dispatch, + ]); + + useEffect(() => { + if (selectOptionsProviderAction) { + dispatch( + fetchOptions({ + section: selectOptionsProviderAction, + action: selectOptionsProviderAction, + provider, + providerData, + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectOptionsProviderAction, dispatch]); + + useEffect(() => { + if (!previousProviderData) { + return; + } + + const prevKey = getProviderDataKey(previousProviderData); + const nextKey = getProviderDataKey(providerData); + + if (!isEqual(prevKey, nextKey)) { + setIsRefetchRequired(true); + } + }, [providerData, previousProviderData, setIsRefetchRequired]); + + useEffect(() => { + return () => { + if (selectOptionsProviderAction) { + dispatch(clearOptions({ section: selectOptionsProviderAction })); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <EnhancedSelectInput + {...otherProps} + isFetching={isFetching} + values={values} + onOpen={handleOpen} + /> + ); +} + +export default ProviderOptionSelectInput; diff --git a/frontend/src/Components/Form/Select/QualityProfileSelectInput.tsx b/frontend/src/Components/Form/Select/QualityProfileSelectInput.tsx new file mode 100644 index 000000000..036f0f82c --- /dev/null +++ b/frontend/src/Components/Form/Select/QualityProfileSelectInput.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import { QualityProfilesAppState } from 'App/State/SettingsAppState'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import { EnhancedSelectInputChanged } from 'typings/inputs'; +import QualityProfile from 'typings/QualityProfile'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; + +function createQualityProfilesSelector( + includeNoChange: boolean, + includeNoChangeDisabled: boolean, + includeMixed: boolean +) { + return createSelector( + createSortedSectionSelector( + 'settings.qualityProfiles', + sortByProp<QualityProfile, 'name'>('name') + ), + (qualityProfiles: QualityProfilesAppState) => { + const values: EnhancedSelectInputValue<number | string>[] = + qualityProfiles.items.map((qualityProfile) => { + return { + key: qualityProfile.id, + value: qualityProfile.name, + }; + }); + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + get value() { + return translate('NoChange'); + }, + isDisabled: includeNoChangeDisabled, + }); + } + + if (includeMixed) { + values.unshift({ + key: 'mixed', + get value() { + return `(${translate('Mixed')})`; + }, + isDisabled: true, + }); + } + + return values; + } + ); +} + +interface QualityProfileSelectInputConnectorProps + extends Omit< + EnhancedSelectInputProps< + EnhancedSelectInputValue<number | string>, + number | string + >, + 'values' + > { + name: string; + includeNoChange?: boolean; + includeNoChangeDisabled?: boolean; + includeMixed?: boolean; +} + +function QualityProfileSelectInput({ + name, + value, + includeNoChange = false, + includeNoChangeDisabled = true, + includeMixed = false, + onChange, + ...otherProps +}: QualityProfileSelectInputConnectorProps) { + const values = useSelector( + createQualityProfilesSelector( + includeNoChange, + includeNoChangeDisabled, + includeMixed + ) + ); + + const handleChange = useCallback( + ({ value: newValue }: EnhancedSelectInputChanged<string | number>) => { + onChange({ + name, + value: newValue === 'noChange' ? value : newValue, + }); + }, + [name, value, onChange] + ); + + useEffect(() => { + if ( + !value || + !values.some((option) => option.key === value || option.key === value) + ) { + const firstValue = values.find( + (option) => typeof option.key === 'number' + ); + + if (firstValue) { + onChange({ name, value: firstValue.key }); + } + } + }, [name, value, values, onChange]); + + return ( + <EnhancedSelectInput + {...otherProps} + name={name} + value={value} + values={values} + onChange={handleChange} + /> + ); +} + +export default QualityProfileSelectInput; diff --git a/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx new file mode 100644 index 000000000..4704a3cd4 --- /dev/null +++ b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx @@ -0,0 +1,215 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { addRootFolder } from 'Store/Actions/rootFolderActions'; +import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; +import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; +import RootFolderSelectInputOption from './RootFolderSelectInputOption'; +import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue'; + +const ADD_NEW_KEY = 'addNew'; + +export interface RootFolderSelectInputValue + extends EnhancedSelectInputValue<string> { + isMissing?: boolean; +} + +interface RootFolderSelectInputProps + extends Omit< + EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, + 'value' | 'values' + > { + name: string; + value?: string; + isSaving: boolean; + saveError?: object; + includeNoChange: boolean; +} + +function createRootFolderOptionsSelector( + value: string | undefined, + includeMissingValue: boolean, + includeNoChange: boolean, + includeNoChangeDisabled: boolean +) { + return createSelector( + createRootFoldersSelector(), + + (rootFolders) => { + const values: RootFolderSelectInputValue[] = rootFolders.items.map( + (rootFolder) => { + return { + key: rootFolder.path, + value: rootFolder.path, + freeSpace: rootFolder.freeSpace, + isMissing: false, + }; + } + ); + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + get value() { + return translate('NoChange'); + }, + isDisabled: includeNoChangeDisabled, + isMissing: false, + }); + } + + if (!values.length) { + values.push({ + key: '', + value: '', + isDisabled: true, + isHidden: true, + }); + } + + if ( + includeMissingValue && + value && + !values.find((v) => v.key === value) + ) { + values.push({ + key: value, + value, + isMissing: true, + isDisabled: true, + }); + } + + values.push({ + key: ADD_NEW_KEY, + value: translate('AddANewPath'), + }); + + return { + values, + isSaving: rootFolders.isSaving, + saveError: rootFolders.saveError, + }; + } + ); +} + +function RootFolderSelectInput({ + name, + value, + includeNoChange = false, + onChange, + ...otherProps +}: RootFolderSelectInputProps) { + const dispatch = useDispatch(); + const { values, isSaving, saveError } = useSelector( + createRootFolderOptionsSelector(value, true, includeNoChange, false) + ); + const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] = + useState(false); + const [newRootFolderPath, setNewRootFolderPath] = useState(''); + const previousIsSaving = usePrevious(isSaving); + + const handleChange = useCallback( + ({ value: newValue }: EnhancedSelectInputChanged<string>) => { + if (newValue === 'addNew') { + setIsAddNewRootFolderModalOpen(true); + } else { + onChange({ name, value: newValue }); + } + }, + [name, setIsAddNewRootFolderModalOpen, onChange] + ); + + const handleNewRootFolderSelect = useCallback( + ({ value: newValue }: InputChanged<string>) => { + setNewRootFolderPath(newValue); + dispatch(addRootFolder(newValue)); + }, + [setNewRootFolderPath, dispatch] + ); + + const handleAddRootFolderModalClose = useCallback(() => { + setIsAddNewRootFolderModalOpen(false); + }, [setIsAddNewRootFolderModalOpen]); + + useEffect(() => { + if ( + !value && + values.length && + values.some((v) => !!v.key && v.key !== ADD_NEW_KEY) + ) { + const defaultValue = values[0]; + + if (defaultValue.key !== ADD_NEW_KEY) { + onChange({ name, value: defaultValue.key }); + } + } + + if (previousIsSaving && !isSaving && !saveError && newRootFolderPath) { + onChange({ name, value: newRootFolderPath }); + setNewRootFolderPath(''); + } + }, [ + name, + value, + values, + isSaving, + saveError, + previousIsSaving, + newRootFolderPath, + onChange, + ]); + + useEffect(() => { + if (value == null && values[0].key === '') { + onChange({ name, value: '' }); + } else if ( + !value || + !values.some((v) => v.key === value) || + value === ADD_NEW_KEY + ) { + const defaultValue = values[0]; + + if (defaultValue.key === ADD_NEW_KEY) { + onChange({ name, value: '' }); + } else { + onChange({ name, value: defaultValue.key }); + } + } + + // Only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + <EnhancedSelectInput + {...otherProps} + name={name} + value={value ?? ''} + values={values} + selectedValueComponent={RootFolderSelectInputSelectedValue} + optionComponent={RootFolderSelectInputOption} + onChange={handleChange} + /> + + <FileBrowserModal + isOpen={isAddNewRootFolderModalOpen} + name="rootFolderPath" + value="" + onChange={handleNewRootFolderSelect} + onModalClose={handleAddRootFolderModalClose} + /> + </> + ); +} + +export default RootFolderSelectInput; diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css b/frontend/src/Components/Form/Select/RootFolderSelectInputOption.css similarity index 100% rename from frontend/src/Components/Form/RootFolderSelectInputOption.css rename to frontend/src/Components/Form/Select/RootFolderSelectInputOption.css diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts b/frontend/src/Components/Form/Select/RootFolderSelectInputOption.css.d.ts similarity index 100% rename from frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts rename to frontend/src/Components/Form/Select/RootFolderSelectInputOption.css.d.ts diff --git a/frontend/src/Components/Form/Select/RootFolderSelectInputOption.tsx b/frontend/src/Components/Form/Select/RootFolderSelectInputOption.tsx new file mode 100644 index 000000000..d71f0d638 --- /dev/null +++ b/frontend/src/Components/Form/Select/RootFolderSelectInputOption.tsx @@ -0,0 +1,67 @@ +import classNames from 'classnames'; +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInputOption, { + EnhancedSelectInputOptionProps, +} from './EnhancedSelectInputOption'; +import styles from './RootFolderSelectInputOption.css'; + +interface RootFolderSelectInputOptionProps + extends EnhancedSelectInputOptionProps { + id: string; + value: string; + freeSpace?: number; + isMissing?: boolean; + seriesFolder?: string; + isMobile: boolean; + isWindows?: boolean; +} + +function RootFolderSelectInputOption(props: RootFolderSelectInputOptionProps) { + const { + id, + value, + freeSpace, + isMissing, + seriesFolder, + isMobile, + isWindows, + ...otherProps + } = props; + + const slashCharacter = isWindows ? '\\' : '/'; + + return ( + <EnhancedSelectInputOption id={id} isMobile={isMobile} {...otherProps}> + <div + className={classNames(styles.optionText, isMobile && styles.isMobile)} + > + <div className={styles.value}> + {value} + + {seriesFolder && id !== 'addNew' ? ( + <div className={styles.seriesFolder}> + {slashCharacter} + {seriesFolder} + </div> + ) : null} + </div> + + {freeSpace == null ? null : ( + <div className={styles.freeSpace}> + {translate('RootFolderSelectFreeSpace', { + freeSpace: formatBytes(freeSpace), + })} + </div> + )} + + {isMissing ? ( + <div className={styles.isMissing}>{translate('Missing')}</div> + ) : null} + </div> + </EnhancedSelectInputOption> + ); +} + +export default RootFolderSelectInputOption; diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css b/frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.css similarity index 100% rename from frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css rename to frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.css diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts b/frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.css.d.ts similarity index 100% rename from frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts rename to frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.css.d.ts diff --git a/frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.tsx b/frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.tsx new file mode 100644 index 000000000..e06101f2a --- /dev/null +++ b/frontend/src/Components/Form/Select/RootFolderSelectInputSelectedValue.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; +import { RootFolderSelectInputValue } from './RootFolderSelectInput'; +import styles from './RootFolderSelectInputSelectedValue.css'; + +interface RootFolderSelectInputSelectedValueProps { + selectedValue: string; + values: RootFolderSelectInputValue[]; + freeSpace?: number; + seriesFolder?: string; + isWindows?: boolean; + includeFreeSpace?: boolean; +} + +function RootFolderSelectInputSelectedValue( + props: RootFolderSelectInputSelectedValueProps +) { + const { + selectedValue, + values, + freeSpace, + seriesFolder, + includeFreeSpace = true, + isWindows, + ...otherProps + } = props; + + const slashCharacter = isWindows ? '\\' : '/'; + const value = values.find((v) => v.key === selectedValue)?.value; + + return ( + <EnhancedSelectInputSelectedValue + className={styles.selectedValue} + {...otherProps} + > + <div className={styles.pathContainer}> + <div className={styles.path}>{value}</div> + + {seriesFolder ? ( + <div className={styles.seriesFolder}> + {slashCharacter} + {seriesFolder} + </div> + ) : null} + </div> + + {freeSpace != null && includeFreeSpace ? ( + <div className={styles.freeSpace}> + {translate('RootFolderSelectFreeSpace', { + freeSpace: formatBytes(freeSpace), + })} + </div> + ) : null} + </EnhancedSelectInputSelectedValue> + ); +} + +export default RootFolderSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/Select/SeriesTypeSelectInput.tsx similarity index 64% rename from frontend/src/Components/Form/SeriesTypeSelectInput.tsx rename to frontend/src/Components/Form/Select/SeriesTypeSelectInput.tsx index 17082c75c..6a3bba650 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/Select/SeriesTypeSelectInput.tsx @@ -1,17 +1,21 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import * as seriesTypes from 'Utilities/Series/seriesTypes'; import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; +import EnhancedSelectInput, { + EnhancedSelectInputProps, + EnhancedSelectInputValue, +} from './EnhancedSelectInput'; import SeriesTypeSelectInputOption from './SeriesTypeSelectInputOption'; import SeriesTypeSelectInputSelectedValue from './SeriesTypeSelectInputSelectedValue'; -interface SeriesTypeSelectInputProps { +interface SeriesTypeSelectInputProps + extends EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string> { includeNoChange: boolean; includeNoChangeDisabled?: boolean; includeMixed: boolean; } -interface ISeriesTypeOption { +export interface ISeriesTypeOption { key: string; value: string; format?: string; @@ -43,29 +47,33 @@ const seriesTypeOptions: ISeriesTypeOption[] = [ ]; function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { - const values = [...seriesTypeOptions]; - const { includeNoChange = false, includeNoChangeDisabled = true, includeMixed = false, } = props; - if (includeNoChange) { - values.unshift({ - key: 'noChange', - value: translate('NoChange'), - isDisabled: includeNoChangeDisabled, - }); - } + const values = useMemo(() => { + const result = [...seriesTypeOptions]; - if (includeMixed) { - values.unshift({ - key: 'mixed', - value: `(${translate('Mixed')})`, - isDisabled: true, - }); - } + if (includeNoChange) { + result.unshift({ + key: 'noChange', + value: translate('NoChange'), + isDisabled: includeNoChangeDisabled, + }); + } + + if (includeMixed) { + result.unshift({ + key: 'mixed', + value: `(${translate('Mixed')})`, + isDisabled: true, + }); + } + + return result; + }, [includeNoChange, includeNoChangeDisabled, includeMixed]); return ( <EnhancedSelectInput diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputOption.css b/frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.css similarity index 100% rename from frontend/src/Components/Form/SeriesTypeSelectInputOption.css rename to frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.css diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputOption.css.d.ts b/frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.css.d.ts similarity index 100% rename from frontend/src/Components/Form/SeriesTypeSelectInputOption.css.d.ts rename to frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.css.d.ts diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputOption.tsx b/frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.tsx similarity index 63% rename from frontend/src/Components/Form/SeriesTypeSelectInputOption.tsx rename to frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.tsx index 2fb358cce..0bfc3b6e9 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInputOption.tsx +++ b/frontend/src/Components/Form/Select/SeriesTypeSelectInputOption.tsx @@ -1,20 +1,23 @@ import classNames from 'classnames'; import React from 'react'; -import EnhancedSelectInputOption from './EnhancedSelectInputOption'; +import EnhancedSelectInputOption, { + EnhancedSelectInputOptionProps, +} from './EnhancedSelectInputOption'; import styles from './SeriesTypeSelectInputOption.css'; -interface SeriesTypeSelectInputOptionProps { - key: string; +interface SeriesTypeSelectInputOptionProps + extends EnhancedSelectInputOptionProps { + id: string; value: string; format: string; isMobile: boolean; } function SeriesTypeSelectInputOption(props: SeriesTypeSelectInputOptionProps) { - const { key, value, format, isMobile, ...otherProps } = props; + const { id, value, format, isMobile, ...otherProps } = props; return ( - <EnhancedSelectInputOption id={key} isMobile={isMobile} {...otherProps}> + <EnhancedSelectInputOption {...otherProps} id={id} isMobile={isMobile}> <div className={classNames(styles.optionText, isMobile && styles.isMobile)} > diff --git a/frontend/src/Components/Form/Select/SeriesTypeSelectInputSelectedValue.tsx b/frontend/src/Components/Form/Select/SeriesTypeSelectInputSelectedValue.tsx new file mode 100644 index 000000000..b6470f1a4 --- /dev/null +++ b/frontend/src/Components/Form/Select/SeriesTypeSelectInputSelectedValue.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; +import { ISeriesTypeOption } from './SeriesTypeSelectInput'; + +interface SeriesTypeSelectInputOptionProps { + selectedValue: string; + values: ISeriesTypeOption[]; + format: string; +} +function SeriesTypeSelectInputSelectedValue( + props: SeriesTypeSelectInputOptionProps +) { + const { selectedValue, values, ...otherProps } = props; + const format = values.find((v) => v.key === selectedValue)?.format; + + return ( + <HintedSelectInputSelectedValue + {...otherProps} + selectedValue={selectedValue} + values={values} + hint={format} + /> + ); +} + +export default SeriesTypeSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/UMaskInput.css b/frontend/src/Components/Form/Select/UMaskInput.css similarity index 93% rename from frontend/src/Components/Form/UMaskInput.css rename to frontend/src/Components/Form/Select/UMaskInput.css index 91486687e..a777aaeef 100644 --- a/frontend/src/Components/Form/UMaskInput.css +++ b/frontend/src/Components/Form/Select/UMaskInput.css @@ -1,53 +1,53 @@ -.inputWrapper { - display: flex; -} - -.inputFolder { - composes: input from '~Components/Form/Input.css'; - - max-width: 100px; -} - -.inputUnitWrapper { - position: relative; - width: 100%; -} - -.inputUnit { - composes: inputUnit from '~Components/Form/FormInputGroup.css'; - - right: 40px; - font-family: $monoSpaceFontFamily; -} - -.unit { - font-family: $monoSpaceFontFamily; -} - -.details { - margin-top: 5px; - margin-left: 17px; - line-height: 20px; - - > div { - display: flex; - - label { - flex: 0 0 50px; - } - - .value { - width: 50px; - text-align: right; - } - - .unit { - width: 90px; - text-align: right; - } - } -} - -.readOnly { - background-color: var(--inputReadOnlyBackgroundColor); -} +.inputWrapper { + display: flex; +} + +.inputFolder { + composes: input from '~Components/Form/Input.css'; + + max-width: 100px; +} + +.inputUnitWrapper { + position: relative; + width: 100%; +} + +.inputUnit { + composes: inputUnit from '~Components/Form/FormInputGroup.css'; + + right: 40px; + font-family: $monoSpaceFontFamily; +} + +.unit { + font-family: $monoSpaceFontFamily; +} + +.details { + margin-top: 5px; + margin-left: 17px; + line-height: 20px; + + > div { + display: flex; + + label { + flex: 0 0 50px; + } + + .value { + width: 50px; + text-align: right; + } + + .unit { + width: 90px; + text-align: right; + } + } +} + +.readOnly { + background-color: var(--inputReadOnlyBackgroundColor); +} diff --git a/frontend/src/Components/Form/UMaskInput.css.d.ts b/frontend/src/Components/Form/Select/UMaskInput.css.d.ts similarity index 100% rename from frontend/src/Components/Form/UMaskInput.css.d.ts rename to frontend/src/Components/Form/Select/UMaskInput.css.d.ts diff --git a/frontend/src/Components/Form/Select/UMaskInput.tsx b/frontend/src/Components/Form/Select/UMaskInput.tsx new file mode 100644 index 000000000..1f537f968 --- /dev/null +++ b/frontend/src/Components/Form/Select/UMaskInput.tsx @@ -0,0 +1,142 @@ +/* eslint-disable no-bitwise */ +import PropTypes from 'prop-types'; +import React, { SyntheticEvent } from 'react'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import EnhancedSelectInput from './EnhancedSelectInput'; +import styles from './UMaskInput.css'; + +const umaskOptions = [ + { + key: '755', + get value() { + return translate('Umask755Description', { octal: '755' }); + }, + hint: 'drwxr-xr-x', + }, + { + key: '775', + get value() { + return translate('Umask775Description', { octal: '775' }); + }, + hint: 'drwxrwxr-x', + }, + { + key: '770', + get value() { + return translate('Umask770Description', { octal: '770' }); + }, + hint: 'drwxrwx---', + }, + { + key: '750', + get value() { + return translate('Umask750Description', { octal: '750' }); + }, + hint: 'drwxr-x---', + }, + { + key: '777', + get value() { + return translate('Umask777Description', { octal: '777' }); + }, + hint: 'drwxrwxrwx', + }, +]; + +function formatPermissions(permissions: number) { + const hasSticky = permissions & 0o1000; + const hasSetGID = permissions & 0o2000; + const hasSetUID = permissions & 0o4000; + + let result = ''; + + for (let i = 0; i < 9; i++) { + const bit = (permissions & (1 << i)) !== 0; + let digit = bit ? 'xwr'[i % 3] : '-'; + if (i === 6 && hasSetUID) { + digit = bit ? 's' : 'S'; + } else if (i === 3 && hasSetGID) { + digit = bit ? 's' : 'S'; + } else if (i === 0 && hasSticky) { + digit = bit ? 't' : 'T'; + } + result = digit + result; + } + + return result; +} + +interface UMaskInputProps { + name: string; + value: string; + hasError?: boolean; + hasWarning?: boolean; + onChange: (change: InputChanged) => void; + onFocus?: (event: SyntheticEvent) => void; + onBlur?: (event: SyntheticEvent) => void; +} + +function UMaskInput({ name, value, onChange }: UMaskInputProps) { + const valueNum = parseInt(value, 8); + const umaskNum = 0o777 & ~valueNum; + const umask = umaskNum.toString(8).padStart(4, '0'); + const folderNum = 0o777 & ~umaskNum; + const folder = folderNum.toString(8).padStart(3, '0'); + const fileNum = 0o666 & ~umaskNum; + const file = fileNum.toString(8).padStart(3, '0'); + const unit = formatPermissions(folderNum); + + const values = umaskOptions.map((v) => { + return { ...v, hint: <span className={styles.unit}>{v.hint}</span> }; + }); + + return ( + <div> + <div className={styles.inputWrapper}> + <div className={styles.inputUnitWrapper}> + <EnhancedSelectInput + name={name} + value={value} + values={values} + isEditable={true} + onChange={onChange} + /> + + <div className={styles.inputUnit}>d{unit}</div> + </div> + </div> + + <div className={styles.details}> + <div> + <label>{translate('Umask')}</label> + <div className={styles.value}>{umask}</div> + </div> + + <div> + <label>{translate('Folder')}</label> + <div className={styles.value}>{folder}</div> + <div className={styles.unit}>d{formatPermissions(folderNum)}</div> + </div> + + <div> + <label>{translate('File')}</label> + <div className={styles.value}>{file}</div> + <div className={styles.unit}>{formatPermissions(fileNum)}</div> + </div> + </div> + </div> + ); +} + +UMaskInput.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onFocus: PropTypes.func, + onBlur: PropTypes.func, +}; + +export default UMaskInput; diff --git a/frontend/src/Components/Form/SelectInput.js b/frontend/src/Components/Form/SelectInput.js deleted file mode 100644 index 553501afc..000000000 --- a/frontend/src/Components/Form/SelectInput.js +++ /dev/null @@ -1,95 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './SelectInput.css'; - -class SelectInput extends Component { - - // - // Listeners - - onChange = (event) => { - this.props.onChange({ - name: this.props.name, - value: event.target.value - }); - }; - - // - // Render - - render() { - const { - className, - disabledClassName, - name, - value, - values, - isDisabled, - hasError, - hasWarning, - autoFocus, - onBlur - } = this.props; - - return ( - <select - className={classNames( - className, - hasError && styles.hasError, - hasWarning && styles.hasWarning, - isDisabled && disabledClassName - )} - disabled={isDisabled} - name={name} - value={value} - autoFocus={autoFocus} - onChange={this.onChange} - onBlur={onBlur} - > - { - values.map((option) => { - const { - key, - value: optionValue, - ...otherOptionProps - } = option; - - return ( - <option - key={key} - value={key} - {...otherOptionProps} - > - {typeof optionValue === 'function' ? optionValue() : optionValue} - </option> - ); - }) - } - </select> - ); - } -} - -SelectInput.propTypes = { - className: PropTypes.string, - disabledClassName: PropTypes.string, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - isDisabled: PropTypes.bool, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - autoFocus: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func -}; - -SelectInput.defaultProps = { - className: styles.select, - disabledClassName: styles.isDisabled, - isDisabled: false, - autoFocus: false -}; - -export default SelectInput; diff --git a/frontend/src/Components/Form/SelectInput.tsx b/frontend/src/Components/Form/SelectInput.tsx new file mode 100644 index 000000000..4716c2dfd --- /dev/null +++ b/frontend/src/Components/Form/SelectInput.tsx @@ -0,0 +1,76 @@ +import classNames from 'classnames'; +import React, { ChangeEvent, SyntheticEvent, useCallback } from 'react'; +import { InputChanged } from 'typings/inputs'; +import styles from './SelectInput.css'; + +interface SelectInputOption { + key: string; + value: string | number | (() => string | number); +} + +interface SelectInputProps<T> { + className?: string; + disabledClassName?: string; + name: string; + value: string | number; + values: SelectInputOption[]; + isDisabled?: boolean; + hasError?: boolean; + hasWarning?: boolean; + autoFocus?: boolean; + onChange: (change: InputChanged<T>) => void; + onBlur?: (event: SyntheticEvent) => void; +} + +function SelectInput<T>({ + className = styles.select, + disabledClassName = styles.isDisabled, + name, + value, + values, + isDisabled = false, + hasError, + hasWarning, + autoFocus = false, + onBlur, + onChange, +}: SelectInputProps<T>) { + const handleChange = useCallback( + (event: ChangeEvent<HTMLSelectElement>) => { + onChange({ + name, + value: event.target.value as T, + }); + }, + [name, onChange] + ); + + return ( + <select + className={classNames( + className, + hasError && styles.hasError, + hasWarning && styles.hasWarning, + isDisabled && disabledClassName + )} + disabled={isDisabled} + name={name} + value={value} + autoFocus={autoFocus} + onChange={handleChange} + onBlur={onBlur} + > + {values.map((option) => { + const { key, value: optionValue, ...otherOptionProps } = option; + + return ( + <option key={key} value={key} {...otherOptionProps}> + {typeof optionValue === 'function' ? optionValue() : optionValue} + </option> + ); + })} + </select> + ); +} + +export default SelectInput; diff --git a/frontend/src/Components/Form/SeriesTagInput.tsx b/frontend/src/Components/Form/SeriesTagInput.tsx deleted file mode 100644 index 3d8279aa6..000000000 --- a/frontend/src/Components/Form/SeriesTagInput.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 - <TagInputConnector - {...otherProps} - value={finalValue} - onChange={handleChange} - /> - ); -} diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css b/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css deleted file mode 100644 index c76b0a263..000000000 --- a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css +++ /dev/null @@ -1,20 +0,0 @@ -.selectedValue { - composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css'; - - display: flex; - align-items: center; - justify-content: space-between; - overflow: hidden; -} - -.value { - display: flex; -} - -.format { - flex: 0 0 auto; - margin-left: 15px; - color: var(--gray); - text-align: right; - font-size: $smallFontSize; -} diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css.d.ts b/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css.d.ts deleted file mode 100644 index f6e19e481..000000000 --- a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'format': string; - 'selectedValue': string; - 'value': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.tsx b/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.tsx deleted file mode 100644 index 94d2b7157..000000000 --- a/frontend/src/Components/Form/SeriesTypeSelectInputSelectedValue.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; -import styles from './SeriesTypeSelectInputSelectedValue.css'; - -interface SeriesTypeSelectInputOptionProps { - key: string; - value: string; - format: string; -} -function SeriesTypeSelectInputSelectedValue( - props: SeriesTypeSelectInputOptionProps -) { - const { value, format, ...otherProps } = props; - - return ( - <EnhancedSelectInputSelectedValue - className={styles.selectedValue} - {...otherProps} - > - <div className={styles.value}>{value}</div> - - {format == null ? null : <div className={styles.format}>{format}</div>} - </EnhancedSelectInputSelectedValue> - ); -} - -export default SeriesTypeSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/DeviceInput.css b/frontend/src/Components/Form/Tag/DeviceInput.css similarity index 64% rename from frontend/src/Components/Form/DeviceInput.css rename to frontend/src/Components/Form/Tag/DeviceInput.css index 7abe83db5..189cafc6b 100644 --- a/frontend/src/Components/Form/DeviceInput.css +++ b/frontend/src/Components/Form/Tag/DeviceInput.css @@ -3,6 +3,6 @@ } .input { - composes: input from '~./TagInput.css'; + composes: input from '~Components/Form/Tag/TagInput.css'; composes: hasButton from '~Components/Form/Input.css'; } diff --git a/frontend/src/Components/Form/DeviceInput.css.d.ts b/frontend/src/Components/Form/Tag/DeviceInput.css.d.ts similarity index 100% rename from frontend/src/Components/Form/DeviceInput.css.d.ts rename to frontend/src/Components/Form/Tag/DeviceInput.css.d.ts diff --git a/frontend/src/Components/Form/Tag/DeviceInput.tsx b/frontend/src/Components/Form/Tag/DeviceInput.tsx new file mode 100644 index 000000000..3c483d1f2 --- /dev/null +++ b/frontend/src/Components/Form/Tag/DeviceInput.tsx @@ -0,0 +1,149 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FormInputButton from 'Components/Form/FormInputButton'; +import Icon from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import { + clearOptions, + defaultState, + fetchOptions, +} from 'Store/Actions/providerOptionActions'; +import { InputChanged } from 'typings/inputs'; +import TagInput, { TagInputProps } from './TagInput'; +import styles from './DeviceInput.css'; + +interface DeviceTag { + id: string; + name: string; +} + +interface DeviceInputProps extends TagInputProps<DeviceTag> { + className?: string; + name: string; + value: string[]; + hasError?: boolean; + hasWarning?: boolean; + provider: string; + providerData: object; + onChange: (change: InputChanged<string[]>) => unknown; +} + +function createDeviceTagsSelector(value: string[]) { + return createSelector( + (state: AppState) => state.providerOptions.devices || defaultState, + (devices) => { + return { + ...devices, + selectedDevices: value.map((valueDevice) => { + const device = devices.items.find((d) => d.id === valueDevice); + + if (device) { + return { + id: device.id, + name: `${device.name} (${device.id})`, + }; + } + + return { + id: valueDevice, + name: `Unknown (${valueDevice})`, + }; + }), + }; + } + ); +} + +function DeviceInput({ + className = styles.deviceInputWrapper, + name, + value, + hasError, + hasWarning, + provider, + providerData, + onChange, +}: DeviceInputProps) { + const dispatch = useDispatch(); + const { items, selectedDevices, isFetching } = useSelector( + createDeviceTagsSelector(value) + ); + + const handleRefreshPress = useCallback(() => { + dispatch( + fetchOptions({ + section: 'devices', + action: 'getDevices', + provider, + providerData, + }) + ); + }, [provider, providerData, dispatch]); + + const handleTagAdd = useCallback( + (device: DeviceTag) => { + // New tags won't have an ID, only a name. + const deviceId = device.id || device.name; + + onChange({ + name, + value: [...value, deviceId], + }); + }, + [name, value, onChange] + ); + + const handleTagDelete = useCallback( + ({ index }: { index: number }) => { + const newValue = value.slice(); + newValue.splice(index, 1); + + onChange({ + name, + value: newValue, + }); + }, + [name, value, onChange] + ); + + useEffect(() => { + dispatch( + fetchOptions({ + section: 'devices', + action: 'getDevices', + provider, + providerData, + }) + ); + + return () => { + dispatch(clearOptions({ section: 'devices' })); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch]); + + return ( + <div className={className}> + <TagInput + inputContainerClassName={styles.input} + name={name} + tags={selectedDevices} + tagList={items} + allowNew={true} + minQueryLength={0} + hasError={hasError} + hasWarning={hasWarning} + onTagAdd={handleTagAdd} + onTagDelete={handleTagDelete} + /> + + <FormInputButton onPress={handleRefreshPress}> + <Icon name={icons.REFRESH} isSpinning={isFetching} /> + </FormInputButton> + </div> + ); +} + +export default DeviceInput; diff --git a/frontend/src/Components/Form/Tag/SeriesTagInput.tsx b/frontend/src/Components/Form/Tag/SeriesTagInput.tsx new file mode 100644 index 000000000..6d4beb20a --- /dev/null +++ b/frontend/src/Components/Form/Tag/SeriesTagInput.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import { addTag } from 'Store/Actions/tagActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import { InputChanged } from 'typings/inputs'; +import sortByProp from 'Utilities/Array/sortByProp'; +import TagInput, { TagBase } from './TagInput'; + +interface SeriesTag extends TagBase { + id: number; + name: string; +} + +interface SeriesTagInputProps { + name: string; + value: number | number[]; + onChange: (change: InputChanged<number | number[]>) => void; +} + +const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i'); + +function isValidTag(tagName: string) { + try { + return !VALID_TAG_REGEX.test(tagName); + } catch (e) { + return false; + } +} + +function createSeriesTagsSelector(tags: number[]) { + return createSelector(createTagsSelector(), (tagList) => { + const sortedTags = tagList.sort(sortByProp('label')); + const filteredTagList = sortedTags.filter((tag) => !tags.includes(tag.id)); + + return { + tags: tags.reduce((acc: SeriesTag[], tag) => { + const matchingTag = tagList.find((t) => t.id === tag); + + if (matchingTag) { + acc.push({ + id: tag, + name: matchingTag.label, + }); + } + + return acc; + }, []), + + tagList: filteredTagList.map(({ id, label: name }) => { + return { + id, + name, + }; + }), + + allTags: sortedTags, + }; + }); +} + +export default function SeriesTagInput({ + name, + value, + onChange, +}: SeriesTagInputProps) { + const dispatch = useDispatch(); + const isArray = Array.isArray(value); + + const arrayValue = useMemo(() => { + if (isArray) { + return value; + } + + return value === 0 ? [] : [value]; + }, [isArray, value]); + + const { tags, tagList, allTags } = useSelector( + createSeriesTagsSelector(arrayValue) + ); + + const handleTagCreated = useCallback( + (tag: SeriesTag) => { + if (isArray) { + onChange({ name, value: [...value, tag.id] }); + } else { + onChange({ + name, + value: tag.id, + }); + } + }, + [name, value, isArray, onChange] + ); + + const handleTagAdd = useCallback( + (newTag: SeriesTag) => { + if (newTag.id) { + if (isArray) { + onChange({ name, value: [...value, newTag.id] }); + } else { + onChange({ name, value: newTag.id }); + } + + return; + } + + const existingTag = allTags.some((t) => t.label === newTag.name); + + if (isValidTag(newTag.name) && !existingTag) { + dispatch( + addTag({ + tag: { label: newTag.name }, + onTagCreated: handleTagCreated, + }) + ); + } + }, + [name, value, isArray, allTags, handleTagCreated, onChange, dispatch] + ); + + const handleTagDelete = useCallback( + ({ index }: { index: number }) => { + if (isArray) { + const newValue = value.slice(); + newValue.splice(index, 1); + + onChange({ name, value: newValue }); + } else { + onChange({ name, value: 0 }); + } + }, + [name, value, isArray, onChange] + ); + + return ( + <TagInput + name={name} + tags={tags} + tagList={tagList} + onTagAdd={handleTagAdd} + onTagDelete={handleTagDelete} + /> + ); +} diff --git a/frontend/src/Components/Form/TagInput.css b/frontend/src/Components/Form/Tag/TagInput.css similarity index 74% rename from frontend/src/Components/Form/TagInput.css rename to frontend/src/Components/Form/Tag/TagInput.css index eeddab5b4..2ca02825e 100644 --- a/frontend/src/Components/Form/TagInput.css +++ b/frontend/src/Components/Form/Tag/TagInput.css @@ -1,5 +1,5 @@ .input { - composes: input from '~./AutoSuggestInput.css'; + composes: input from '~Components/Form/AutoSuggestInput.css'; padding: 0; min-height: 35px; @@ -8,7 +8,8 @@ &.isFocused { outline: 0; border-color: var(--inputFocusBorderColor); - box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), + 0 0 8px var(--inputFocusBoxShadowColor); } } diff --git a/frontend/src/Components/Form/TagInput.css.d.ts b/frontend/src/Components/Form/Tag/TagInput.css.d.ts similarity index 100% rename from frontend/src/Components/Form/TagInput.css.d.ts rename to frontend/src/Components/Form/Tag/TagInput.css.d.ts diff --git a/frontend/src/Components/Form/Tag/TagInput.tsx b/frontend/src/Components/Form/Tag/TagInput.tsx new file mode 100644 index 000000000..c113c06d3 --- /dev/null +++ b/frontend/src/Components/Form/Tag/TagInput.tsx @@ -0,0 +1,371 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { + KeyboardEvent, + Ref, + SyntheticEvent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { + ChangeEvent, + RenderInputComponentProps, + RenderSuggestion, + SuggestionsFetchRequestedParams, +} from 'react-autosuggest'; +import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback'; +import { kinds } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import tagShape from 'Helpers/Props/Shapes/tagShape'; +import { InputChanged } from 'typings/inputs'; +import AutoSuggestInput from '../AutoSuggestInput'; +import TagInputInput from './TagInputInput'; +import TagInputTag, { EditedTag, TagInputTagProps } from './TagInputTag'; +import styles from './TagInput.css'; + +export interface TagBase { + id: boolean | number | string; + name: string | number; +} + +function getTag<T extends { id: T['id']; name: T['name'] }>( + value: string, + selectedIndex: number, + suggestions: T[], + allowNew: boolean +) { + if (selectedIndex == null && value) { + const existingTag = suggestions.find( + (suggestion) => suggestion.name === value + ); + + if (existingTag) { + return existingTag; + } else if (allowNew) { + return { id: 0, name: value } as T; + } + } else if (selectedIndex != null) { + return suggestions[selectedIndex]; + } + + return null; +} + +function handleSuggestionsClearRequested() { + // Required because props aren't always rendered, but no-op + // because we don't want to reset the paths after a path is selected. +} + +export interface ReplacementTag<T extends TagBase> { + index: number; + id: T['id']; +} + +export interface TagInputProps<T extends TagBase> { + className?: string; + inputContainerClassName?: string; + name: string; + tags: T[]; + tagList: T[]; + allowNew?: boolean; + kind?: Kind; + placeholder?: string; + delimiters?: string[]; + minQueryLength?: number; + canEdit?: boolean; + hasError?: boolean; + hasWarning?: boolean; + tagComponent?: React.ElementType; + onChange?: (change: InputChanged<T['id'][]>) => void; + onTagAdd: (newTag: T) => void; + onTagDelete: TagInputTagProps<T>['onDelete']; + onTagReplace?: ( + tagToReplace: ReplacementTag<T>, + newTagName: T['name'] + ) => void; +} + +function TagInput<T extends TagBase>({ + className = styles.internalInput, + inputContainerClassName = styles.input, + name, + tags, + tagList, + allowNew = true, + kind = 'info', + placeholder = '', + delimiters = ['Tab', 'Enter', ' ', ','], + minQueryLength = 1, + canEdit = false, + tagComponent = TagInputTag, + hasError, + hasWarning, + onChange, + onTagAdd, + onTagDelete, + onTagReplace, + ...otherProps +}: TagInputProps<T>) { + const [value, setValue] = useState(''); + const [suggestions, setSuggestions] = useState<T[]>([]); + const [isFocused, setIsFocused] = useState(false); + const autoSuggestRef = useRef(null); + + const addTag = useDebouncedCallback( + (tag: T | null) => { + if (!tag) { + return; + } + + onTagAdd(tag); + + setValue(''); + setSuggestions([]); + }, + 250, + { + leading: true, + trailing: false, + } + ); + + const handleEditTag = useCallback( + ({ value: newValue, ...otherProps }: EditedTag<T>) => { + if (value && onTagReplace) { + onTagReplace(otherProps, value); + } else { + onTagDelete(otherProps); + } + + setValue(String(newValue)); + }, + [value, setValue, onTagDelete, onTagReplace] + ); + + const handleInputContainerPress = useCallback(() => { + // @ts-expect-error Ref isn't typed yet + autoSuggestRef?.current?.input.focus(); + }, []); + + const handleInputChange = useCallback( + (_event: SyntheticEvent, { newValue, method }: ChangeEvent) => { + const finalValue = + // @ts-expect-error newValue may be an object? + typeof newValue === 'object' ? newValue.name : newValue; + + if (method === 'type') { + setValue(finalValue); + } + }, + [setValue] + ); + + const handleSuggestionsFetchRequested = useCallback( + ({ value: newValue }: SuggestionsFetchRequestedParams) => { + const lowerCaseValue = newValue.toLowerCase(); + + const suggestions = tagList.filter((tag) => { + return ( + String(tag.name).toLowerCase().includes(lowerCaseValue) && + !tags.some((t) => t.id === tag.id) + ); + }); + + setSuggestions(suggestions); + }, + [tags, tagList, setSuggestions] + ); + + const handleInputKeyDown = useCallback( + (event: KeyboardEvent<HTMLElement>) => { + const key = event.key; + + if (!autoSuggestRef.current) { + return; + } + + if (key === 'Backspace' && !value.length) { + const index = tags.length - 1; + + if (index >= 0) { + onTagDelete({ index, id: tags[index].id }); + } + + setTimeout(() => { + handleSuggestionsFetchRequested({ + value: '', + reason: 'input-changed', + }); + }); + + event.preventDefault(); + } + + if (delimiters.includes(key)) { + // @ts-expect-error Ref isn't typed yet + const selectedIndex = autoSuggestRef.current.highlightedSuggestionIndex; + const tag = getTag<T>(value, selectedIndex, suggestions, allowNew); + + if (tag) { + addTag(tag); + event.preventDefault(); + } + } + }, + [ + tags, + allowNew, + delimiters, + onTagDelete, + value, + suggestions, + addTag, + handleSuggestionsFetchRequested, + ] + ); + + const handleInputFocus = useCallback(() => { + setIsFocused(true); + }, [setIsFocused]); + + const handleInputBlur = useCallback(() => { + setIsFocused(false); + + if (!autoSuggestRef.current) { + return; + } + + // @ts-expect-error Ref isn't typed yet + const selectedIndex = autoSuggestRef.current.highlightedSuggestionIndex; + const tag = getTag(value, selectedIndex, suggestions, allowNew); + + if (tag) { + addTag(tag); + } + }, [allowNew, value, suggestions, autoSuggestRef, addTag, setIsFocused]); + + const handleSuggestionSelected = useCallback( + (_event: SyntheticEvent, { suggestion }: { suggestion: T }) => { + addTag(suggestion); + }, + [addTag] + ); + + const getSuggestionValue = useCallback(({ name }: T): string => { + return String(name); + }, []); + + const shouldRenderSuggestions = useCallback( + (v: string) => { + return v.length >= minQueryLength; + }, + [minQueryLength] + ); + + const renderSuggestion: RenderSuggestion<T> = useCallback(({ name }: T) => { + return name; + }, []); + + const renderInputComponent = useCallback( + ( + inputProps: RenderInputComponentProps, + forwardedRef: Ref<HTMLDivElement> + ) => { + return ( + <TagInputInput + forwardedRef={forwardedRef} + tags={tags} + kind={kind} + inputProps={inputProps} + isFocused={isFocused} + canEdit={canEdit} + tagComponent={tagComponent} + onTagDelete={onTagDelete} + onTagEdit={handleEditTag} + onInputContainerPress={handleInputContainerPress} + /> + ); + }, + [ + tags, + kind, + canEdit, + isFocused, + tagComponent, + handleInputContainerPress, + handleEditTag, + onTagDelete, + ] + ); + + useEffect(() => { + return () => { + addTag.cancel(); + }; + }, [addTag]); + + return ( + <AutoSuggestInput + {...otherProps} + forwardedRef={autoSuggestRef} + className={className} + inputContainerClassName={classNames( + inputContainerClassName, + isFocused && styles.isFocused, + hasError && styles.hasError, + hasWarning && styles.hasWarning + )} + name={name} + value={value} + placeholder={placeholder} + suggestions={suggestions} + getSuggestionValue={getSuggestionValue} + shouldRenderSuggestions={shouldRenderSuggestions} + focusInputOnSuggestionClick={false} + renderSuggestion={renderSuggestion} + renderInputComponent={renderInputComponent} + onInputChange={handleInputChange} + onInputKeyDown={handleInputKeyDown} + onInputFocus={handleInputFocus} + onInputBlur={handleInputBlur} + onSuggestionSelected={handleSuggestionSelected} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={handleSuggestionsClearRequested} + /> + ); +} + +TagInput.propTypes = { + className: PropTypes.string, + inputContainerClassName: PropTypes.string, + tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, + tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, + allowNew: PropTypes.bool, + kind: PropTypes.oneOf(kinds.all), + placeholder: PropTypes.string, + delimiters: PropTypes.arrayOf(PropTypes.string), + minQueryLength: PropTypes.number, + canEdit: PropTypes.bool, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + tagComponent: PropTypes.elementType, + onTagAdd: PropTypes.func.isRequired, + onTagDelete: PropTypes.func.isRequired, + onTagReplace: PropTypes.func, +}; + +TagInput.defaultProps = { + className: styles.internalInput, + inputContainerClassName: styles.input, + allowNew: true, + kind: kinds.INFO, + placeholder: '', + delimiters: ['Tab', 'Enter', ' ', ','], + minQueryLength: 1, + canEdit: false, + tagComponent: TagInputTag, +}; + +export default TagInput; diff --git a/frontend/src/Components/Form/TagInputInput.css b/frontend/src/Components/Form/Tag/TagInputInput.css similarity index 100% rename from frontend/src/Components/Form/TagInputInput.css rename to frontend/src/Components/Form/Tag/TagInputInput.css diff --git a/frontend/src/Components/Form/TagInputInput.css.d.ts b/frontend/src/Components/Form/Tag/TagInputInput.css.d.ts similarity index 100% rename from frontend/src/Components/Form/TagInputInput.css.d.ts rename to frontend/src/Components/Form/Tag/TagInputInput.css.d.ts diff --git a/frontend/src/Components/Form/Tag/TagInputInput.tsx b/frontend/src/Components/Form/Tag/TagInputInput.tsx new file mode 100644 index 000000000..d181136b8 --- /dev/null +++ b/frontend/src/Components/Form/Tag/TagInputInput.tsx @@ -0,0 +1,71 @@ +import React, { MouseEvent, Ref, useCallback } from 'react'; +import { Kind } from 'Helpers/Props/kinds'; +import { TagBase } from './TagInput'; +import { TagInputTagProps } from './TagInputTag'; +import styles from './TagInputInput.css'; + +interface TagInputInputProps<T extends TagBase> { + forwardedRef?: Ref<HTMLDivElement>; + className?: string; + tags: TagBase[]; + inputProps: object; + kind: Kind; + isFocused: boolean; + canEdit: boolean; + tagComponent: React.ElementType; + onTagDelete: TagInputTagProps<T>['onDelete']; + onTagEdit: TagInputTagProps<T>['onEdit']; + onInputContainerPress: () => void; +} + +function TagInputInput<T extends TagBase>(props: TagInputInputProps<T>) { + const { + forwardedRef, + className = styles.inputContainer, + tags, + inputProps, + kind, + isFocused, + canEdit, + tagComponent: TagComponent, + onTagDelete, + onTagEdit, + onInputContainerPress, + } = props; + + const handleMouseDown = useCallback( + (event: MouseEvent<HTMLDivElement>) => { + event.preventDefault(); + + if (isFocused) { + return; + } + + onInputContainerPress(); + }, + [isFocused, onInputContainerPress] + ); + + return ( + <div ref={forwardedRef} className={className} onMouseDown={handleMouseDown}> + {tags.map((tag, index) => { + return ( + <TagComponent + key={tag.id} + index={index} + tag={tag} + kind={kind} + canEdit={canEdit} + isLastTag={index === tags.length - 1} + onDelete={onTagDelete} + onEdit={onTagEdit} + /> + ); + })} + + <input {...inputProps} /> + </div> + ); +} + +export default TagInputInput; diff --git a/frontend/src/Components/Form/TagInputTag.css b/frontend/src/Components/Form/Tag/TagInputTag.css similarity index 100% rename from frontend/src/Components/Form/TagInputTag.css rename to frontend/src/Components/Form/Tag/TagInputTag.css diff --git a/frontend/src/Components/Form/TagInputTag.css.d.ts b/frontend/src/Components/Form/Tag/TagInputTag.css.d.ts similarity index 100% rename from frontend/src/Components/Form/TagInputTag.css.d.ts rename to frontend/src/Components/Form/Tag/TagInputTag.css.d.ts diff --git a/frontend/src/Components/Form/Tag/TagInputTag.tsx b/frontend/src/Components/Form/Tag/TagInputTag.tsx new file mode 100644 index 000000000..484bf45e0 --- /dev/null +++ b/frontend/src/Components/Form/Tag/TagInputTag.tsx @@ -0,0 +1,79 @@ +import React, { useCallback } from 'react'; +import MiddleTruncate from 'react-middle-truncate'; +import Label, { LabelProps } from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import { TagBase } from './TagInput'; +import styles from './TagInputTag.css'; + +export interface DeletedTag<T extends TagBase> { + index: number; + id: T['id']; +} + +export interface EditedTag<T extends TagBase> { + index: number; + id: T['id']; + value: T['name']; +} + +export interface TagInputTagProps<T extends TagBase> { + index: number; + tag: T; + kind: LabelProps['kind']; + canEdit: boolean; + onDelete: (deletedTag: DeletedTag<T>) => void; + onEdit: (editedTag: EditedTag<T>) => void; +} + +function TagInputTag<T extends TagBase>({ + tag, + kind, + index, + canEdit, + onDelete, + onEdit, +}: TagInputTagProps<T>) { + const handleDelete = useCallback(() => { + onDelete({ + index, + id: tag.id, + }); + }, [index, tag, onDelete]); + + const handleEdit = useCallback(() => { + onEdit({ + index, + id: tag.id, + value: tag.name, + }); + }, [index, tag, onEdit]); + + return ( + <div className={styles.tag} tabIndex={-1}> + <Label className={styles.label} kind={kind}> + <Link + className={canEdit ? styles.linkWithEdit : styles.link} + tabIndex={-1} + onPress={handleDelete} + > + <MiddleTruncate text={String(tag.name)} start={10} end={10} /> + </Link> + + {canEdit ? ( + <div className={styles.editContainer}> + <IconButton + className={styles.editButton} + name={icons.EDIT} + size={9} + onPress={handleEdit} + /> + </div> + ) : null} + </Label> + </div> + ); +} + +export default TagInputTag; diff --git a/frontend/src/Components/Form/Tag/TagSelectInput.tsx b/frontend/src/Components/Form/Tag/TagSelectInput.tsx new file mode 100644 index 000000000..21fde893c --- /dev/null +++ b/frontend/src/Components/Form/Tag/TagSelectInput.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useMemo } from 'react'; +import { InputChanged } from 'typings/inputs'; +import TagInput, { TagBase, TagInputProps } from './TagInput'; + +interface SelectTag extends TagBase { + id: number; + name: string; +} + +interface TagSelectValue { + value: string; + key: number; + order: number; +} + +interface TagSelectInputProps extends TagInputProps<SelectTag> { + name: string; + value: number[]; + values: TagSelectValue[]; + onChange: (change: InputChanged<number | number[]>) => unknown; +} + +function TagSelectInput({ + name, + value, + values, + onChange, + ...otherProps +}: TagSelectInputProps) { + const { tags, tagList, allTags } = useMemo(() => { + const sortedTags = values.sort((a, b) => a.key - b.key); + + return { + tags: value.reduce((acc: SelectTag[], tag) => { + const matchingTag = values.find((t) => t.key === tag); + + if (matchingTag) { + acc.push({ + id: tag, + name: matchingTag.value, + }); + } + + return acc; + }, []), + + tagList: sortedTags.map((sorted) => { + return { + id: sorted.key, + name: sorted.value, + }; + }), + + allTags: sortedTags, + }; + }, [value, values]); + + const handleTagAdd = useCallback( + (newTag: SelectTag) => { + const existingTag = allTags.some((tag) => tag.key === newTag.id); + const newValue = value.slice(); + + if (existingTag) { + newValue.push(newTag.id); + } + + onChange({ name, value: newValue }); + }, + [name, value, allTags, onChange] + ); + + const handleTagDelete = useCallback( + ({ index }: { index: number }) => { + const newValue = value.slice(); + newValue.splice(index, 1); + + onChange({ + name, + value: newValue, + }); + }, + [name, value, onChange] + ); + + return ( + <TagInput + {...otherProps} + name={name} + tags={tags} + tagList={tagList} + onTagAdd={handleTagAdd} + onTagDelete={handleTagDelete} + /> + ); +} + +export default TagSelectInput; diff --git a/frontend/src/Components/Form/Tag/TextTagInput.tsx b/frontend/src/Components/Form/Tag/TextTagInput.tsx new file mode 100644 index 000000000..6e2082c50 --- /dev/null +++ b/frontend/src/Components/Form/Tag/TextTagInput.tsx @@ -0,0 +1,109 @@ +import React, { useCallback, useMemo } from 'react'; +import { InputChanged } from 'typings/inputs'; +import split from 'Utilities/String/split'; +import TagInput, { ReplacementTag, TagBase, TagInputProps } from './TagInput'; + +interface TextTag extends TagBase { + id: string; + name: string; +} + +interface TextTagInputProps extends TagInputProps<TextTag> { + name: string; + value: string | string[]; + onChange: (change: InputChanged<string[]>) => unknown; +} + +function TextTagInput({ + name, + value, + onChange, + ...otherProps +}: TextTagInputProps) { + const { tags, tagList, valueArray } = useMemo(() => { + const tagsArray = Array.isArray(value) ? value : split(value); + + return { + tags: tagsArray.reduce((result: TextTag[], tag) => { + if (tag) { + result.push({ + id: tag, + name: tag, + }); + } + + return result; + }, []), + tagList: [], + valueArray: tagsArray, + }; + }, [value]); + + const handleTagAdd = useCallback( + (newTag: TextTag) => { + // Split and trim tags before adding them to the list, this will + // cleanse tags pasted in that had commas and spaces which leads + // to oddities with restrictions (as an example). + + const newValue = [...valueArray]; + const newTags = newTag.name.startsWith('/') + ? [newTag.name] + : split(newTag.name); + + newTags.forEach((newTag) => { + const newTagValue = newTag.trim(); + + if (newTagValue) { + newValue.push(newTagValue); + } + }); + + onChange({ name, value: newValue }); + }, + [name, valueArray, onChange] + ); + + const handleTagDelete = useCallback( + ({ index }: { index: number }) => { + const newValue = [...valueArray]; + newValue.splice(index, 1); + + onChange({ + name, + value: newValue, + }); + }, + [name, valueArray, onChange] + ); + + const handleTagReplace = useCallback( + (tagToReplace: ReplacementTag<TextTag>, newTagName: string) => { + const newValue = [...valueArray]; + newValue.splice(tagToReplace.index, 1); + + const newTagValue = newTagName.trim(); + + if (newTagValue) { + newValue.push(newTagValue); + } + + onChange({ name, value: newValue }); + }, + [name, valueArray, onChange] + ); + + return ( + <TagInput + {...otherProps} + name={name} + delimiters={['Tab', 'Enter', ',']} + tags={tags} + tagList={tagList} + onTagAdd={handleTagAdd} + onTagDelete={handleTagDelete} + onTagReplace={handleTagReplace} + /> + ); +} + +export default TextTagInput; diff --git a/frontend/src/Components/Form/TagInput.js b/frontend/src/Components/Form/TagInput.js deleted file mode 100644 index 840d627f8..000000000 --- a/frontend/src/Components/Form/TagInput.js +++ /dev/null @@ -1,301 +0,0 @@ -import classNames from 'classnames'; -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { kinds } from 'Helpers/Props'; -import tagShape from 'Helpers/Props/Shapes/tagShape'; -import AutoSuggestInput from './AutoSuggestInput'; -import TagInputInput from './TagInputInput'; -import TagInputTag from './TagInputTag'; -import styles from './TagInput.css'; - -function getTag(value, selectedIndex, suggestions, allowNew) { - if (selectedIndex == null && value) { - const existingTag = suggestions.find((suggestion) => suggestion.name === value); - - if (existingTag) { - return existingTag; - } else if (allowNew) { - return { name: value }; - } - } else if (selectedIndex != null) { - return suggestions[selectedIndex]; - } -} - -class TagInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - value: '', - suggestions: [], - isFocused: false - }; - - this._autosuggestRef = null; - } - - componentWillUnmount() { - this.addTag.cancel(); - } - - // - // Control - - _setAutosuggestRef = (ref) => { - this._autosuggestRef = ref; - }; - - getSuggestionValue({ name }) { - return name; - } - - shouldRenderSuggestions = (value) => { - return value.length >= this.props.minQueryLength; - }; - - renderSuggestion({ name }) { - return name; - } - - addTag = _.debounce((tag) => { - this.props.onTagAdd(tag); - - this.setState({ - value: '', - suggestions: [] - }); - }, 250, { leading: true, trailing: false }); - - // - // Listeners - - onTagEdit = ({ value, ...otherProps }) => { - const currentValue = this.state.value; - - if (currentValue && this.props.onTagReplace) { - this.props.onTagReplace(otherProps, { name: currentValue }); - } else { - this.props.onTagDelete(otherProps); - } - - this.setState({ value }); - }; - - onInputContainerPress = () => { - this._autosuggestRef.input.focus(); - }; - - onInputChange = (event, { newValue, method }) => { - const value = _.isObject(newValue) ? newValue.name : newValue; - - if (method === 'type') { - this.setState({ value }); - } - }; - - onInputKeyDown = (event) => { - const { - tags, - allowNew, - delimiters, - onTagDelete - } = this.props; - - const { - value, - suggestions - } = this.state; - - const key = event.key; - - if (key === 'Backspace' && !value.length) { - const index = tags.length - 1; - - if (index >= 0) { - onTagDelete({ index, id: tags[index].id }); - } - - setTimeout(() => { - this.onSuggestionsFetchRequested({ value: '' }); - }); - - event.preventDefault(); - } - - if (delimiters.includes(key)) { - const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex; - const tag = getTag(value, selectedIndex, suggestions, allowNew); - - if (tag) { - this.addTag(tag); - event.preventDefault(); - } - } - }; - - onInputFocus = () => { - this.setState({ isFocused: true }); - }; - - onInputBlur = () => { - this.setState({ isFocused: false }); - - if (!this._autosuggestRef) { - return; - } - - const { - allowNew - } = this.props; - - const { - value, - suggestions - } = this.state; - - const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex; - const tag = getTag(value, selectedIndex, suggestions, allowNew); - - if (tag) { - this.addTag(tag); - } - }; - - onSuggestionsFetchRequested = ({ value }) => { - const lowerCaseValue = value.toLowerCase(); - - const { - tags, - tagList - } = this.props; - - const suggestions = tagList.filter((tag) => { - return ( - tag.name.toLowerCase().includes(lowerCaseValue) && - !tags.some((t) => t.id === tag.id)); - }); - - this.setState({ suggestions }); - }; - - onSuggestionsClearRequested = () => { - // Required because props aren't always rendered, but no-op - // because we don't want to reset the paths after a path is selected. - }; - - onSuggestionSelected = (event, { suggestion }) => { - this.addTag(suggestion); - }; - - // - // Render - - renderInputComponent = (inputProps, forwardedRef) => { - const { - tags, - kind, - canEdit, - tagComponent, - onTagDelete - } = this.props; - - return ( - <TagInputInput - forwardedRef={forwardedRef} - tags={tags} - kind={kind} - inputProps={inputProps} - isFocused={this.state.isFocused} - canEdit={canEdit} - tagComponent={tagComponent} - onTagDelete={onTagDelete} - onTagEdit={this.onTagEdit} - onInputContainerPress={this.onInputContainerPress} - /> - ); - }; - - render() { - const { - className, - inputContainerClassName, - hasError, - hasWarning, - ...otherProps - } = this.props; - - const { - value, - suggestions, - isFocused - } = this.state; - - return ( - <AutoSuggestInput - {...otherProps} - forwardedRef={this._setAutosuggestRef} - className={className} - inputContainerClassName={classNames( - inputContainerClassName, - isFocused && styles.isFocused, - hasError && styles.hasError, - hasWarning && styles.hasWarning - )} - value={value} - suggestions={suggestions} - getSuggestionValue={this.getSuggestionValue} - shouldRenderSuggestions={this.shouldRenderSuggestions} - focusInputOnSuggestionClick={false} - renderSuggestion={this.renderSuggestion} - renderInputComponent={this.renderInputComponent} - onInputChange={this.onInputChange} - onInputKeyDown={this.onInputKeyDown} - onInputFocus={this.onInputFocus} - onInputBlur={this.onInputBlur} - onSuggestionSelected={this.onSuggestionSelected} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onChange={this.onInputChange} - /> - ); - } -} - -TagInput.propTypes = { - className: PropTypes.string.isRequired, - inputContainerClassName: PropTypes.string.isRequired, - tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, - tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, - allowNew: PropTypes.bool.isRequired, - kind: PropTypes.oneOf(kinds.all).isRequired, - placeholder: PropTypes.string.isRequired, - delimiters: PropTypes.arrayOf(PropTypes.string).isRequired, - minQueryLength: PropTypes.number.isRequired, - canEdit: PropTypes.bool, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - tagComponent: PropTypes.elementType.isRequired, - onTagAdd: PropTypes.func.isRequired, - onTagDelete: PropTypes.func.isRequired, - onTagReplace: PropTypes.func -}; - -TagInput.defaultProps = { - className: styles.internalInput, - inputContainerClassName: styles.input, - allowNew: true, - kind: kinds.INFO, - placeholder: '', - delimiters: ['Tab', 'Enter', ' ', ','], - minQueryLength: 1, - canEdit: false, - tagComponent: TagInputTag -}; - -export default TagInput; diff --git a/frontend/src/Components/Form/TagInputConnector.js b/frontend/src/Components/Form/TagInputConnector.js deleted file mode 100644 index 8d0782fa5..000000000 --- a/frontend/src/Components/Form/TagInputConnector.js +++ /dev/null @@ -1,157 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { addTag } from 'Store/Actions/tagActions'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import TagInput from './TagInput'; - -const validTagRegex = new RegExp('[^-_a-z0-9]', 'i'); - -function isValidTag(tagName) { - try { - return !validTagRegex.test(tagName); - } catch (e) { - return false; - } -} - -function createMapStateToProps() { - return createSelector( - (state, { value }) => value, - createTagsSelector(), - (tags, tagList) => { - const sortedTags = _.sortBy(tagList, 'label'); - const filteredTagList = _.filter(sortedTags, (tag) => _.indexOf(tags, tag.id) === -1); - - return { - tags: tags.reduce((acc, tag) => { - const matchingTag = _.find(tagList, { id: tag }); - - if (matchingTag) { - acc.push({ - id: tag, - name: matchingTag.label - }); - } - - return acc; - }, []), - - tagList: filteredTagList.map(({ id, label: name }) => { - return { - id, - name - }; - }), - - allTags: sortedTags - }; - } - ); -} - -const mapDispatchToProps = { - addTag -}; - -class TagInputConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - name, - value, - tags, - onChange - } = this.props; - - if (value.length !== tags.length) { - onChange({ name, value: tags.map((tag) => tag.id) }); - } - } - - // - // Listeners - - onTagAdd = (tag) => { - const { - name, - value, - allTags - } = this.props; - - if (!tag.id) { - const existingTag =_.some(allTags, { label: tag.name }); - - if (isValidTag(tag.name) && !existingTag) { - this.props.addTag({ - tag: { label: tag.name }, - onTagCreated: this.onTagCreated - }); - } - - return; - } - - const newValue = value.slice(); - newValue.push(tag.id); - - this.props.onChange({ name, value: newValue }); - }; - - onTagDelete = ({ index }) => { - const { - name, - value - } = this.props; - - const newValue = value.slice(); - newValue.splice(index, 1); - - this.props.onChange({ - name, - value: newValue - }); - }; - - onTagCreated = (tag) => { - const { - name, - value - } = this.props; - - const newValue = value.slice(); - newValue.push(tag.id); - - this.props.onChange({ name, value: newValue }); - }; - - // - // Render - - render() { - return ( - <TagInput - onTagAdd={this.onTagAdd} - onTagDelete={this.onTagDelete} - onTagReplace={this.onTagReplace} - {...this.props} - /> - ); - } -} - -TagInputConnector.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.number).isRequired, - tags: PropTypes.arrayOf(PropTypes.object).isRequired, - allTags: PropTypes.arrayOf(PropTypes.object).isRequired, - onChange: PropTypes.func.isRequired, - addTag: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(TagInputConnector); diff --git a/frontend/src/Components/Form/TagInputInput.js b/frontend/src/Components/Form/TagInputInput.js deleted file mode 100644 index 86628b134..000000000 --- a/frontend/src/Components/Form/TagInputInput.js +++ /dev/null @@ -1,84 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { kinds } from 'Helpers/Props'; -import tagShape from 'Helpers/Props/Shapes/tagShape'; -import styles from './TagInputInput.css'; - -class TagInputInput extends Component { - - onMouseDown = (event) => { - event.preventDefault(); - - const { - isFocused, - onInputContainerPress - } = this.props; - - if (isFocused) { - return; - } - - onInputContainerPress(); - }; - - render() { - const { - forwardedRef, - className, - tags, - inputProps, - kind, - canEdit, - tagComponent: TagComponent, - onTagDelete, - onTagEdit - } = this.props; - - return ( - <div - ref={forwardedRef} - className={className} - onMouseDown={this.onMouseDown} - > - { - tags.map((tag, index) => { - return ( - <TagComponent - key={tag.id} - index={index} - tag={tag} - kind={kind} - canEdit={canEdit} - isLastTag={index === tags.length - 1} - onDelete={onTagDelete} - onEdit={onTagEdit} - /> - ); - }) - } - - <input {...inputProps} /> - </div> - ); - } -} - -TagInputInput.propTypes = { - forwardedRef: PropTypes.func, - className: PropTypes.string.isRequired, - tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, - inputProps: PropTypes.object.isRequired, - kind: PropTypes.oneOf(kinds.all).isRequired, - isFocused: PropTypes.bool.isRequired, - canEdit: PropTypes.bool.isRequired, - tagComponent: PropTypes.elementType.isRequired, - onTagDelete: PropTypes.func.isRequired, - onTagEdit: PropTypes.func.isRequired, - onInputContainerPress: PropTypes.func.isRequired -}; - -TagInputInput.defaultProps = { - className: styles.inputContainer -}; - -export default TagInputInput; diff --git a/frontend/src/Components/Form/TagInputTag.js b/frontend/src/Components/Form/TagInputTag.js deleted file mode 100644 index 05a780442..000000000 --- a/frontend/src/Components/Form/TagInputTag.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import MiddleTruncate from 'react-middle-truncate'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import { icons, kinds } from 'Helpers/Props'; -import tagShape from 'Helpers/Props/Shapes/tagShape'; -import styles from './TagInputTag.css'; - -class TagInputTag extends Component { - - // - // Listeners - - onDelete = () => { - const { - index, - tag, - onDelete - } = this.props; - - onDelete({ - index, - id: tag.id - }); - }; - - onEdit = () => { - const { - index, - tag, - onEdit - } = this.props; - - onEdit({ - index, - id: tag.id, - value: tag.name - }); - }; - - // - // Render - - render() { - const { - tag, - kind, - canEdit - } = this.props; - - return ( - <div - className={styles.tag} - tabIndex={-1} - > - <Label - className={styles.label} - kind={kind} - > - <Link - className={canEdit ? styles.linkWithEdit : styles.link} - tabIndex={-1} - onPress={this.onDelete} - > - <MiddleTruncate - text={tag.name} - start={10} - end={10} - /> - </Link> - - { - canEdit ? - <div className={styles.editContainer}> - <IconButton - className={styles.editButton} - name={icons.EDIT} - size={9} - onPress={this.onEdit} - /> - </div> : - null - } - </Label> - </div> - ); - } -} - -TagInputTag.propTypes = { - index: PropTypes.number.isRequired, - tag: PropTypes.shape(tagShape), - kind: PropTypes.oneOf(kinds.all).isRequired, - canEdit: PropTypes.bool.isRequired, - onDelete: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired -}; - -export default TagInputTag; diff --git a/frontend/src/Components/Form/TagSelectInputConnector.js b/frontend/src/Components/Form/TagSelectInputConnector.js deleted file mode 100644 index 23afe6da1..000000000 --- a/frontend/src/Components/Form/TagSelectInputConnector.js +++ /dev/null @@ -1,102 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import TagInput from './TagInput'; - -function createMapStateToProps() { - return createSelector( - (state, { value }) => value, - (state, { values }) => values, - (tags, tagList) => { - const sortedTags = _.sortBy(tagList, 'value'); - - return { - tags: tags.reduce((acc, tag) => { - const matchingTag = _.find(tagList, { key: tag }); - - if (matchingTag) { - acc.push({ - id: tag, - name: matchingTag.value - }); - } - - return acc; - }, []), - - tagList: sortedTags.map(({ key: id, value: name }) => { - return { - id, - name - }; - }), - - allTags: sortedTags - }; - } - ); -} - -class TagSelectInputConnector extends Component { - - // - // Listeners - - onTagAdd = (tag) => { - const { - name, - value, - allTags - } = this.props; - - const existingTag =_.some(allTags, { key: tag.id }); - - const newValue = value.slice(); - - if (existingTag) { - newValue.push(tag.id); - } - - this.props.onChange({ name, value: newValue }); - }; - - onTagDelete = ({ index }) => { - const { - name, - value - } = this.props; - - const newValue = value.slice(); - newValue.splice(index, 1); - - this.props.onChange({ - name, - value: newValue - }); - }; - - // - // Render - - render() { - return ( - <TagInput - onTagAdd={this.onTagAdd} - onTagDelete={this.onTagDelete} - {...this.props} - /> - ); - } -} - -TagSelectInputConnector.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.number).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - allTags: PropTypes.arrayOf(PropTypes.object).isRequired, - onChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps)(TagSelectInputConnector); diff --git a/frontend/src/Components/Form/TextArea.js b/frontend/src/Components/Form/TextArea.js deleted file mode 100644 index 44fd3a249..000000000 --- a/frontend/src/Components/Form/TextArea.js +++ /dev/null @@ -1,172 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './TextArea.css'; - -class TextArea extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._input = null; - this._selectionStart = null; - this._selectionEnd = null; - this._selectionTimeout = null; - this._isMouseTarget = false; - } - - componentDidMount() { - window.addEventListener('mouseup', this.onDocumentMouseUp); - } - - componentWillUnmount() { - window.removeEventListener('mouseup', this.onDocumentMouseUp); - - if (this._selectionTimeout) { - this._selectionTimeout = clearTimeout(this._selectionTimeout); - } - } - - // - // Control - - setInputRef = (ref) => { - this._input = ref; - }; - - selectionChange() { - if (this._selectionTimeout) { - this._selectionTimeout = clearTimeout(this._selectionTimeout); - } - - this._selectionTimeout = setTimeout(() => { - const selectionStart = this._input.selectionStart; - const selectionEnd = this._input.selectionEnd; - - const selectionChanged = ( - this._selectionStart !== selectionStart || - this._selectionEnd !== selectionEnd - ); - - this._selectionStart = selectionStart; - this._selectionEnd = selectionEnd; - - if (this.props.onSelectionChange && selectionChanged) { - this.props.onSelectionChange(selectionStart, selectionEnd); - } - }, 10); - } - - // - // Listeners - - onChange = (event) => { - const { - name, - onChange - } = this.props; - - const payload = { - name, - value: event.target.value - }; - - onChange(payload); - }; - - onFocus = (event) => { - if (this.props.onFocus) { - this.props.onFocus(event); - } - - this.selectionChange(); - }; - - onKeyUp = () => { - this.selectionChange(); - }; - - onMouseDown = () => { - this._isMouseTarget = true; - }; - - onMouseUp = () => { - this.selectionChange(); - }; - - onDocumentMouseUp = () => { - if (this._isMouseTarget) { - this.selectionChange(); - } - - this._isMouseTarget = false; - }; - - // - // Render - - render() { - const { - className, - readOnly, - autoFocus, - placeholder, - name, - value, - hasError, - hasWarning, - onBlur - } = this.props; - - return ( - <textarea - ref={this.setInputRef} - readOnly={readOnly} - autoFocus={autoFocus} - placeholder={placeholder} - className={classNames( - className, - readOnly && styles.readOnly, - hasError && styles.hasError, - hasWarning && styles.hasWarning - )} - name={name} - value={value} - onChange={this.onChange} - onFocus={this.onFocus} - onBlur={onBlur} - onKeyUp={this.onKeyUp} - onMouseDown={this.onMouseDown} - onMouseUp={this.onMouseUp} - /> - ); - } -} - -TextArea.propTypes = { - className: PropTypes.string.isRequired, - readOnly: PropTypes.bool, - autoFocus: PropTypes.bool, - placeholder: PropTypes.string, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]).isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - onSelectionChange: PropTypes.func -}; - -TextArea.defaultProps = { - className: styles.input, - type: 'text', - readOnly: false, - autoFocus: false, - value: '' -}; - -export default TextArea; diff --git a/frontend/src/Components/Form/TextArea.tsx b/frontend/src/Components/Form/TextArea.tsx new file mode 100644 index 000000000..f37d5cb5f --- /dev/null +++ b/frontend/src/Components/Form/TextArea.tsx @@ -0,0 +1,143 @@ +import classNames from 'classnames'; +import React, { + ChangeEvent, + SyntheticEvent, + useCallback, + useEffect, + useRef, +} from 'react'; +import { InputChanged } from 'typings/inputs'; +import styles from './TextArea.css'; + +interface TextAreaProps { + className?: string; + readOnly?: boolean; + autoFocus?: boolean; + placeholder?: string; + name: string; + value?: string; + hasError?: boolean; + hasWarning?: boolean; + onChange: (change: InputChanged<string>) => void; + onFocus?: (event: SyntheticEvent) => void; + onBlur?: (event: SyntheticEvent) => void; + onSelectionChange?: (start: number | null, end: number | null) => void; +} + +function TextArea({ + className = styles.input, + readOnly = false, + autoFocus = false, + placeholder, + name, + value = '', + hasError, + hasWarning, + onBlur, + onFocus, + onChange, + onSelectionChange, +}: TextAreaProps) { + const inputRef = useRef<HTMLTextAreaElement>(null); + const selectionTimeout = useRef<ReturnType<typeof setTimeout>>(); + const selectionStart = useRef<number | null>(); + const selectionEnd = useRef<number | null>(); + const isMouseTarget = useRef(false); + + const selectionChanged = useCallback(() => { + if (selectionTimeout.current) { + clearTimeout(selectionTimeout.current); + } + + selectionTimeout.current = setTimeout(() => { + if (!inputRef.current) { + return; + } + + const start = inputRef.current.selectionStart; + const end = inputRef.current.selectionEnd; + + const selectionChanged = + selectionStart.current !== start || selectionEnd.current !== end; + + selectionStart.current = start; + selectionEnd.current = end; + + if (selectionChanged) { + onSelectionChange?.(start, end); + } + }, 10); + }, [onSelectionChange]); + + const handleChange = useCallback( + (event: ChangeEvent<HTMLTextAreaElement>) => { + onChange({ + name, + value: event.target.value, + }); + }, + [name, onChange] + ); + + const handleFocus = useCallback( + (event: SyntheticEvent) => { + onFocus?.(event); + + selectionChanged(); + }, + [selectionChanged, onFocus] + ); + + const handleKeyUp = useCallback(() => { + selectionChanged(); + }, [selectionChanged]); + + const handleMouseDown = useCallback(() => { + isMouseTarget.current = true; + }, []); + + const handleMouseUp = useCallback(() => { + selectionChanged(); + }, [selectionChanged]); + + const handleDocumentMouseUp = useCallback(() => { + if (isMouseTarget.current) { + selectionChanged(); + } + + isMouseTarget.current = false; + }, [selectionChanged]); + + useEffect(() => { + window.addEventListener('mouseup', handleDocumentMouseUp); + + return () => { + window.removeEventListener('mouseup', handleDocumentMouseUp); + }; + }, [handleDocumentMouseUp]); + + return ( + <textarea + ref={inputRef} + readOnly={readOnly} + autoFocus={autoFocus} + placeholder={placeholder} + className={classNames( + className, + readOnly && styles.readOnly, + hasError && styles.hasError, + hasWarning && styles.hasWarning + )} + name={name} + value={value} + onChange={handleChange} + onFocus={handleFocus} + onBlur={onBlur} + onKeyUp={handleKeyUp} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + /> + ); +} + +export default TextArea; diff --git a/frontend/src/Components/Form/TextInput.js b/frontend/src/Components/Form/TextInput.js deleted file mode 100644 index e018dd5a3..000000000 --- a/frontend/src/Components/Form/TextInput.js +++ /dev/null @@ -1,205 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './TextInput.css'; - -class TextInput extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._input = null; - this._selectionStart = null; - this._selectionEnd = null; - this._selectionTimeout = null; - this._isMouseTarget = false; - } - - componentDidMount() { - window.addEventListener('mouseup', this.onDocumentMouseUp); - } - - componentWillUnmount() { - window.removeEventListener('mouseup', this.onDocumentMouseUp); - - if (this._selectionTimeout) { - this._selectionTimeout = clearTimeout(this._selectionTimeout); - } - } - - // - // Control - - setInputRef = (ref) => { - this._input = ref; - }; - - selectionChange() { - if (this._selectionTimeout) { - this._selectionTimeout = clearTimeout(this._selectionTimeout); - } - - this._selectionTimeout = setTimeout(() => { - const selectionStart = this._input.selectionStart; - const selectionEnd = this._input.selectionEnd; - - const selectionChanged = ( - this._selectionStart !== selectionStart || - this._selectionEnd !== selectionEnd - ); - - this._selectionStart = selectionStart; - this._selectionEnd = selectionEnd; - - if (this.props.onSelectionChange && selectionChanged) { - this.props.onSelectionChange(selectionStart, selectionEnd); - } - }, 10); - } - - // - // Listeners - - onChange = (event) => { - const { - name, - type, - onChange - } = this.props; - - const payload = { - name, - value: event.target.value - }; - - // Also return the files for a file input type. - - if (type === 'file') { - payload.files = event.target.files; - } - - onChange(payload); - }; - - onFocus = (event) => { - if (this.props.onFocus) { - this.props.onFocus(event); - } - - this.selectionChange(); - }; - - onKeyUp = () => { - this.selectionChange(); - }; - - onMouseDown = () => { - this._isMouseTarget = true; - }; - - onMouseUp = () => { - this.selectionChange(); - }; - - onDocumentMouseUp = () => { - if (this._isMouseTarget) { - this.selectionChange(); - } - - this._isMouseTarget = false; - }; - - onWheel = () => { - if (this.props.type === 'number') { - this._input.blur(); - } - }; - - // - // Render - - render() { - const { - className, - type, - readOnly, - autoFocus, - placeholder, - name, - value, - hasError, - hasWarning, - hasButton, - step, - min, - max, - onBlur, - onCopy - } = this.props; - - return ( - <input - ref={this.setInputRef} - type={type} - readOnly={readOnly} - autoFocus={autoFocus} - placeholder={placeholder} - className={classNames( - className, - readOnly && styles.readOnly, - hasError && styles.hasError, - hasWarning && styles.hasWarning, - hasButton && styles.hasButton - )} - name={name} - value={value} - step={step} - min={min} - max={max} - onChange={this.onChange} - onFocus={this.onFocus} - onBlur={onBlur} - onCopy={onCopy} - onCut={onCopy} - onKeyUp={this.onKeyUp} - onMouseDown={this.onMouseDown} - onMouseUp={this.onMouseUp} - onWheel={this.onWheel} - /> - ); - } -} - -TextInput.propTypes = { - className: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - readOnly: PropTypes.bool, - autoFocus: PropTypes.bool, - placeholder: PropTypes.string, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]).isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - hasButton: PropTypes.bool, - step: PropTypes.number, - min: PropTypes.number, - max: PropTypes.number, - onChange: PropTypes.func.isRequired, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - onCopy: PropTypes.func, - onSelectionChange: PropTypes.func -}; - -TextInput.defaultProps = { - className: styles.input, - type: 'text', - readOnly: false, - autoFocus: false, - value: '' -}; - -export default TextInput; diff --git a/frontend/src/Components/Form/TextInput.tsx b/frontend/src/Components/Form/TextInput.tsx new file mode 100644 index 000000000..007651d30 --- /dev/null +++ b/frontend/src/Components/Form/TextInput.tsx @@ -0,0 +1,177 @@ +import classNames from 'classnames'; +import React, { + ChangeEvent, + SyntheticEvent, + useCallback, + useEffect, + useRef, +} from 'react'; +import { InputType } from 'Helpers/Props/inputTypes'; +import { FileInputChanged, InputChanged } from 'typings/inputs'; +import styles from './TextInput.css'; + +export interface TextInputProps<T> { + className?: string; + type?: InputType; + readOnly?: boolean; + autoFocus?: boolean; + placeholder?: string; + name: string; + value: string | number | string[]; + hasError?: boolean; + hasWarning?: boolean; + hasButton?: boolean; + step?: number; + min?: number; + max?: number; + onChange: (change: InputChanged<T> | FileInputChanged) => void; + onFocus?: (event: SyntheticEvent) => void; + onBlur?: (event: SyntheticEvent) => void; + onCopy?: (event: SyntheticEvent) => void; + onSelectionChange?: (start: number | null, end: number | null) => void; +} + +function TextInput<T>({ + className = styles.input, + type = 'text', + readOnly = false, + autoFocus = false, + placeholder, + name, + value = '', + hasError, + hasWarning, + hasButton, + step, + min, + max, + onBlur, + onFocus, + onCopy, + onChange, + onSelectionChange, +}: TextInputProps<T>) { + const inputRef = useRef<HTMLInputElement>(null); + const selectionTimeout = useRef<ReturnType<typeof setTimeout>>(); + const selectionStart = useRef<number | null>(); + const selectionEnd = useRef<number | null>(); + const isMouseTarget = useRef(false); + + const selectionChanged = useCallback(() => { + if (selectionTimeout.current) { + clearTimeout(selectionTimeout.current); + } + + selectionTimeout.current = setTimeout(() => { + if (!inputRef.current) { + return; + } + + const start = inputRef.current.selectionStart; + const end = inputRef.current.selectionEnd; + + const selectionChanged = + selectionStart.current !== start || selectionEnd.current !== end; + + selectionStart.current = start; + selectionEnd.current = end; + + if (selectionChanged) { + onSelectionChange?.(start, end); + } + }, 10); + }, [onSelectionChange]); + + const handleChange = useCallback( + (event: ChangeEvent<HTMLInputElement>) => { + onChange({ + name, + value: event.target.value, + files: type === 'file' ? event.target.files : undefined, + }); + }, + [name, type, onChange] + ); + + const handleFocus = useCallback( + (event: SyntheticEvent) => { + onFocus?.(event); + + selectionChanged(); + }, + [selectionChanged, onFocus] + ); + + const handleKeyUp = useCallback(() => { + selectionChanged(); + }, [selectionChanged]); + + const handleMouseDown = useCallback(() => { + isMouseTarget.current = true; + }, []); + + const handleMouseUp = useCallback(() => { + selectionChanged(); + }, [selectionChanged]); + + const handleWheel = useCallback(() => { + if (type === 'number') { + inputRef.current?.blur(); + } + }, [type]); + + const handleDocumentMouseUp = useCallback(() => { + if (isMouseTarget.current) { + selectionChanged(); + } + + isMouseTarget.current = false; + }, [selectionChanged]); + + useEffect(() => { + window.addEventListener('mouseup', handleDocumentMouseUp); + + return () => { + window.removeEventListener('mouseup', handleDocumentMouseUp); + }; + }, [handleDocumentMouseUp]); + + useEffect(() => { + return () => { + clearTimeout(selectionTimeout.current); + }; + }, []); + + return ( + <input + ref={inputRef} + type={type} + readOnly={readOnly} + autoFocus={autoFocus} + placeholder={placeholder} + className={classNames( + className, + readOnly && styles.readOnly, + hasError && styles.hasError, + hasWarning && styles.hasWarning, + hasButton && styles.hasButton + )} + name={name} + value={value} + step={step} + min={min} + max={max} + onChange={handleChange} + onFocus={handleFocus} + onBlur={onBlur} + onCopy={onCopy} + onCut={onCopy} + onKeyUp={handleKeyUp} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + onWheel={handleWheel} + /> + ); +} + +export default TextInput; diff --git a/frontend/src/Components/Form/TextTagInputConnector.js b/frontend/src/Components/Form/TextTagInputConnector.js deleted file mode 100644 index be5abb16e..000000000 --- a/frontend/src/Components/Form/TextTagInputConnector.js +++ /dev/null @@ -1,120 +0,0 @@ - -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import split from 'Utilities/String/split'; -import TagInput from './TagInput'; - -function createMapStateToProps() { - return createSelector( - (state, { value }) => value, - (tags) => { - const tagsArray = Array.isArray(tags) ? tags : split(tags); - - return { - tags: tagsArray.reduce((result, tag) => { - if (tag) { - result.push({ - id: tag, - name: tag - }); - } - - return result; - }, []), - valueArray: tagsArray - }; - } - ); -} - -class TextTagInputConnector extends Component { - - // - // Listeners - - onTagAdd = (tag) => { - const { - name, - valueArray, - onChange - } = this.props; - - // Split and trim tags before adding them to the list, this will - // cleanse tags pasted in that had commas and spaces which leads - // to oddities with restrictions (as an example). - - const newValue = [...valueArray]; - const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name); - - newTags.forEach((newTag) => { - const newTagValue = newTag.trim(); - - if (newTagValue) { - newValue.push(newTagValue); - } - }); - - onChange({ name, value: newValue }); - }; - - onTagDelete = ({ index }) => { - const { - name, - valueArray, - onChange - } = this.props; - - const newValue = [...valueArray]; - newValue.splice(index, 1); - - onChange({ - name, - value: newValue - }); - }; - - onTagReplace = (tagToReplace, newTag) => { - const { - name, - valueArray, - onChange - } = this.props; - - const newValue = [...valueArray]; - newValue.splice(tagToReplace.index, 1); - - const newTagValue = newTag.name.trim(); - - if (newTagValue) { - newValue.push(newTagValue); - } - - onChange({ name, value: newValue }); - }; - - // - // Render - - render() { - return ( - <TagInput - delimiters={['Tab', 'Enter', ',']} - tagList={[]} - onTagAdd={this.onTagAdd} - onTagDelete={this.onTagDelete} - onTagReplace={this.onTagReplace} - {...this.props} - /> - ); - } -} - -TextTagInputConnector.propTypes = { - name: PropTypes.string.isRequired, - valueArray: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, null)(TextTagInputConnector); diff --git a/frontend/src/Components/Form/UMaskInput.js b/frontend/src/Components/Form/UMaskInput.js deleted file mode 100644 index 544865197..000000000 --- a/frontend/src/Components/Form/UMaskInput.js +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable no-bitwise */ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; -import styles from './UMaskInput.css'; - -const umaskOptions = [ - { - key: '755', - get value() { - return translate('Umask755Description', { octal: '755' }); - }, - hint: 'drwxr-xr-x' - }, - { - key: '775', - get value() { - return translate('Umask775Description', { octal: '775' }); - }, - hint: 'drwxrwxr-x' - }, - { - key: '770', - get value() { - return translate('Umask770Description', { octal: '770' }); - }, - hint: 'drwxrwx---' - }, - { - key: '750', - get value() { - return translate('Umask750Description', { octal: '750' }); - }, - hint: 'drwxr-x---' - }, - { - key: '777', - get value() { - return translate('Umask777Description', { octal: '777' }); - }, - hint: 'drwxrwxrwx' - } -]; - -function formatPermissions(permissions) { - - const hasSticky = permissions & 0o1000; - const hasSetGID = permissions & 0o2000; - const hasSetUID = permissions & 0o4000; - - let result = ''; - - for (let i = 0; i < 9; i++) { - const bit = (permissions & (1 << i)) !== 0; - let digit = bit ? 'xwr'[i % 3] : '-'; - if (i === 6 && hasSetUID) { - digit = bit ? 's' : 'S'; - } else if (i === 3 && hasSetGID) { - digit = bit ? 's' : 'S'; - } else if (i === 0 && hasSticky) { - digit = bit ? 't' : 'T'; - } - result = digit + result; - } - - return result; -} - -class UMaskInput extends Component { - - // - // Render - - render() { - const { - name, - value, - onChange - } = this.props; - - const valueNum = parseInt(value, 8); - const umaskNum = 0o777 & ~valueNum; - const umask = umaskNum.toString(8).padStart(4, '0'); - const folderNum = 0o777 & ~umaskNum; - const folder = folderNum.toString(8).padStart(3, '0'); - const fileNum = 0o666 & ~umaskNum; - const file = fileNum.toString(8).padStart(3, '0'); - - const unit = formatPermissions(folderNum); - - const values = umaskOptions.map((v) => { - return { ...v, hint: <span className={styles.unit}>{v.hint}</span> }; - }); - - return ( - <div> - <div className={styles.inputWrapper}> - <div className={styles.inputUnitWrapper}> - <EnhancedSelectInput - name={name} - value={value} - values={values} - isEditable={true} - onChange={onChange} - /> - - <div className={styles.inputUnit}> - d{unit} - </div> - </div> - </div> - <div className={styles.details}> - <div> - <label>{translate('Umask')}</label> - <div className={styles.value}>{umask}</div> - </div> - <div> - <label>{translate('Folder')}</label> - <div className={styles.value}>{folder}</div> - <div className={styles.unit}>d{formatPermissions(folderNum)}</div> - </div> - <div> - <label>{translate('File')}</label> - <div className={styles.value}>{file}</div> - <div className={styles.unit}>{formatPermissions(fileNum)}</div> - </div> - </div> - </div> - ); - } -} - -UMaskInput.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onFocus: PropTypes.func, - onBlur: PropTypes.func -}; - -export default UMaskInput; diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 95f85c119..787bb2bdc 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import { throttle } from 'lodash'; import React, { + ComponentProps, ForwardedRef, forwardRef, MutableRefObject, @@ -24,6 +25,7 @@ interface ScrollerProps { scrollTop?: number; initialScrollTop?: number; children?: ReactNode; + style?: ComponentProps<'div'>['style']; onScroll?: (payload: OnScroll) => void; } diff --git a/frontend/src/Helpers/Hooks/useDebouncedCallback.tsx b/frontend/src/Helpers/Hooks/useDebouncedCallback.tsx new file mode 100644 index 000000000..28e4f17df --- /dev/null +++ b/frontend/src/Helpers/Hooks/useDebouncedCallback.tsx @@ -0,0 +1,16 @@ +import { debounce, DebouncedFunc, DebounceSettings } from 'lodash'; +import { useCallback } from 'react'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default function useDebouncedCallback<T extends (...args: any) => any>( + callback: T, + delay: number, + options?: DebounceSettings +): DebouncedFunc<T> { + // eslint-disable-next-line react-hooks/exhaustive-deps + return useCallback(debounce(callback, delay, options), [ + callback, + delay, + options, + ]); +} diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.ts similarity index 74% rename from frontend/src/Helpers/Props/inputTypes.js rename to frontend/src/Helpers/Props/inputTypes.ts index a71c28d8c..d0ecc3553 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.ts @@ -2,7 +2,6 @@ export const AUTO_COMPLETE = 'autoComplete'; export const CAPTCHA = 'captcha'; export const CHECK = 'check'; export const DEVICE = 'device'; -export const KEY_VALUE_LIST = 'keyValueList'; export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const FLOAT = 'float'; @@ -32,7 +31,6 @@ export const all = [ CAPTCHA, CHECK, DEVICE, - KEY_VALUE_LIST, MONITOR_EPISODES_SELECT, MONITOR_NEW_ITEMS_SELECT, FLOAT, @@ -54,5 +52,36 @@ export const all = [ TEXT_AREA, TEXT_TAG, TAG_SELECT, - UMASK + UMASK, ]; + +export type InputType = + | 'autoComplete' + | 'captcha' + | 'check' + | 'device' + | 'keyValueList' + | 'monitorEpisodesSelect' + | 'monitorNewItemsSelect' + | 'file' + | 'float' + | 'number' + | 'oauth' + | 'password' + | 'path' + | 'qualityProfileSelect' + | 'indexerSelect' + | 'indexerFlagsSelect' + | 'languageSelect' + | 'downloadClientSelect' + | 'rootFolderSelect' + | 'select' + | 'seriesTag' + | 'dynamicSelect' + | 'seriesTypeSelect' + | 'tag' + | 'text' + | 'textArea' + | 'textTag' + | 'tagSelect' + | 'umask'; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index c87b98380..74473b5ed 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -22,7 +22,7 @@ import { setEpisodesSort, } from 'Store/Actions/episodeSelectionActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import { CheckInputChanged } from 'typings/inputs'; +import { CheckInputChanged, InputChanged } from 'typings/inputs'; import { SelectStateInputProps } from 'typings/props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; @@ -105,7 +105,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { selectedEpisodesCount > 0 && selectedEpisodesCount % selectedCount === 0; const onFilterChange = useCallback( - ({ value }: { value: string }) => { + ({ value }: InputChanged<string>) => { setFilter(value.toLowerCase()); }, [setFilter] diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index aefda32a6..62b2da885 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; -import PathInputConnector from 'Components/Form/PathInputConnector'; +import PathInput from 'Components/Form/PathInput'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import ModalBody from 'Components/Modal/ModalBody'; @@ -104,9 +104,10 @@ function InteractiveImportSelectFolderModalContent( </ModalHeader> <ModalBody> - <PathInputConnector + <PathInput name="folder" value={folder} + includeFiles={false} onChange={onPathChange} /> diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx index 7ae5824bb..f13797153 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx @@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props'; import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import dimensions from 'Styles/Variables/dimensions'; +import { InputChanged } from 'typings/inputs'; import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import SelectSeriesModalTableHeader from './SelectSeriesModalTableHeader'; @@ -149,7 +150,7 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { }, [listRef, scrollerRef]); const onFilterChange = useCallback( - ({ value }: { value: string }) => { + ({ value }: InputChanged<string>) => { setFilter(value); }, [setFilter] diff --git a/frontend/src/Parse/Parse.tsx b/frontend/src/Parse/Parse.tsx index 7c9120edd..b14772794 100644 --- a/frontend/src/Parse/Parse.tsx +++ b/frontend/src/Parse/Parse.tsx @@ -8,6 +8,7 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import { icons } from 'Helpers/Props'; import { clear, fetch } from 'Store/Actions/parseActions'; +import { InputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; import ParseResult from './ParseResult'; @@ -21,7 +22,7 @@ function Parse() { const dispatch = useDispatch(); const onInputChange = useCallback( - ({ value }: { value: string }) => { + ({ value }: InputChanged<string>) => { const trimmedValue = value.trim(); setTitle(value); diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx index 80a91fe49..90246eb2c 100644 --- a/frontend/src/Parse/ParseModalContent.tsx +++ b/frontend/src/Parse/ParseModalContent.tsx @@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { icons } from 'Helpers/Props'; import { clear, fetch } from 'Store/Actions/parseActions'; +import { InputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; import ParseResult from './ParseResult'; @@ -28,7 +29,7 @@ function ParseModalContent(props: ParseModalContentProps) { const dispatch = useDispatch(); const onInputChange = useCallback( - ({ value }: { value: string }) => { + ({ value }: InputChanged<string>) => { const trimmedValue = value.trim(); setTitle(value); diff --git a/frontend/src/Series/Index/Select/SeriesIndexPosterSelect.tsx b/frontend/src/Series/Index/Select/SeriesIndexPosterSelect.tsx index 506f85905..4e2167ebc 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexPosterSelect.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexPosterSelect.tsx @@ -15,9 +15,8 @@ function SeriesIndexPosterSelect(props: SeriesIndexPosterSelectProps) { const isSelected = selectState.selectedState[seriesId]; const onSelectPress = useCallback( - (event: SyntheticEvent) => { - const nativeEvent = event.nativeEvent as PointerEvent; - const shiftKey = nativeEvent.shiftKey; + (event: SyntheticEvent<HTMLElement, PointerEvent>) => { + const shiftKey = event.nativeEvent.shiftKey; selectDispatch({ type: 'toggleSelected', diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx index b8b71716a..612d23c4d 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx @@ -310,7 +310,7 @@ function NamingModal(props: NamingModalProps) { ); const handleInputSelectionChange = useCallback( - (selectionStart: number, selectionEnd: number) => { + (selectionStart: number | null, selectionEnd: number | null) => { setSelectionStart(selectionStart); setSelectionEnd(selectionEnd); }, diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.css b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.css index 050567669..a6ef7e3c0 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.css +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.css @@ -5,7 +5,7 @@ } .tagInternalInput { - composes: internalInput from '~Components/Form/TagInput.css'; + composes: internalInput from '~Components/Form/Tag/TagInput.css'; flex: 0 0 100%; } diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx index 9ff1eb9aa..d4cc963c2 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -// @ts-expect-error 'MiddleTruncate' isn't typed import MiddleTruncate from 'react-middle-truncate'; import { useDispatch } from 'react-redux'; import { Tag } from 'App/State/TagsAppState'; diff --git a/frontend/src/Store/Actions/providerOptionActions.js b/frontend/src/Store/Actions/providerOptionActions.js index 4dc38a98f..afc096293 100644 --- a/frontend/src/Store/Actions/providerOptionActions.js +++ b/frontend/src/Store/Actions/providerOptionActions.js @@ -55,10 +55,19 @@ export const actionHandlers = handleThunks({ payload }; - dispatch(set({ - section: subsection, - isFetching: true - })); + // Subsection might not yet be defined + if (getState()[section][payload.section]) { + dispatch(set({ + section: subsection, + isFetching: true + })); + } else { + dispatch(set({ + section: subsection, + ...defaultState, + isFetching: true + })); + } const promise = requestAction(payload); diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.ts index abee01f75..7e13b8a31 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import getSectionState from 'Utilities/State/getSectionState'; function createSortedSectionSelector<T>( @@ -6,7 +7,7 @@ function createSortedSectionSelector<T>( comparer: (a: T, b: T) => number ) { return createSelector( - (state) => state, + (state: AppState) => state, (state) => { const sectionState = getSectionState(state, section, true); diff --git a/frontend/src/Store/Selectors/selectSettings.js b/frontend/src/Store/Selectors/selectSettings.js index 3e30478b7..e3db2bf9d 100644 --- a/frontend/src/Store/Selectors/selectSettings.js +++ b/frontend/src/Store/Selectors/selectSettings.js @@ -10,6 +10,11 @@ function getValidationFailures(saveError) { function mapFailure(failure) { return { + errorMessage: failure.errorMessage, + infoLink: failure.infoLink, + detailedDescription: failure.detailedDescription, + + // TODO: Remove these renamed properties message: failure.errorMessage, link: failure.infoLink, detailedMessage: failure.detailedDescription diff --git a/frontend/src/typings/DownloadClient.ts b/frontend/src/typings/DownloadClient.ts index 2c032c22a..547b8c620 100644 --- a/frontend/src/typings/DownloadClient.ts +++ b/frontend/src/typings/DownloadClient.ts @@ -1,18 +1,11 @@ import ModelBase from 'App/ModelBase'; +import Field from './Field'; -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} +export type Protocol = 'torrent' | 'usenet' | 'unknown'; interface DownloadClient extends ModelBase { enable: boolean; - protocol: string; + protocol: Protocol; priority: number; removeCompletedDownloads: boolean; removeFailedDownloads: boolean; diff --git a/frontend/src/typings/Field.ts b/frontend/src/typings/Field.ts new file mode 100644 index 000000000..4ebb05278 --- /dev/null +++ b/frontend/src/typings/Field.ts @@ -0,0 +1,21 @@ +export interface FieldSelectOption<T> { + value: T; + name: string; + order: number; + hint?: string; + parentValue?: T; + isDisabled?: boolean; + additionalProperties?: Record<string, unknown>; +} + +interface Field { + order: number; + name: string; + label: string; + value: boolean | number | string; + type: string; + advanced: boolean; + privacy: string; +} + +export default Field; diff --git a/frontend/src/typings/Helpers/ArrayElement.ts b/frontend/src/typings/Helpers/ArrayElement.ts new file mode 100644 index 000000000..dce610209 --- /dev/null +++ b/frontend/src/typings/Helpers/ArrayElement.ts @@ -0,0 +1,3 @@ +type ArrayElement<V> = V extends (infer U)[] ? U : V; + +export default ArrayElement; diff --git a/frontend/src/typings/ImportList.ts b/frontend/src/typings/ImportList.ts index 7af4c2bbc..a7aa48f26 100644 --- a/frontend/src/typings/ImportList.ts +++ b/frontend/src/typings/ImportList.ts @@ -1,14 +1,5 @@ import ModelBase from 'App/ModelBase'; - -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} +import Field from './Field'; interface ImportList extends ModelBase { enable: boolean; diff --git a/frontend/src/typings/Indexer.ts b/frontend/src/typings/Indexer.ts index e6c23eda2..dbfed94a8 100644 --- a/frontend/src/typings/Indexer.ts +++ b/frontend/src/typings/Indexer.ts @@ -1,14 +1,5 @@ import ModelBase from 'App/ModelBase'; - -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} +import Field from './Field'; interface Indexer extends ModelBase { enableRss: boolean; diff --git a/frontend/src/typings/Notification.ts b/frontend/src/typings/Notification.ts index e2b5ad7eb..3aa3a2d48 100644 --- a/frontend/src/typings/Notification.ts +++ b/frontend/src/typings/Notification.ts @@ -1,14 +1,5 @@ import ModelBase from 'App/ModelBase'; - -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} +import Field from './Field'; interface Notification extends ModelBase { enable: boolean; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index cf91149b6..c9c335bfc 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -3,4 +3,17 @@ export type InputChanged<T = unknown> = { value: T; }; -export type CheckInputChanged = InputChanged<boolean>; +export type InputOnChange<T> = (change: InputChanged<T>) => void; + +export interface CheckInputChanged extends InputChanged<boolean> { + shiftKey: boolean; +} + +export interface FileInputChanged extends InputChanged<string> { + files: FileList | null | undefined; +} + +export interface EnhancedSelectInputChanged<T> extends InputChanged<T> { + value: T; + additionalProperties?: unknown; +} diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index 5cdcbc003..b84a60ada 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -1,6 +1,8 @@ export interface ValidationFailure { propertyName: string; errorMessage: string; + infoLink?: string; + detailedDescription?: string; severity: 'error' | 'warning'; } diff --git a/frontend/typings/MiddleTruncate.d.ts b/frontend/typings/MiddleTruncate.d.ts new file mode 100644 index 000000000..598c28156 --- /dev/null +++ b/frontend/typings/MiddleTruncate.d.ts @@ -0,0 +1,16 @@ +declare module 'react-middle-truncate' { + import { ComponentPropsWithoutRef } from 'react'; + + interface MiddleTruncateProps extends ComponentPropsWithoutRef<'div'> { + text: string; + ellipsis?: string; + start?: number | RegExp | string; + end?: number | RegExp | string; + smartCopy?: 'all' | 'partial'; + onResizeDebounceMs?: number; + } + + export default function MiddleTruncate( + props: MiddleTruncateProps + ): JSX.Element; +} diff --git a/frontend/typings/jdu.d.ts b/frontend/typings/jdu.d.ts new file mode 100644 index 000000000..38038348f --- /dev/null +++ b/frontend/typings/jdu.d.ts @@ -0,0 +1,3 @@ +declare module 'jdu' { + export function replace(value: string): string; +} diff --git a/package.json b/package.json index cff0e309d..83034c9b9 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,9 @@ "@babel/preset-typescript": "7.25.7", "@types/lodash": "4.14.195", "@types/qs": "6.9.16", + "@types/react-autosuggest": "10.1.11", "@types/react-document-title": "2.0.10", + "@types/react-google-recaptcha": "2.1.9", "@types/react-lazyload": "3.2.3", "@types/react-router-dom": "5.3.3", "@types/react-text-truncate": "0.19.0", diff --git a/yarn.lock b/yarn.lock index 2870d9142..43427e636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1321,6 +1321,13 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== +"@types/react-autosuggest@10.1.11": + version "10.1.11" + resolved "https://registry.yarnpkg.com/@types/react-autosuggest/-/react-autosuggest-10.1.11.tgz#d087dbfc03e092ac742d18b8b80f5986f03929f3" + integrity sha512-lneJrX/5TZJzKHPJ6UuUjsh9OfeyQHKYEVHyBh5Y7LeRbCZxyIsjBmpxdPy1iH++Ger0qcyW+phPpYH+g3naLA== + dependencies: + "@types/react" "*" + "@types/react-document-title@2.0.10": version "2.0.10" resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.10.tgz#f9c4563744b735750d84519ba1bc7099e1b2d1d0" @@ -1335,6 +1342,13 @@ dependencies: "@types/react" "*" +"@types/react-google-recaptcha@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz#cd1ffe571fe738473b66690a86dad6c9d3648427" + integrity sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug== + dependencies: + "@types/react" "*" + "@types/react-lazyload@3.2.3": version "3.2.3" resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.2.3.tgz#42129b6e11353bfe8ed2ba5e1b964676ee23668b" @@ -6192,16 +6206,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6292,14 +6297,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From c41e3ce1e3cb440b09ecf3b1d9fa2631d7f1484d Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:17:58 +0300 Subject: [PATCH 613/762] Update paths mapping translations for series specific --- src/NzbDrone.Core/Localization/Core/en.json | 4 ++-- src/NzbDrone.Core/Localization/Core/es.json | 4 ++-- src/NzbDrone.Core/Localization/Core/fi.json | 4 ++-- src/NzbDrone.Core/Localization/Core/fr.json | 4 ++-- src/NzbDrone.Core/Localization/Core/pt_BR.json | 4 ++-- src/NzbDrone.Core/Localization/Core/ru.json | 4 ++-- src/NzbDrone.Core/Localization/Core/tr.json | 4 ++-- src/NzbDrone.Core/Localization/Core/zh_CN.json | 4 ++-- .../Notifications/MediaBrowser/MediaBrowserSettings.cs | 4 ++-- .../Notifications/Plex/Server/PlexServerSettings.cs | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5c8616c83..48be6525c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1419,9 +1419,9 @@ "NotificationsSendGridSettingsApiKeyHelpText": "The API Key generated by SendGrid", "NotificationsSettingsUpdateLibrary": "Update Library", "NotificationsSettingsUpdateMapPathsFrom": "Map Paths From", - "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')", "NotificationsSettingsUpdateMapPathsTo": "Map Paths To", - "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')", "NotificationsSettingsUseSslHelpText": "Connect to {serviceName} over HTTPS instead of HTTP", "NotificationsSettingsWebhookMethod": "Method", "NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index e7371281e..018694912 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1413,7 +1413,7 @@ "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')", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "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", @@ -2051,7 +2051,7 @@ "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')", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "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 causará que este perfil solo se aplique a lanzamientos desde ese indexador.", "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList", "ImportListsMyAnimeListSettingsListStatus": "Estado de lista", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index d30fded92..a0105cf6d 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1584,7 +1584,7 @@ "NotificationsPushcutSettingsTimeSensitiveHelpText": "Merkitsee ilmoituksen kiireelliseksi (\"Time Sensitive\").", "NotificationsPushcutSettingsNotificationNameHelpText": "Ilmoituksen nimi Pushcut-sovelluksen ilmoitusvälilehdeltä.", "NotificationsPushoverSettingsSound": "Ääni", - "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", "NotificationsPushoverSettingsExpire": "Erääntyminen", "NotificationsSendGridSettingsApiKeyHelpText": "SendGridin luoma rajapinnan (API) avain.", "NotificationsSimplepushSettingsEventHelpText": "Mukauta push-ilmoitusten toimintaa.", @@ -1614,7 +1614,7 @@ "NotificationsKodiSettingsDisplayTime": "Näytä aika", "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", "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).", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{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.", "NotificationsTraktSettingsAccessToken": "Käyttötunniste", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 0d1312936..f8768f3da 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1786,7 +1786,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')", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "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", @@ -1980,7 +1980,7 @@ "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')", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "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", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 228078976..e1d508b26 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1791,7 +1791,7 @@ "NotificationsSettingsUpdateLibrary": "Atualizar Biblioteca", "NotificationsSettingsUpdateMapPathsFrom": "Mapear Caminhos De", "NotificationsSettingsUpdateMapPathsTo": "Mapear Caminhos Para", - "NotificationsSettingsUpdateMapPathsToHelpText": "Caminho {serviceName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "Caminho {serviceName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", "NotificationsSettingsUseSslHelpText": "Conecte-se a {serviceName} por HTTPS em vez de HTTP", "NotificationsSettingsWebhookMethod": "Método", "NotificationsSettingsWebhookMethodHelpText": "Qual método HTTP usar para enviar ao Webservice", @@ -1857,7 +1857,7 @@ "NotificationsPushBulletSettingSenderIdHelpText": "O ID do dispositivo para enviar notificações, use device_iden no URL do dispositivo em pushbullet.com (deixe em branco para enviar de você mesmo)", "NotificationsPushcutSettingsNotificationNameHelpText": "Nome da notificação na aba Notificações do aplicativo Pushcut", "NotificationsPushoverSettingsExpireHelpText": "Tempo máximo para tentar novamente alertas de emergência, máximo de 86.400 segundos\"", - "NotificationsSettingsUpdateMapPathsFromHelpText": "Caminho {appName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "Caminho {appName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", "NotificationsSignalSettingsGroupIdPhoneNumber": "ID do Grupo/Número de Telefone", "NotificationsSignalSettingsSenderNumberHelpText": "Número de telefone do registro do remetente no signal-api", "NotificationsSlackSettingsChannelHelpText": "Substitui o canal padrão para o webhook de entrada (#other-channel)", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 297a8dfba..4618e8db5 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -853,7 +853,7 @@ "NotificationsPushoverSettingsRetryHelpText": "Интервал повтора экстренных оповещений, минимум 30 секунд", "NotificationsPushoverSettingsSound": "Звук", "NotificationsSettingsUpdateMapPathsFrom": "Карта путей от", - "NotificationsSettingsUpdateMapPathsFromHelpText": "Путь {appName}, используемый для изменения путей к сериалам, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "Путь {appName}, используемый для изменения путей к сериалам, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", "NotificationsSettingsUseSslHelpText": "Подключитесь к {serviceName} по протоколу HTTPS вместо HTTP", "NotificationsSettingsWebhookMethod": "Метод", "NotificationsSettingsWebhookMethodHelpText": "Какой метод HTTP использовать для отправки в веб-сервис", @@ -957,7 +957,7 @@ "OnlyUsenet": "Только Usenet", "NoHistory": "Нет истории", "NotificationsValidationInvalidAuthenticationToken": "Токен аутентификации недействителен", - "NotificationsSettingsUpdateMapPathsToHelpText": "Путь {serviceName}, используемый для изменения путей к сериям, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "Путь {serviceName}, используемый для изменения путей к сериям, когда {serviceName} видит путь к библиотеке иначе, чем {appName} (требуется 'Обновить библиотеку')", "NotificationsAppriseSettingsConfigurationKey": "Применить конфигурационный ключ", "MonitorPilotEpisode": "Пилотный эпизод", "MonitorNoNewSeasons": "Нет новых сезонов", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 11fbf777f..b088ea40a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -507,7 +507,7 @@ "IndexerDownloadClientHelpText": "Bu dizinleyiciden yakalamak için hangi indirme istemcisinin kullanılacağını belirtin", "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", "LastDuration": "Yürütme Süresi", - "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", "NotificationsPlexSettingsAuthToken": "Kimlik Doğrulama Jetonu", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Alıcının Grup Kimliği / Telefon Numarası", "NotificationsSignalSettingsUsernameHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan kullanıcı adı", @@ -777,7 +777,7 @@ "NotificationsTwitterSettingsMention": "Bahset", "NotificationsTwitterSettingsMentionHelpText": "Gönderilen tweetlerde bu kullanıcıdan bahsedin", "NotificationsPushoverSettingsDevices": "Cihazlar", - "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} yolu, {serviceName} kitaplık yolu konumunu {appName}'dan farklı gördüğünde seri yollarını değiştirmek için kullanılır ('Kütüphaneyi Güncelle' gerektirir)", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName} yolu, {serviceName} kitaplık yolu konumunu {appName}'dan farklı gördüğünde seri yollarını değiştirmek için kullanılır ('Kütüphaneyi Güncelle' gerektirir)", "NotificationsSettingsWebhookMethodHelpText": "Web hizmetine göndermek için hangi HTTP yönteminin kullanılacağı", "NotificationsValidationUnableToSendTestMessageApiResponse": "Test mesajı gönderilemiyor. API'den yanıt: {error}", "NzbgetHistoryItemMessage": "PAR Durumu: {parStatus} - Paketten Çıkarma Durumu: {unpackStatus} - Taşıma Durumu: {moveStatus} - Komut Dosyası Durumu: {scriptStatus} - Silme Durumu: {deleteStatus} - İşaretleme Durumu: {markStatus}", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index edf704752..fb90a8b5c 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1862,7 +1862,7 @@ "NotificationsPushcutSettingsTimeSensitive": "紧急", "NotificationsSettingsUpdateLibrary": "更新资源库", "NotificationsPushoverSettingsSound": "声音", - "NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", "NotificationsSettingsUseSslHelpText": "使用 HTTPS 而非 HTTP 连接至 {serviceName}", "NotificationsSignalSettingsSenderNumberHelpText": "发送者在 Signal API 中注册的电话号码", "NotificationsSlackSettingsChannelHelpText": "覆盖传入 Webhook 的默认渠道(#other-channel)", @@ -1938,7 +1938,7 @@ "LogSizeLimit": "日志大小限制", "LogSizeLimitHelpText": "存档前的最大日志文件大小(MB)。默认值为 1 MB。", "NotificationsDiscordSettingsOnImportFieldsHelpText": "更改用于 “导入” 通知的字段", - "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName} 路径,当 {serviceName} 与 {appName} 对资源库路径的识别不一致时,可以使用此设置修改系列路径(需要`更新资源库`)", "NotificationsPushoverSettingsRetryHelpText": "紧急警报的重试间隔,最少 30 秒", "NotificationsSlackSettingsIconHelpText": "更改用于发送到 Slack 的消息图标(表情符号或 URL)", "NotificationsTelegramSettingsSendSilentlyHelpText": "静默发送消息。用户将收到没有声音的通知", diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index bd15c140e..4ee4a0b92 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -52,11 +52,11 @@ namespace NzbDrone.Core.Notifications.Emby [FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsEmbySettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", HelpText = "NotificationsSettingsUpdateMapPathsFromSeriesHelpText", Type = FieldType.Textbox, Advanced = true)] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Emby/Jellyfin")] public string MapFrom { get; set; } - [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", HelpText = "NotificationsSettingsUpdateMapPathsToHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", HelpText = "NotificationsSettingsUpdateMapPathsToSeriesHelpText", Type = FieldType.Textbox, Advanced = true)] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Emby/Jellyfin")] public string MapTo { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index 721d80dce..983c05963 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -57,11 +57,11 @@ namespace NzbDrone.Core.Notifications.Plex.Server [FieldDefinition(7, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } - [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")] + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromSeriesHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Plex")] public string MapFrom { get; set; } - [FieldDefinition(9, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")] + [FieldDefinition(9, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToSeriesHelpText")] [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Plex")] public string MapTo { get; set; } From 804eaa12272f6f4bef336995d6d2a870e143c11d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 2 Nov 2024 20:24:50 +0000 Subject: [PATCH 614/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Lars <lars.erik.heloe@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: http://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/nb_NO.json | 5 ++++- src/NzbDrone.Core/Localization/Core/tr.json | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index ed9052f14..e6bd6ad21 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -15,5 +15,8 @@ "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" + "Actions": "Handlinger", + "AddCustomFilter": "Legg til eget filter", + "AddConnection": "Legg til tilkobling", + "AddDelayProfile": "Legg til forsinkelsesprofil" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index b088ea40a..95eadfe44 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -145,7 +145,7 @@ "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", "CloneAutoTag": "Otomatik Etiketi Klonla", "Dash": "Çizgi", - "DeleteReleaseProfileMessageText": "'{name}' bu yayımlama profilini silmek istediğinizden emin misiniz?", + "DeleteReleaseProfileMessageText": "'{name}' sürüm profilini silmek istediğinizden emin misiniz?", "DownloadClientFreeboxApiError": "Freebox API'si şu hatayı döndürdü: {errorDescription}", "DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil", "DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?", @@ -866,5 +866,17 @@ "YesterdayAt": "Dün saat {time}'da", "CustomFormatsSpecificationExceptLanguage": "Dil Dışında", "CustomFormatsSpecificationExceptLanguageHelpText": "Seçilen dil dışında herhangi bir dil mevcutsa eşleşir", - "LastSearched": "Son Aranan" + "LastSearched": "Son Aranan", + "Enable": "Etkinleştir", + "OnFileUpgrade": "Dosyada Yükseltme", + "SmartReplace": "Akıllı Değiştir", + "OnFileImport": "Dosya İçe Aktarımı", + "SmartReplaceHint": "İsme bağlı olarak Dash veya Space Dash", + "DefaultNotFoundMessage": "Kaybolmuş olmalısın, burada görülecek bir şey yok.", + "ToggleUnmonitoredToMonitored": "İzlenmiyor, izlemek için tıklayın", + "ToggleMonitoredToUnmonitored": "İzleniyor, izlemeyi bırakmak için tıklayın", + "Install": "Kur", + "InstallMajorVersionUpdate": "Güncellemeyi Kur", + "InstallMajorVersionUpdateMessage": "Bu güncelleştirme yeni bir ana sürüm yükleyecek ve sisteminizle uyumlu olmayabilir. Bu güncelleştirmeyi yüklemek istediğinizden emin misiniz?", + "InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin." } From 1fcfb88d2aa0126c4b3c878c8e310311ea57d04d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 20:48:00 -0700 Subject: [PATCH 615/762] New: Use instance name in PWA manifest Closes #7315 --- frontend/src/Content/manifest.json | 2 +- .../Frontend/Mappers/ManifestMapper.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/Content/manifest.json b/frontend/src/Content/manifest.json index 5cb58dc0d..5c2b3d59d 100644 --- a/frontend/src/Content/manifest.json +++ b/frontend/src/Content/manifest.json @@ -1,5 +1,5 @@ { - "name": "Sonarr", + "name": "__INSTANCE_NAME__", "icons": [ { "src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png", diff --git a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs index 424af1cda..ca69d0430 100644 --- a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -8,9 +8,14 @@ namespace Sonarr.Http.Frontend.Mappers { public class ManifestMapper : UrlBaseReplacementResourceMapperBase { + private readonly IConfigFileProvider _configFileProvider; + + private string _generatedContent; + public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) : base(diskProvider, configFileProvider, logger) { + _configFileProvider = configFileProvider; FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "manifest.json"); } @@ -23,5 +28,21 @@ namespace Sonarr.Http.Frontend.Mappers { return resourceUrl.StartsWith("/Content/manifest"); } + + protected override string GetFileText() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var text = base.GetFileText(); + + text = text.Replace("__INSTANCE_NAME__", _configFileProvider.InstanceName); + + _generatedContent = text; + + return _generatedContent; + } } } From e88f25d3bf72fe98a9a2cf7045f60090c6b795bc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 20:56:27 -0700 Subject: [PATCH 616/762] Fixed: Parse version after quality in renamed files Closes #7302 --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 7 +++++++ src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index eb0f4b5ba..025b865b4 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -502,6 +502,13 @@ namespace NzbDrone.Core.Test.ParserTests result.Revision.IsRepack.Should().Be(isRepack); } + [TestCase("[MTBB] Series Title - S02E02 - 027 - Episode Title [WEBDL-1080p v2][x264][AAC]", 2)] + public void should_be_able_to_parse_anime_version(string title, int version) + { + var result = QualityParser.ParseQuality(title); + result.Revision.Version.Should().Be(version); + } + private void ParseAndVerifyQuality(string title, Quality quality, bool proper) { var result = QualityParser.ParseQuality(title); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 5205c8a21..cdb297d3d 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex RepackRegex = new (@"\b(?<repack>repack\d?|rerip\d?)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex VersionRegex = new (@"\d[-._ ]?v(?<version>\d)[-._ ]|\[v(?<version>\d)\]|repack(?<version>\d)|rerip(?<version>\d)", + private static readonly Regex VersionRegex = new (@"\d[-._ ]?v(?<version>\d)[-._ ]|\[v(?<version>\d)\]|repack(?<version>\d)|rerip(?<version>\d)|(?:480|576|720|1080|2160)p[._ ]v(?<version>\d)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex RealRegex = new (@"\b(?<real>REAL)\b", From e006b405323c276eb5b7f2dd97b97c80394a6930 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 21:47:54 -0700 Subject: [PATCH 617/762] New: Add individual edit to Manage Custom Formats Closes #5905 --- .../EditCustomFormatModalConnector.js | 2 + .../ManageCustomFormatsModalContent.tsx | 5 ++ .../Manage/ManageCustomFormatsModalRow.css | 6 ++ .../ManageCustomFormatsModalRow.css.d.ts | 1 + .../Manage/ManageCustomFormatsModalRow.tsx | 78 ++++++++++++++++++- 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js index 52b2f09f6..3e79425cd 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditCustomFormatModal from './EditCustomFormatModal'; +import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector'; function mapStateToProps() { return {}; @@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component { } EditCustomFormatModalConnector.propTypes = { + ...EditCustomFormatModalContentConnector.propTypes, onModalClose: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx index bd16c74e9..891c9941f 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx @@ -47,6 +47,11 @@ const COLUMNS = [ isSortable: true, isVisible: true, }, + { + name: 'actions', + label: '', + isVisible: true, + }, ]; interface ManageCustomFormatsModalContentProps { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css index a7c85e340..355c70378 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css @@ -4,3 +4,9 @@ word-break: break-all; } + +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 40px; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts index 906d2dc54..d1719edd8 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts @@ -1,6 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'actions': string; 'includeCustomFormatWhenRenaming': string; 'name': string; } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx index 32b135970..57bb7fda0 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx @@ -1,10 +1,18 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; +import { icons } from 'Helpers/Props'; +import { deleteCustomFormat } from 'Store/Actions/settingsActions'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; +import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector'; import styles from './ManageCustomFormatsModalRow.css'; interface ManageCustomFormatsModalRowProps { @@ -16,6 +24,15 @@ interface ManageCustomFormatsModalRowProps { onSelectedChange(result: SelectStateInputProps): void; } +function isDeletingSelector() { + return createSelector( + (state: AppState) => state.settings.customFormats.isDeleting, + (isDeleting) => { + return isDeleting; + } + ); +} + function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { const { id, @@ -25,7 +42,16 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { onSelectedChange, } = props; - const onSelectedChangeWrapper = useCallback( + const dispatch = useDispatch(); + const isDeleting = useSelector(isDeletingSelector()); + + const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] = + useState(false); + + const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] = + useState(false); + + const handlelectedChange = useCallback( (result: SelectStateInputProps) => { onSelectedChange({ ...result, @@ -34,12 +60,33 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { [onSelectedChange] ); + const handleEditCustomFormatModalOpen = useCallback(() => { + setIsEditCustomFormatModalOpen(true); + }, [setIsEditCustomFormatModalOpen]); + + const handleEditCustomFormatModalClose = useCallback(() => { + setIsEditCustomFormatModalOpen(false); + }, [setIsEditCustomFormatModalOpen]); + + const handleDeleteCustomFormatPress = useCallback(() => { + setIsEditCustomFormatModalOpen(false); + setIsDeleteCustomFormatModalOpen(true); + }, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]); + + const handleDeleteCustomFormatModalClose = useCallback(() => { + setIsDeleteCustomFormatModalOpen(false); + }, [setIsDeleteCustomFormatModalOpen]); + + const handleConfirmDeleteCustomFormat = useCallback(() => { + dispatch(deleteCustomFormat({ id })); + }, [id, dispatch]); + return ( <TableRow> <TableSelectCell id={id} isSelected={isSelected} - onSelectedChange={onSelectedChangeWrapper} + onSelectedChange={handlelectedChange} /> <TableRowCell className={styles.name}>{name}</TableRowCell> @@ -47,6 +94,31 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { <TableRowCell className={styles.includeCustomFormatWhenRenaming}> {includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')} </TableRowCell> + + <TableRowCell className={styles.actions}> + <IconButton + name={icons.EDIT} + onPress={handleEditCustomFormatModalOpen} + /> + </TableRowCell> + + <EditCustomFormatModalConnector + id={id} + isOpen={isEditCustomFormatModalOpen} + onModalClose={handleEditCustomFormatModalClose} + onDeleteCustomFormatPress={handleDeleteCustomFormatPress} + /> + + <ConfirmModal + isOpen={isDeleteCustomFormatModalOpen} + kind="danger" + title={translate('DeleteCustomFormat')} + message={translate('DeleteCustomFormatMessageText', { name })} + confirmLabel={translate('Delete')} + isSpinning={isDeleting} + onConfirm={handleConfirmDeleteCustomFormat} + onCancel={handleDeleteCustomFormatModalClose} + /> </TableRow> ); } From 0f225b05c00add562c9a6aa8cc4cf494e83176c1 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 21:48:18 -0700 Subject: [PATCH 618/762] Rename Manage Custom Formats to Manage Formats --- .../CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx index f27f9e503..91f41dc44 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx @@ -12,7 +12,7 @@ function ManageCustomFormatsToolbarButton() { return ( <> <PageToolbarButton - label={translate('ManageCustomFormats')} + label={translate('ManageFormats')} iconName={icons.MANAGE} onPress={openManageModal} /> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 48be6525c..a71c40cda 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1118,6 +1118,7 @@ "ManageDownloadClients": "Manage Download Clients", "ManageEpisodes": "Manage Episodes", "ManageEpisodesSeason": "Manage Episodes files in this season", + "ManageFormats": "Manage Formats", "ManageImportLists": "Manage Import Lists", "ManageIndexers": "Manage Indexers", "ManageLists": "Manage Lists", From 3ddc6ac6de5c27a9aab915672321c8818dc5da48 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 26 Oct 2024 21:57:03 -0700 Subject: [PATCH 619/762] New: Favorite folders in Manual Import Closes #5891 --- .../App/State/InteractiveImportAppState.ts | 11 ++- frontend/src/Helpers/Props/icons.ts | 2 + .../Folder/FavoriteFolderRow.css | 5 ++ .../Folder/FavoriteFolderRow.css.d.ts | 7 ++ .../Folder/FavoriteFolderRow.tsx | 48 +++++++++++ ...eractiveImportSelectFolderModalContent.css | 7 +- ...iveImportSelectFolderModalContent.css.d.ts | 3 +- ...eractiveImportSelectFolderModalContent.tsx | 71 ++++++++++++---- .../InteractiveImport/Folder/RecentFolder.ts | 6 -- .../Folder/RecentFolderRow.css | 2 +- .../Folder/RecentFolderRow.js | 65 -------------- .../Folder/RecentFolderRow.tsx | 85 +++++++++++++++++++ .../Store/Actions/interactiveImportActions.js | 29 +++++++ src/NzbDrone.Core/Localization/Core/en.json | 4 + 14 files changed, 252 insertions(+), 93 deletions(-) create mode 100644 frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css create mode 100644 frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts create mode 100644 frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx delete mode 100644 frontend/src/InteractiveImport/Folder/RecentFolder.ts delete mode 100644 frontend/src/InteractiveImport/Folder/RecentFolderRow.js create mode 100644 frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx diff --git a/frontend/src/App/State/InteractiveImportAppState.ts b/frontend/src/App/State/InteractiveImportAppState.ts index cf86f620d..84fd9f4c1 100644 --- a/frontend/src/App/State/InteractiveImportAppState.ts +++ b/frontend/src/App/State/InteractiveImportAppState.ts @@ -1,11 +1,20 @@ import AppSectionState from 'App/State/AppSectionState'; -import RecentFolder from 'InteractiveImport/Folder/RecentFolder'; import ImportMode from 'InteractiveImport/ImportMode'; import InteractiveImport from 'InteractiveImport/InteractiveImport'; +interface FavoriteFolder { + folder: string; +} + +interface RecentFolder { + folder: string; + lastUsed: string; +} + interface InteractiveImportAppState extends AppSectionState<InteractiveImport> { originalItems: InteractiveImport[]; importMode: ImportMode; + favoriteFolders: FavoriteFolder[]; recentFolders: RecentFolder[]; } diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index 3ba5c4db1..e9a361066 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -13,6 +13,7 @@ import { faFileVideo as farFileVideo, faFolder as farFolder, faHdd as farHdd, + faHeart as farHeart, faKeyboard as farKeyboard, faObjectGroup as farObjectGroup, faObjectUngroup as farObjectUngroup, @@ -163,6 +164,7 @@ export const FOLDER_OPEN = fasFolderOpen; export const GROUP = farObjectGroup; export const HEALTH = fasMedkit; export const HEART = fasHeart; +export const HEART_OUTLINE = farHeart; export const HISTORY = fasHistory; export const HOUSEKEEPING = fasHome; export const IGNORE = fasTimesCircle; diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css new file mode 100644 index 000000000..2839ea389 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css @@ -0,0 +1,5 @@ +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 70px; +} diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts new file mode 100644 index 000000000..d8ea83dc1 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.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/InteractiveImport/Folder/FavoriteFolderRow.tsx b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx new file mode 100644 index 000000000..e39635623 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx @@ -0,0 +1,48 @@ +import React, { SyntheticEvent, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions'; +import translate from 'Utilities/String/translate'; +import styles from './FavoriteFolderRow.css'; + +interface FavoriteFolderRowProps { + folder: string; + onPress: (folder: string) => unknown; +} + +function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) { + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + onPress(folder); + }, [folder, onPress]); + + const handleRemoveFavoritePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + dispatch(removeFavoriteFolder({ folder })); + }, + [folder, dispatch] + ); + + return ( + <TableRowButton onPress={handlePress}> + <TableRowCell>{folder}</TableRowCell> + + <TableRowCell className={styles.actions}> + <IconButton + title={translate('FavoriteFolderRemove')} + kind="danger" + name={icons.HEART} + onPress={handleRemoveFavoritePress} + /> + </TableRowCell> + </TableRowButton> + ); +} + +export default FavoriteFolderRow; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css index 5f9033a18..8a7b5a541 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css @@ -1,7 +1,12 @@ -.recentFoldersContainer { +.foldersContainer { margin-top: 15px; } +.foldersTitle { + border-bottom: 1px solid var(--borderColor); + font-size: 21px; +} + .buttonsContainer { margin-top: 30px; } diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts index 46abdcb9b..0e304b1b1 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts @@ -5,7 +5,8 @@ interface CssExports { 'buttonContainer': string; 'buttonIcon': string; 'buttonsContainer': string; - 'recentFoldersContainer': string; + 'foldersContainer': string; + 'foldersTitle': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index 62b2da885..01b4e4bff 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; @@ -14,14 +14,23 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { icons, kinds, sizes } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; -import { - addRecentFolder, - removeRecentFolder, -} from 'Store/Actions/interactiveImportActions'; +import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; import translate from 'Utilities/String/translate'; +import FavoriteFolderRow from './FavoriteFolderRow'; import RecentFolderRow from './RecentFolderRow'; import styles from './InteractiveImportSelectFolderModalContent.css'; +const favoriteFoldersColumns = [ + { + name: 'folder', + label: () => translate('Folder'), + }, + { + name: 'actions', + label: '', + }, +]; + const recentFoldersColumns = [ { name: 'folder', @@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent( const { modalTitle, onFolderSelect, onModalClose } = props; const [folder, setFolder] = useState(''); const dispatch = useDispatch(); - const recentFolders = useSelector( + const { favoriteFolders, recentFolders } = useSelector( createSelector( - (state: AppState) => state.interactiveImport.recentFolders, - (recentFolders) => { - return recentFolders; + (state: AppState) => state.interactiveImport, + (interactiveImport) => { + return { + favoriteFolders: interactiveImport.favoriteFolders, + recentFolders: interactiveImport.recentFolders, + }; } ) ); + const favoriteFolderMap = useMemo(() => { + return new Map(favoriteFolders.map((f) => [f.folder, f])); + }, [favoriteFolders]); + const onPathChange = useCallback( ({ value }: { value: string }) => { setFolder(value); @@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent( onFolderSelect(folder); }, [folder, onFolderSelect, dispatch]); - const onRemoveRecentFolderPress = useCallback( - (folderToRemove: string) => { - dispatch(removeRecentFolder({ folder: folderToRemove })); - }, - [dispatch] - ); - return ( <ModalContent onModalClose={onModalClose}> <ModalHeader> @@ -111,8 +120,34 @@ function InteractiveImportSelectFolderModalContent( onChange={onPathChange} /> + {favoriteFolders.length ? ( + <div className={styles.foldersContainer}> + <div className={styles.foldersTitle}> + {translate('FavoriteFolders')} + </div> + + <Table columns={favoriteFoldersColumns}> + <TableBody> + {favoriteFolders.map((favoriteFolder) => { + return ( + <FavoriteFolderRow + key={favoriteFolder.folder} + folder={favoriteFolder.folder} + onPress={onRecentPathPress} + /> + ); + })} + </TableBody> + </Table> + </div> + ) : null} + {recentFolders.length ? ( - <div className={styles.recentFoldersContainer}> + <div className={styles.foldersContainer}> + <div className={styles.foldersTitle}> + {translate('RecentFolders')} + </div> + <Table columns={recentFoldersColumns}> <TableBody> {recentFolders @@ -124,8 +159,8 @@ function InteractiveImportSelectFolderModalContent( key={recentFolder.folder} folder={recentFolder.folder} lastUsed={recentFolder.lastUsed} + isFavorite={favoriteFolderMap.has(recentFolder.folder)} onPress={onRecentPathPress} - onRemoveRecentFolderPress={onRemoveRecentFolderPress} /> ); })} diff --git a/frontend/src/InteractiveImport/Folder/RecentFolder.ts b/frontend/src/InteractiveImport/Folder/RecentFolder.ts deleted file mode 100644 index 9c6e295f6..000000000 --- a/frontend/src/InteractiveImport/Folder/RecentFolder.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface RecentFolder { - folder: string; - lastUsed: string; -} - -export default RecentFolder; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.css b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css index 58eb9a8e4..2839ea389 100644 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.css +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css @@ -1,5 +1,5 @@ .actions { composes: cell from '~Components/Table/Cells/TableRowCell.css'; - width: 40px; + width: 70px; } diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js deleted file mode 100644 index 83c7493c4..000000000 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowButton from 'Components/Table/TableRowButton'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './RecentFolderRow.css'; - -class RecentFolderRow extends Component { - - // - // Listeners - - onPress = () => { - this.props.onPress(this.props.folder); - }; - - onRemovePress = (event) => { - event.stopPropagation(); - - const { - folder, - onRemoveRecentFolderPress - } = this.props; - - onRemoveRecentFolderPress(folder); - }; - - // - // Render - - render() { - const { - folder, - lastUsed - } = this.props; - - return ( - <TableRowButton onPress={this.onPress}> - <TableRowCell>{folder}</TableRowCell> - - <RelativeDateCell date={lastUsed} /> - - <TableRowCell className={styles.actions}> - <IconButton - title={translate('Remove')} - name={icons.REMOVE} - onPress={this.onRemovePress} - /> - </TableRowCell> - </TableRowButton> - ); - } -} - -RecentFolderRow.propTypes = { - folder: PropTypes.string.isRequired, - lastUsed: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired, - onRemoveRecentFolderPress: PropTypes.func.isRequired -}; - -export default RecentFolderRow; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx b/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx new file mode 100644 index 000000000..31d164e1a --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx @@ -0,0 +1,85 @@ +import React, { SyntheticEvent, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import { + addFavoriteFolder, + removeFavoriteFolder, + removeRecentFolder, +} from 'Store/Actions/interactiveImportActions'; +import translate from 'Utilities/String/translate'; +import styles from './RecentFolderRow.css'; + +interface RecentFolderRowProps { + folder: string; + lastUsed: string; + isFavorite: boolean; + onPress: (folder: string) => unknown; +} + +function RecentFolderRow({ + folder, + lastUsed, + isFavorite, + onPress, +}: RecentFolderRowProps) { + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + onPress(folder); + }, [folder, onPress]); + + const handleFavoritePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + if (isFavorite) { + dispatch(removeFavoriteFolder({ folder })); + } else { + dispatch(addFavoriteFolder({ folder })); + } + }, + [folder, isFavorite, dispatch] + ); + + const handleRemovePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + dispatch(removeRecentFolder({ folder })); + }, + [folder, dispatch] + ); + + return ( + <TableRowButton onPress={handlePress}> + <TableRowCell>{folder}</TableRowCell> + + <RelativeDateCell date={lastUsed} /> + + <TableRowCell className={styles.actions}> + <IconButton + title={ + isFavorite + ? translate('FavoriteFolderRemove') + : translate('FavoriteFolderAdd') + } + kind={isFavorite ? 'danger' : 'default'} + name={isFavorite ? icons.HEART : icons.HEART_OUTLINE} + onPress={handleFavoritePress} + /> + + <IconButton + title={translate('Remove')} + name={icons.REMOVE} + onPress={handleRemovePress} + /> + </TableRowCell> + </TableRowButton> + ); +} + +export default RecentFolderRow; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index ed05ed548..ca0acc56a 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -3,6 +3,7 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import naturalExpansion from 'Utilities/String/naturalExpansion'; import { set, update, updateItem } from './baseActions'; @@ -30,6 +31,7 @@ export const defaultState = { originalItems: [], sortKey: 'relativePath', sortDirection: sortDirections.ASCENDING, + favoriteFolders: [], recentFolders: [], importMode: 'chooseImportMode', sortPredicates: { @@ -58,6 +60,7 @@ export const defaultState = { export const persistState = [ 'interactiveImport.sortKey', 'interactiveImport.sortDirection', + 'interactiveImport.favoriteFolders', 'interactiveImport.recentFolders', 'interactiveImport.importMode' ]; @@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport'; export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; +export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder'; +export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; // @@ -86,6 +91,8 @@ export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPO export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT); export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); +export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER); +export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); // @@ -268,9 +275,31 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { recentFolders }); }, + [ADD_FAVORITE_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const favoriteFolder = { folder }; + const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder')); + + return Object.assign({}, state, { favoriteFolders }); + }, + + [REMOVE_FAVORITE_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const favoriteFolders = state.favoriteFolders.reduce((acc, item) => { + if (item.folder !== folder) { + acc.push(item); + } + + return acc; + }, []); + + return Object.assign({}, state, { favoriteFolders }); + }, + [CLEAR_INTERACTIVE_IMPORT]: function(state) { const newState = { ...defaultState, + favoriteFolders: state.favoriteFolders, recentFolders: state.recentFolders, importMode: state.importMode }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a71c40cda..41969de5d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -690,6 +690,9 @@ "FailedToLoadTranslationsFromApi": "Failed to load translations from API", "FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API", "False": "False", + "FavoriteFolderAdd": "Add Favorite Folder", + "FavoriteFolderRemove": "Remove Favorite Folder", + "FavoriteFolders": "Favorite Folders", "FeatureRequests": "Feature Requests", "File": "File", "FileBrowser": "File Browser", @@ -1621,6 +1624,7 @@ "Real": "Real", "Reason": "Reason", "RecentChanges": "Recent Changes", + "RecentFolders": "Recent Folders", "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {path}. Ensure this path exists and is writable by the user running {appName}", "RecyclingBin": "Recycling Bin", "RecyclingBinCleanup": "Recycling Bin Cleanup", From 020ed32fcfab1c6fbe57af5ea650300272c93fd7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 27 Oct 2024 16:48:53 -0700 Subject: [PATCH 620/762] Use current time for cache break in development --- src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index 448fd25d4..b3f635049 100644 --- a/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Crypto; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; namespace Sonarr.Http.Frontend.Mappers @@ -28,6 +30,11 @@ namespace Sonarr.Http.Frontend.Mappers return resourceUrl; } + if (!RuntimeInfo.IsProduction) + { + return resourceUrl + "?t=" + DateTime.UtcNow.Ticks; + } + var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl)); var pathToFile = mapper.Map(resourceUrl); var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64(); From 1df0ba9e5aef2d2745a45c546c869837ac8e68db Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 28 Oct 2024 16:04:48 -0700 Subject: [PATCH 621/762] Fixed: Use download client name for history column --- frontend/src/Activity/History/HistoryRow.tsx | 7 ++++++- frontend/src/typings/History.ts | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx index 42af2833b..d1ba279dc 100644 --- a/frontend/src/Activity/History/HistoryRow.tsx +++ b/frontend/src/Activity/History/HistoryRow.tsx @@ -195,9 +195,14 @@ function HistoryRow(props: HistoryRowProps) { } if (name === 'downloadClient') { + const downloadClientName = + 'downloadClientName' in data ? data.downloadClientName : null; + const downloadClient = + 'downloadClient' in data ? data.downloadClient : null; + return ( <TableRowCell key={name} className={styles.downloadClient}> - {'downloadClient' in data ? data.downloadClient : ''} + {downloadClientName ?? downloadClient ?? ''} </TableRowCell> ); } diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts index d20895f37..bebde55c0 100644 --- a/frontend/src/typings/History.ts +++ b/frontend/src/typings/History.ts @@ -40,6 +40,8 @@ export interface DownloadFailedHistory { export interface DownloadFolderImportedHistory { customFormatScore?: string; + downloadClient: string; + downloadClientName: string; droppedPath: string; importedPath: string; } From 73208e2f60263b1236f094a2bf6c47ebd5a8a271 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 28 Oct 2024 16:05:57 -0700 Subject: [PATCH 622/762] New: Include source path with Webhook import event episode file --- src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs | 5 ++++- .../Notifications/Webhook/WebhookEpisodeFile.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index 71fe1bff0..9b2fe22fa 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -60,7 +60,10 @@ namespace NzbDrone.Core.Notifications.Webhook ApplicationUrl = _configService.ApplicationUrl, Series = GetSeries(message.Series), Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)), - EpisodeFile = new WebhookEpisodeFile(episodeFile), + EpisodeFile = new WebhookEpisodeFile(episodeFile) + { + SourcePath = message.SourcePath + }, Release = new WebhookGrabbedRelease(message.Release), IsUpgrade = message.OldFiles.Any(), DownloadClient = message.DownloadClientInfo?.Name, diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs index c0348931b..c1a0d2364 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs @@ -37,6 +37,7 @@ namespace NzbDrone.Core.Notifications.Webhook public long Size { get; set; } public DateTime DateAdded { get; set; } public WebhookEpisodeFileMediaInfo MediaInfo { get; set; } + public string SourcePath { get; set; } public string RecycleBinPath { get; set; } } } From 22005dc8c500cd77e4a710248582cd4a0036988f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:57:43 +0200 Subject: [PATCH 623/762] =?UTF-8?q?Fixed:=20Cleaning=20the=20French=20prep?= =?UTF-8?q?osition=20'=C3=A0'=20from=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParserTests/NormalizeSeriesTitleFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs index 4c9b683b6..1c47b4fe8 100644 --- a/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs @@ -24,6 +24,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("test/test", "testtest")] [TestCase("90210", "90210")] [TestCase("24", "24")] + [TestCase("Test: Something à Deux", "testsomethingdeux")] + [TestCase("Parler à", "parlera")] public void should_remove_special_characters_and_casing(string dirty, string clean) { var result = dirty.CleanSeriesTitle(); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 12d481c0d..01ab47b5d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -517,7 +517,7 @@ namespace NzbDrone.Core.Parser // Regex to detect whether the title was reversed. private static readonly Regex ReversedTitleRegex = new Regex(@"(?:^|[-._ ])(p027|p0801|\d{2,3}E\d{2}S)[-._ ]", RegexOptions.Compiled); - private static readonly RegexReplace NormalizeRegex = new RegexReplace(@"((?:\b|_)(?<!^)(a(?!$)|an|the|and|or|of)(?!$)(?:\b|_))|\W|_", + private static readonly RegexReplace NormalizeRegex = new RegexReplace(@"((?:\b|_)(?<!^)([aà](?!$)|an|the|and|or|of)(?!$)(?:\b|_))|\W|_", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -845,7 +845,7 @@ namespace NzbDrone.Core.Parser // Replace `%` with `percent` to deal with the 3% case title = PercentRegex.Replace(title, "percent"); - return NormalizeRegex.Replace(title).ToLower().RemoveAccent(); + return NormalizeRegex.Replace(title).ToLowerInvariant().RemoveAccent(); } public static string NormalizeEpisodeTitle(string title) From 38c0135d7cd05b22bede934f8571c439dcf70a88 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:16:33 +0200 Subject: [PATCH 624/762] Fixed: Loading queue with pending releases for deleted series --- .../Datastore/Extensions/BuilderExtensions.cs | 14 ++++++++++---- .../Download/Pending/PendingReleaseRepository.cs | 7 ++++++- .../Download/Pending/PendingReleaseService.cs | 7 ++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs index 3bff36b7a..66dd34adc 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs @@ -64,19 +64,25 @@ namespace NzbDrone.Core.Datastore public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) { var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); - var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); - return builder.Join($"\"{rightTable}\" ON {wb.ToString()}"); + return builder.Join($"\"{rightTable}\" ON {wb}"); } public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) { var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); - var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); - return builder.LeftJoin($"\"{rightTable}\" ON {wb.ToString()}"); + return builder.LeftJoin($"\"{rightTable}\" ON {wb}"); + } + + public static SqlBuilder InnerJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) + { + var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); + var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); + + return builder.InnerJoin($"\"{rightTable}\" ON {wb}"); } public static SqlBuilder GroupBy<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property) diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs index 5545a2f52..1db5cf5a7 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download.Pending { @@ -30,7 +31,11 @@ namespace NzbDrone.Core.Download.Pending public List<PendingRelease> WithoutFallback() { - return Query(p => p.Reason != PendingReleaseReason.Fallback); + var builder = new SqlBuilder(_database.DatabaseType) + .InnerJoin<PendingRelease, Series>((p, s) => p.SeriesId == s.Id) + .Where<PendingRelease>(p => p.Reason != PendingReleaseReason.Fallback); + + return Query(builder); } } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 624dbdf46..8b6592f73 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -274,10 +274,7 @@ namespace NzbDrone.Core.Download.Pending { foreach (var series in knownRemoteEpisodes.Values.Select(v => v.Series)) { - if (!seriesMap.ContainsKey(series.Id)) - { - seriesMap[series.Id] = series; - } + seriesMap.TryAdd(series.Id, series); } } @@ -293,7 +290,7 @@ namespace NzbDrone.Core.Download.Pending // Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up) if (series == null) { - return null; + continue; } // Languages will be empty if added before upgrading to v4, reparsing the languages if they're empty will set it to Unknown or better. From 8e636d7a37043f3abb209ec1c0c61c0ac6693ba4 Mon Sep 17 00:00:00 2001 From: Aviad Levy <aviadlevy1@gmail.com> Date: Fri, 1 Nov 2024 07:48:04 +0200 Subject: [PATCH 625/762] Fixed: Telegram notification link text --- src/NzbDrone.Core/Notifications/Telegram/Telegram.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index ba0c2d6dc..91b37000b 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -128,12 +128,12 @@ namespace NzbDrone.Core.Notifications.Telegram if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0) { - links.Add(new TelegramLink("TVMaze", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show")); + links.Add(new TelegramLink("Trakt", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show")); } if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0) { - links.Add(new TelegramLink("Trakt", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_")); + links.Add(new TelegramLink("TVMaze", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_")); } } From 409823c7e8df510623246d9816b67639b29fb6a6 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:33:13 +0200 Subject: [PATCH 626/762] Fixed: Interactive searches when using Escape to close previous searches --- .../src/InteractiveSearch/InteractiveSearch.tsx | 16 ++++++++++------ .../Search/SeasonInteractiveSearchModal.tsx | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx index 6dd3c2f1f..9dff36198 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -160,13 +160,17 @@ function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) { [dispatch] ); - useEffect(() => { - // Only fetch releases if they are not already being fetched and not yet populated. + useEffect( + () => { + // Only fetch releases if they are not already being fetched and not yet populated. - if (!isFetching && !isPopulated) { - dispatch(fetchReleases(searchPayload)); - } - }, [isFetching, isPopulated, searchPayload, dispatch]); + if (!isFetching && !isPopulated) { + dispatch(fetchReleases(searchPayload)); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); const errorMessage = getErrorMessage(error); diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx b/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx index babe59469..bc2a5b753 100644 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx @@ -23,10 +23,10 @@ function SeasonInteractiveSearchModal( const dispatch = useDispatch(); const handleModalClose = useCallback(() => { + onModalClose(); + dispatch(cancelFetchReleases()); dispatch(clearReleases()); - - onModalClose(); }, [dispatch, onModalClose]); useEffect(() => { From 8d4ba77b12991e96945c73dc7fef83c6b6075b6e Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:43:31 +0200 Subject: [PATCH 627/762] Fixed: New values for custom filters --- frontend/src/Components/Form/Tag/TagInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Components/Form/Tag/TagInput.tsx b/frontend/src/Components/Form/Tag/TagInput.tsx index c113c06d3..bde24f369 100644 --- a/frontend/src/Components/Form/Tag/TagInput.tsx +++ b/frontend/src/Components/Form/Tag/TagInput.tsx @@ -26,7 +26,7 @@ import TagInputTag, { EditedTag, TagInputTagProps } from './TagInputTag'; import styles from './TagInput.css'; export interface TagBase { - id: boolean | number | string; + id: boolean | number | string | null; name: string | number; } @@ -44,7 +44,7 @@ function getTag<T extends { id: T['id']; name: T['name'] }>( if (existingTag) { return existingTag; } else if (allowNew) { - return { id: 0, name: value } as T; + return { name: value } as T; } } else if (selectedIndex != null) { return suggestions[selectedIndex]; From 832de3e75e7041d25e8dd29ee35d0142adf6ee5a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:01:10 +0200 Subject: [PATCH 628/762] Fixed: Root folder existence for import lists health check --- .../Checks/ImportListRootFolderCheck.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs index fc4ee7826..ea9ceffbc 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs @@ -1,13 +1,20 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent<IImportList>))] + [CheckOn(typeof(ProviderDeletedEvent<IImportList>))] + [CheckOn(typeof(ModelEvent<RootFolder>))] [CheckOn(typeof(SeriesDeletedEvent))] [CheckOn(typeof(SeriesMovedEvent))] [CheckOn(typeof(EpisodeImportedEvent), CheckOnCondition.FailedOnly)] @@ -16,17 +23,21 @@ namespace NzbDrone.Core.HealthCheck.Checks { private readonly IImportListFactory _importListFactory; private readonly IDiskProvider _diskProvider; + private readonly IRootFolderService _rootFolderService; - public ImportListRootFolderCheck(IImportListFactory importListFactory, IDiskProvider diskProvider, ILocalizationService localizationService) + public ImportListRootFolderCheck(IImportListFactory importListFactory, IDiskProvider diskProvider, IRootFolderService rootFolderService, ILocalizationService localizationService) : base(localizationService) { _importListFactory = importListFactory; _diskProvider = diskProvider; + _rootFolderService = rootFolderService; } public override HealthCheck Check() { var importLists = _importListFactory.All(); + var rootFolders = _rootFolderService.All(); + var missingRootFolders = new Dictionary<string, List<ImportListDefinition>>(); foreach (var importList in importLists) @@ -40,7 +51,10 @@ namespace NzbDrone.Core.HealthCheck.Checks continue; } - if (!_diskProvider.FolderExists(rootFolderPath)) + if (rootFolderPath.IsNullOrWhiteSpace() || + !rootFolderPath.IsPathValid(PathValidationType.CurrentOs) || + !rootFolders.Any(r => r.Path.PathEquals(rootFolderPath)) || + !_diskProvider.FolderExists(rootFolderPath)) { missingRootFolders.Add(rootFolderPath, new List<ImportListDefinition> { importList }); } From a77bf6435246dd6cafcea7a3f62842b076366e4a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 14:01:15 -0800 Subject: [PATCH 629/762] New: Monitor New Seasons column for series list Closes #7311 --- frontend/src/Series/Index/Table/SeriesIndexRow.css | 6 ++++++ .../src/Series/Index/Table/SeriesIndexRow.css.d.ts | 1 + frontend/src/Series/Index/Table/SeriesIndexRow.tsx | 11 +++++++++++ .../src/Series/Index/Table/SeriesIndexTableHeader.css | 6 ++++++ .../Index/Table/SeriesIndexTableHeader.css.d.ts | 1 + frontend/src/Store/Actions/seriesIndexActions.js | 10 ++++++++++ 6 files changed, 35 insertions(+) diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css b/frontend/src/Series/Index/Table/SeriesIndexRow.css index 1ad943161..7098911d6 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.css +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css @@ -148,6 +148,12 @@ flex: 0 0 145px; } +.monitorNewItems { + composes: cell; + + flex: 0 0 175px; +} + .actions { composes: cell; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts index 0d507efd4..e07bb3cac 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts @@ -14,6 +14,7 @@ interface CssExports { 'genres': string; 'latestSeason': string; 'link': string; + 'monitorNewItems': string; 'network': string; 'nextAiring': string; 'originalLanguage': string; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 0e6548535..727e5b4d0 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -55,6 +55,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { const { title, monitored, + monitorNewItems, status, path, titleSlug, @@ -450,6 +451,16 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { ); } + if (name === 'monitorNewItems') { + return ( + <VirtualTableRowCell key={name} className={styles[name]}> + {monitorNewItems === 'all' + ? translate('SeasonsMonitoredAll') + : translate('SeasonsMonitoredNone')} + </VirtualTableRowCell> + ); + } + if (name === 'actions') { return ( <VirtualTableRowCell key={name} className={styles[name]}> diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css index dc8a171c1..8e3b8f751 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css @@ -109,6 +109,12 @@ flex: 0 0 145px; } +.monitorNewItems { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 175px; +} + .actions { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts index 208fa4a20..5cff4a8ec 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts @@ -10,6 +10,7 @@ interface CssExports { 'episodeProgress': string; 'genres': string; 'latestSeason': string; + 'monitorNewItems': string; 'network': string; 'nextAiring': string; 'originalLanguage': string; diff --git a/frontend/src/Store/Actions/seriesIndexActions.js b/frontend/src/Store/Actions/seriesIndexActions.js index cc1d7c574..90451afc9 100644 --- a/frontend/src/Store/Actions/seriesIndexActions.js +++ b/frontend/src/Store/Actions/seriesIndexActions.js @@ -194,6 +194,12 @@ export const defaultState = { isSortable: true, isVisible: false }, + { + name: 'monitorNewItems', + label: () => translate('MonitorNewSeasons'), + isSortable: true, + isVisible: false + }, { name: 'actions', columnLabel: () => translate('Actions'), @@ -274,6 +280,10 @@ export const defaultState = { const { ratings = {} } = item; return ratings.value; + }, + + monitorNewItems: function(item) { + return item.monitorNewItems === 'all' ? 1 : 0; } }, From 978349e24135572889095c743d0e7fac734ba7e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 14:43:50 -0800 Subject: [PATCH 630/762] New: Reject files during import that have no audio tracks Closes #7298 --- .../HasAudioTrackSpecificationFixture.cs | 70 +++++++++++++++++++ .../HasAudioTrackSpecification.cs | 35 ++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecificationFixture.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecificationFixture.cs new file mode 100644 index 000000000..eba293380 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecificationFixture.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications +{ + [TestFixture] + public class HasAudioTrackSpecificationFixture : CoreTest<HasAudioTrackSpecification> + { + private Series _series; + private LocalEpisode _localEpisode; + private string _rootFolder; + + [SetUp] + public void Setup() + { + _rootFolder = @"C:\Test\TV".AsOsAgnostic(); + + _series = Builder<Series>.CreateNew() + .With(s => s.SeriesType = SeriesTypes.Standard) + .With(s => s.Path = Path.Combine(_rootFolder, "30 Rock")) + .Build(); + + var episodes = Builder<Episode>.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + _localEpisode = new LocalEpisode + { + Path = @"C:\Test\Unsorted\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), + Episodes = episodes, + Series = _series + }; + } + + [Test] + public void should_accept_if_media_info_is_null() + { + _localEpisode.MediaInfo = null; + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_reject_if_audio_stream_count_is_0() + { + _localEpisode.MediaInfo = Builder<MediaInfoModel>.CreateNew().With(m => m.AudioStreamCount = 0).Build(); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_accept_if_audio_stream_count_is_0() + { + _localEpisode.MediaInfo = Builder<MediaInfoModel>.CreateNew().With(m => m.AudioStreamCount = 1).Build(); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs new file mode 100644 index 000000000..4a66eeea1 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs @@ -0,0 +1,35 @@ +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class HasAudioTrackSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public HasAudioTrackSpecification(Logger logger) + { + _logger = logger; + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + if (localEpisode.MediaInfo == null) + { + _logger.Debug("Failed to get media info from the file, make sure ffprobe is available, skipping check"); + return Decision.Accept(); + } + + if (localEpisode.MediaInfo.AudioStreamCount == 0) + { + _logger.Debug("No audio tracks found in file"); + + return Decision.Reject("No audio tracks detected"); + } + + return Decision.Accept(); + } + } +} From 78cf13d341e6690bf6079dd1819d060d002155a7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 14:46:31 -0800 Subject: [PATCH 631/762] Increase retries for DebouncerFixture --- src/NzbDrone.Common.Test/TPLTests/DebouncerFixture.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common.Test/TPLTests/DebouncerFixture.cs b/src/NzbDrone.Common.Test/TPLTests/DebouncerFixture.cs index c8844b2f3..d61381f13 100644 --- a/src/NzbDrone.Common.Test/TPLTests/DebouncerFixture.cs +++ b/src/NzbDrone.Common.Test/TPLTests/DebouncerFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using FluentAssertions; using NUnit.Framework; @@ -20,7 +20,7 @@ namespace NzbDrone.Common.Test.TPLTests } [Test] - [Retry(3)] + [Retry(10)] public void should_hold_the_call_for_debounce_duration() { var counter = new Counter(); @@ -38,7 +38,7 @@ namespace NzbDrone.Common.Test.TPLTests } [Test] - [Retry(3)] + [Retry(10)] public void should_throttle_calls() { var counter = new Counter(); @@ -62,7 +62,7 @@ namespace NzbDrone.Common.Test.TPLTests } [Test] - [Retry(3)] + [Retry(10)] public void should_hold_the_call_while_paused() { var counter = new Counter(); @@ -96,7 +96,7 @@ namespace NzbDrone.Common.Test.TPLTests } [Test] - [Retry(3)] + [Retry(10)] public void should_handle_pause_reentrancy() { var counter = new Counter(); From b8af3af9f16db96337832c2989f4e7ff3dc2ed30 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 15:24:06 -0800 Subject: [PATCH 632/762] Fixed: Filtering queue by multiple qualities --- src/Sonarr.Api.V3/Queue/QueueController.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 96917878e..0fc12bb14 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -136,7 +136,7 @@ namespace Sonarr.Api.V3.Queue [HttpGet] [Produces("application/json")] - public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, int? quality = null) + public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) { var pagingResource = new PagingResource<QueueResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>( @@ -165,10 +165,10 @@ namespace Sonarr.Api.V3.Queue "timeleft", SortDirection.Ascending); - return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality?.ToHashSet(), includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } - private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> seriesIds, DownloadProtocol? protocol, HashSet<int> languages, int? quality, bool includeUnknownSeriesItems) + private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> seriesIds, DownloadProtocol? protocol, HashSet<int> languages, HashSet<int> quality, bool includeUnknownSeriesItems) { var ascending = pagingSpec.SortDirection == SortDirection.Ascending; var orderByFunc = GetOrderByFunc(pagingSpec); @@ -179,6 +179,8 @@ namespace Sonarr.Api.V3.Queue var hasSeriesIdFilter = seriesIds.Any(); var hasLanguageFilter = languages.Any(); + var hasQualityFilter = quality.Any(); + var fullQueue = filteredQueue.Concat(pending).Where(q => { var include = true; @@ -198,9 +200,9 @@ namespace Sonarr.Api.V3.Queue include &= q.Languages.Any(l => languages.Contains(l.Id)); } - if (include && quality.HasValue) + if (include && hasQualityFilter) { - include &= q.Quality.Quality.Id == quality.Value; + include &= quality.Contains(q.Quality.Quality.Id); } return include; @@ -282,7 +284,7 @@ namespace Sonarr.Api.V3.Queue switch (pagingSpec.SortKey) { case "status": - return q => q.Status; + return q => q.Status.ToString(); case "series.sortTitle": return q => q.Series?.SortTitle ?? q.Title; case "title": From fb540040ef66e90c55b82539b85df378d6c76bd3 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 15:28:48 -0800 Subject: [PATCH 633/762] New: Filter queue by status Closes #7196 --- .../Filter/Builder/FilterBuilderRow.js | 4 ++ .../QueueStatusFilterBuilderRowValue.tsx | 67 +++++++++++++++++++ .../SeriesStatusFilterBuilderRowValue.js | 4 +- .../Helpers/Props/filterBuilderValueTypes.js | 1 + frontend/src/Store/Actions/queueActions.js | 6 ++ .../Download/Pending/PendingReleaseService.cs | 3 +- src/NzbDrone.Core/Localization/Core/en.json | 4 ++ src/NzbDrone.Core/Queue/Queue.cs | 2 +- src/NzbDrone.Core/Queue/QueueService.cs | 2 +- src/NzbDrone.Core/Queue/QueueStatus.cs | 16 +++++ src/Sonarr.Api.V3/Queue/QueueController.cs | 12 +++- src/Sonarr.Api.V3/Queue/QueueResource.cs | 6 +- 12 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx create mode 100644 src/NzbDrone.Core/Queue/QueueStatus.cs diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 7110dddf3..0b00c0f03 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -13,6 +13,7 @@ import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; +import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue'; import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue'; import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; @@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.QUALITY_PROFILE: return QualityProfileFilterBuilderRowValue; + case filterBuilderValueTypes.QUEUE_STATUS: + return QueueStatusFilterBuilderRowValue; + case filterBuilderValueTypes.SEASONS_MONITORED_STATUS: return SeasonsMonitoredStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx new file mode 100644 index 000000000..1127493a5 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +const statusTagList = [ + { + id: 'queued', + get name() { + return translate('Queued'); + }, + }, + { + id: 'paused', + get name() { + return translate('Paused'); + }, + }, + { + id: 'downloading', + get name() { + return translate('Downloading'); + }, + }, + { + id: 'completed', + get name() { + return translate('Completed'); + }, + }, + { + id: 'failed', + get name() { + return translate('Failed'); + }, + }, + { + id: 'warning', + get name() { + return translate('Warning'); + }, + }, + { + id: 'delay', + get name() { + return translate('Delay'); + }, + }, + { + id: 'downloadClientUnavailable', + get name() { + return translate('DownloadClientUnavailable'); + }, + }, + { + id: 'fallback', + get name() { + return translate('Fallback'); + }, + }, +]; + +function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + return <FilterBuilderRowValue {...props} tagList={statusTagList} />; +} + +export default QueueStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js index 3464300f1..e017f72e7 100644 --- a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js @@ -2,7 +2,7 @@ import React from 'react'; import translate from 'Utilities/String/translate'; import FilterBuilderRowValue from './FilterBuilderRowValue'; -const seriesStatusList = [ +const statusTagList = [ { id: 'continuing', get name() { @@ -32,7 +32,7 @@ const seriesStatusList = [ function SeriesStatusFilterBuilderRowValue(props) { return ( <FilterBuilderRowValue - tagList={seriesStatusList} + tagList={statusTagList} {...props} /> ); diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index d9a5d58c7..a6666ba03 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -8,6 +8,7 @@ export const LANGUAGE = 'language'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; +export const QUEUE_STATUS = 'queueStatus'; export const SEASONS_MONITORED_STATUS = 'seasonsMonitoredStatus'; export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 5f91318ad..ca97e5213 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -212,6 +212,12 @@ export const defaultState = { label: () => translate('Protocol'), type: filterBuilderTypes.EQUAL, valueType: filterBuilderValueTypes.PROTOCOL + }, + { + name: 'status', + label: () => translate('Status'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.QUEUE_STATUS } ] } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 8b6592f73..47d8d0755 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -15,6 +15,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Queue; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -389,7 +390,7 @@ namespace NzbDrone.Core.Download.Pending Timeleft = timeleft, EstimatedCompletionTime = ect, Added = pendingRelease.Added, - Status = pendingRelease.Reason.ToString(), + Status = Enum.TryParse(pendingRelease.Reason.ToString(), out QueueStatus outValue) ? outValue : QueueStatus.Unknown, Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol, Indexer = pendingRelease.RemoteEpisode.Release.Indexer, DownloadClient = downloadClientName diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 41969de5d..a478ad5b3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -323,6 +323,7 @@ "DefaultNameCopiedProfile": "{name} - Copy", "DefaultNameCopiedSpecification": "{name} - Copy", "DefaultNotFoundMessage": "You must be lost, nothing to see here.", + "Delay": "Delay", "DelayMinutes": "{delay} Minutes", "DelayProfile": "Delay Profile", "DelayProfileProtocol": "Protocol: {preferredProtocol}", @@ -541,6 +542,7 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", + "DownloadClientUnavailable": "Download Client Unavailable", "DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error", "DownloadClientValidationApiKeyIncorrect": "API Key Incorrect", "DownloadClientValidationApiKeyRequired": "API Key Required", @@ -689,6 +691,7 @@ "FailedToLoadTagsFromApi": "Failed to load tags from API", "FailedToLoadTranslationsFromApi": "Failed to load translations from API", "FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API", + "Fallback": "Fallback", "False": "False", "FavoriteFolderAdd": "Add Favorite Folder", "FavoriteFolderRemove": "Remove Favorite Folder", @@ -2114,6 +2117,7 @@ "WantMoreControlAddACustomFormat": "Want more control over which downloads are preferred? Add a [Custom Format](/settings/customformats)", "Wanted": "Wanted", "Warn": "Warn", + "Warning": "Warning", "Week": "Week", "WeekColumnHeader": "Week Column Header", "WeekColumnHeaderHelpText": "Shown above each column when week is the active view", diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index c5d2a123a..c38749678 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Queue public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public DateTime? Added { get; set; } - public string Status { get; set; } + public QueueStatus Status { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } public TrackedDownloadState? TrackedDownloadState { get; set; } public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 8bb11a13c..7142bd03c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -69,7 +69,7 @@ namespace NzbDrone.Core.Queue Size = trackedDownload.DownloadItem.TotalSize, Sizeleft = trackedDownload.DownloadItem.RemainingSize, Timeleft = trackedDownload.DownloadItem.RemainingTime, - Status = trackedDownload.DownloadItem.Status.ToString(), + Status = Enum.TryParse(trackedDownload.DownloadItem.Status.ToString(), out QueueStatus outValue) ? outValue : QueueStatus.Unknown, TrackedDownloadStatus = trackedDownload.Status, TrackedDownloadState = trackedDownload.State, StatusMessages = trackedDownload.StatusMessages.ToList(), diff --git a/src/NzbDrone.Core/Queue/QueueStatus.cs b/src/NzbDrone.Core/Queue/QueueStatus.cs new file mode 100644 index 000000000..77b0751d9 --- /dev/null +++ b/src/NzbDrone.Core/Queue/QueueStatus.cs @@ -0,0 +1,16 @@ +namespace NzbDrone.Core.Queue +{ + public enum QueueStatus + { + Unknown, + Queued, + Paused, + Downloading, + Completed, + Failed, + Warning, + Delay, + DownloadClientUnavailable, + Fallback + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 0fc12bb14..6c7438203 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -136,7 +136,7 @@ namespace Sonarr.Api.V3.Queue [HttpGet] [Produces("application/json")] - public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) + public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null, [FromQuery] QueueStatus[] status = null) { var pagingResource = new PagingResource<QueueResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>( @@ -165,10 +165,10 @@ namespace Sonarr.Api.V3.Queue "timeleft", SortDirection.Ascending); - return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality?.ToHashSet(), includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality?.ToHashSet(), status?.ToHashSet(), includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } - private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> seriesIds, DownloadProtocol? protocol, HashSet<int> languages, HashSet<int> quality, bool includeUnknownSeriesItems) + private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> seriesIds, DownloadProtocol? protocol, HashSet<int> languages, HashSet<int> quality, HashSet<QueueStatus> status, bool includeUnknownSeriesItems) { var ascending = pagingSpec.SortDirection == SortDirection.Ascending; var orderByFunc = GetOrderByFunc(pagingSpec); @@ -180,6 +180,7 @@ namespace Sonarr.Api.V3.Queue var hasSeriesIdFilter = seriesIds.Any(); var hasLanguageFilter = languages.Any(); var hasQualityFilter = quality.Any(); + var hasStatusFilter = status.Any(); var fullQueue = filteredQueue.Concat(pending).Where(q => { @@ -205,6 +206,11 @@ namespace Sonarr.Api.V3.Queue include &= quality.Contains(q.Quality.Quality.Id); } + if (include && hasStatusFilter) + { + include &= status.Contains(q.Status); + } + return include; }).ToList(); diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index e5152f1d7..5a0e47e20 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Queue; using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Series; @@ -30,7 +30,7 @@ namespace Sonarr.Api.V3.Queue public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public DateTime? Added { get; set; } - public string Status { get; set; } + public QueueStatus Status { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } public TrackedDownloadState? TrackedDownloadState { get; set; } public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } @@ -74,7 +74,7 @@ namespace Sonarr.Api.V3.Queue Timeleft = model.Timeleft, EstimatedCompletionTime = model.EstimatedCompletionTime, Added = model.Added, - Status = model.Status.FirstCharToLower(), + Status = model.Status, TrackedDownloadStatus = model.TrackedDownloadStatus, TrackedDownloadState = model.TrackedDownloadState, StatusMessages = model.StatusMessages, From 59f3be08137d10254affb4bfef04fb7aa8646d02 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:12:52 +0200 Subject: [PATCH 634/762] Show a series path as example in Mount Health Check --- src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs index 46f1ea103..a37200e6c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NzbDrone.Common.Disk; using NzbDrone.Core.Localization; @@ -21,16 +22,16 @@ namespace NzbDrone.Core.HealthCheck.Checks { // Not best for optimization but due to possible symlinks and junctions, we get mounts based on series path so internals can handle mount resolution. var mounts = _seriesService.GetAllSeriesPaths() - .Select(s => _diskProvider.GetMount(s.Value)) - .Where(m => m is { MountOptions.IsReadOnly: true }) - .DistinctBy(m => m.RootDirectory) + .Select(p => new Tuple<IMount, string>(_diskProvider.GetMount(p.Value), p.Value)) + .Where(m => m.Item1 is { MountOptions.IsReadOnly: true }) + .DistinctBy(m => m.Item1.RootDirectory) .ToList(); if (mounts.Any()) { return new HealthCheck(GetType(), HealthCheckResult.Error, - $"{_localizationService.GetLocalizedString("MountSeriesHealthCheckMessage")}{string.Join(", ", mounts.Select(m => m.Name))}", + $"{_localizationService.GetLocalizedString("MountSeriesHealthCheckMessage")}{string.Join(", ", mounts.Select(m => $"{m.Item1.Name} ({m.Item2})"))}", "#series-mount-ro"); } From 4e9ef57e3d8c923f765b3337279a0cf4e93d6068 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 4 Nov 2024 04:48:46 +0000 Subject: [PATCH 635/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: mytelegrambot <lacsonluxur@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ko/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 5 ++++- src/NzbDrone.Core/Localization/Core/ko.json | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 018694912..566c0bfe2 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2119,5 +2119,8 @@ "NotificationsGotifySettingsPreferredMetadataLink": "Enlace de metadatos preferido", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Enlace de metadatos para clientes que solo soportan un único enlace", "SkipFreeSpaceCheckHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz", - "FolderNameTokens": "Tokens de nombre de carpeta" + "FolderNameTokens": "Tokens de nombre de carpeta", + "FailedToFetchSettings": "Error al recuperar la configuración", + "MetadataPlexSettingsEpisodeMappings": "Asignaciones de episodios", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Incluye las asignaciones de episodios para todos los archivos en el archivo .plexmatch" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 439c144d4..9956d460d 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -12,5 +12,19 @@ "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다", "AddNew": "새로 추가하기", "History": "내역", - "Sunday": "일요일" + "Sunday": "일요일", + "ApiKeyValidationHealthCheckMessage": "API 키를 {length}자 이상으로 업데이트하세요. 설정 또는 구성 파일을 통해 이 작업을 수행할 수 있습니다.", + "Added": "추가됨", + "AddConnection": "연결 추가", + "AddConnectionImplementation": "연결 추가 - {implementationName}", + "AddCustomFilter": "커스텀 필터 추가", + "AddDownloadClientImplementation": "다운로드 클라이언트 추가 - {implementationName}", + "AddIndexer": "인덱서 추가", + "AddIndexerImplementation": "인덱서 추가 - {implementationName}", + "Any": "모두", + "AppUpdated": "{appName} 업데이트", + "AddingTag": "태그 추가", + "Analytics": "분석", + "Age": "연령", + "All": "모두" } From ae7c07e02fe0d2ee252517622828f974a187852e Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 4 Nov 2024 04:56:06 +0000 Subject: [PATCH 636/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 35 ++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index be98294a0..e66ef48f3 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -5931,8 +5931,21 @@ "name": "quality", "in": "query", "schema": { - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueStatus" + } } } ], @@ -10855,8 +10868,7 @@ "nullable": true }, "status": { - "type": "string", - "nullable": true + "$ref": "#/components/schemas/QueueStatus" }, "trackedDownloadStatus": { "$ref": "#/components/schemas/TrackedDownloadStatus" @@ -10935,6 +10947,21 @@ }, "additionalProperties": false }, + "QueueStatus": { + "enum": [ + "unknown", + "queued", + "paused", + "downloading", + "completed", + "failed", + "warning", + "delay", + "downloadClientUnavailable", + "fallback" + ], + "type": "string" + }, "QueueStatusResource": { "type": "object", "properties": { From 45a62a2e59cc3b8f01274174b1bf66236629455c Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 10 Nov 2024 04:08:42 +0000 Subject: [PATCH 637/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ardenet <1213193613@qq.com> Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 10 +- .../Localization/Core/pt_BR.json | 702 +++++++++--------- src/NzbDrone.Core/Localization/Core/ro.json | 10 +- src/NzbDrone.Core/Localization/Core/tr.json | 23 +- .../Localization/Core/zh_CN.json | 3 +- 5 files changed, 397 insertions(+), 351 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 566c0bfe2..e6fc85ab3 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2122,5 +2122,13 @@ "FolderNameTokens": "Tokens de nombre de carpeta", "FailedToFetchSettings": "Error al recuperar la configuración", "MetadataPlexSettingsEpisodeMappings": "Asignaciones de episodios", - "MetadataPlexSettingsEpisodeMappingsHelpText": "Incluye las asignaciones de episodios para todos los archivos en el archivo .plexmatch" + "MetadataPlexSettingsEpisodeMappingsHelpText": "Incluye las asignaciones de episodios para todos los archivos en el archivo .plexmatch", + "RecentFolders": "Carpetas recientes", + "Warning": "Aviso", + "Delay": "Retardo", + "DownloadClientUnavailable": "Cliente de descarga no disponible", + "FavoriteFolders": "Carpetas favoritas", + "ManageFormats": "Gestionar formatos", + "FavoriteFolderAdd": "Añadir carpeta favorita", + "FavoriteFolderRemove": "Eliminar carpeta favorita" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index e1d508b26..8c52145f6 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1,6 +1,6 @@ { - "ApplyChanges": "Aplicar Mudanças", - "AutomaticAdd": "Adição Automática", + "ApplyChanges": "Aplicar mudanças", + "AutomaticAdd": "Adição automática", "CountSeasons": "{count} Temporadas", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Nenhum cliente de download está disponível", "DownloadClientStatusAllClientHealthCheckMessage": "Todos os clientes de download estão indisponíveis devido a falhas", @@ -11,10 +11,10 @@ "HideAdvanced": "Ocultar opções avançadas", "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", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Habilitar gerenciamento de download concluído", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com a sincronização RSS habilitada, {appName} não capturará novos lançamentos automaticamente", - "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa Automática habilitada, {appName} não fornecerá nenhum resultado de pesquisa automática", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com a sincronização RSS habilitada, o {appName} não capturará novos lançamentos automaticamente", + "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa automática habilitada, o {appName} não fornecerá nenhum resultado de pesquisa automática", "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa Interativa habilitada, {appName} não fornecerá resultados de pesquisa interativas", "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", @@ -55,23 +55,23 @@ "Added": "Adicionado", "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "RemoveCompletedDownloads": "Remover downloads concluídos", - "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na atualização", + "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {downloadClientName}. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads na pasta raiz {rootFolderPath}. Você não deve baixar para uma pasta raiz.", "DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria {appName}. Você deve desativar a classificação em seu cliente de download para evitar problemas de importação.", "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de download indisponíveis devido a falhas: {downloadClientNames}", "EditSelectedIndexers": "Editar indexadores selecionados", "EditSeries": "Editar Série", - "EnableAutomaticSearch": "Ativar pesquisa automática", + "EnableAutomaticSearch": "Ativar a pesquisa automática", "EnableInteractiveSearch": "Ativar pesquisa interativa", "HiddenClickToShow": "Oculto, clique para mostrar", "ImportListRootFolderMissingRootHealthCheckMessage": "Pasta raiz ausente para lista(s) de importação: {rootFolderInfo}", "ImportListStatusUnavailableHealthCheckMessage": "Listas indisponíveis devido a falhas: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível (Multi-computador não suportado)", - "IndexerJackettAllHealthCheckMessage": "Indexadores usando o endpont Jackett 'all' sem suporte: {indexerNames}", + "IndexerJackettAllHealthCheckMessage": "Indexadores que usam o ponto de extremidade \"all\" incompatível do Jackett: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas por mais de 6 horas", - "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos os indexadores compatíveis com rss estão temporariamente indisponíveis devido a erros recentes do indexador", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos os indexadores compatíveis com RSS estão temporariamente indisponíveis devido a erros recentes do indexador", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos os indexadores com capacidade de pesquisa estão temporariamente indisponíveis devido a erros recentes do indexador", "NextAiring": "Próxima Exibição", "OriginalLanguage": "Idioma Original", @@ -87,7 +87,7 @@ "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' não pode ser gravada pelo usuário '{userName}'.", "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' está em uma pasta de translocação do App.", "BlocklistReleases": "Lançamentos na lista de bloqueio", - "CloneCondition": "Clonar Condição", + "CloneCondition": "Clonar condição", "CloneCustomFormat": "Clonar formato personalizado", "Close": "Fechar", "Delete": "Excluir", @@ -106,9 +106,9 @@ "Required": "Requerido", "BlocklistRelease": "Lançamento na lista de bloqueio", "Add": "Adicionar", - "AddingTag": "Adicionar tag", + "AddingTag": "Adicionar etiqueta", "Apply": "Aplicar", - "ApplyTags": "Aplicar Tags", + "ApplyTags": "Aplicar etiquetas", "Cancel": "Cancelar", "CountDownloadClientsSelected": "{count} cliente(s) de download selecionado(s)", "CountImportListsSelected": "{count} lista(s) de importação selecionada(s)", @@ -116,13 +116,13 @@ "DeleteSelectedDownloadClients": "Excluir cliente(s) de download", "DeleteSelectedImportLists": "Excluir lista(s) de importação", "DeleteSelectedIndexers": "Excluir indexador(es)", - "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir {count} cliente(s) de download selecionado(s)?", - "DeleteSelectedImportListsMessageText": "Tem certeza de que deseja excluir {count} lista(s) de importação selecionada(s)?", - "ExistingTag": "Tag existente", + "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir o(s) {count} cliente(s) de download selecionado(s)?", + "DeleteSelectedImportListsMessageText": "Tem certeza de que deseja excluir a(s) {count} lista(s) de importação selecionada(s)?", + "ExistingTag": "Etiqueta existente", "Implementation": "Implementação", "Disabled": "Desabilitado", "Edit": "Editar", - "ManageClients": "Gerenciar Clientes", + "ManageClients": "Gerenciar clientes", "ManageIndexers": "Gerenciar indexadores", "ManageDownloadClients": "Gerenciar clientes de download", "ManageImportLists": "Gerenciar listas de importação", @@ -141,11 +141,11 @@ "Tags": "Tags", "AutoAdd": "Adicionar automaticamente", "RemovingTag": "Removendo a tag", - "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir {count} indexadores selecionados?", + "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir o(s) {count} indexador(es) selecionado(s)?", "RemoveCompleted": "Remoção Concluída", - "LibraryImport": "Importar para biblioteca", - "LogFiles": "Arquivos de registro", - "MediaManagement": "Gerenciamento de Mídia", + "LibraryImport": "Importar biblioteca", + "LogFiles": "Arquivos de log", + "MediaManagement": "Gerenciamento de mídia", "Metadata": "Metadados", "MetadataSource": "Fonte de Metadados", "Missing": "Ausente", @@ -158,33 +158,33 @@ "Tasks": "Tarefas", "Updates": "Atualizações", "Wanted": "Procurado", - "ApplyTagsHelpTextAdd": "Adicionar: Adicione as tags à lista existente de tags", - "ApplyTagsHelpTextReplace": "Substituir: Substitua as tags pelas tags inseridas (não digite nenhuma tag para limpar todas as tags)", - "ApplyTagsHelpTextRemove": "Remover: Remove as tags inseridas", + "ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas", + "ApplyTagsHelpTextReplace": "Substituir: substitui as etiquetas atuais pelas inseridas (deixe em branco para limpar todas as etiquetas)", + "ApplyTagsHelpTextRemove": "Remover: remove as etiquetas inseridas", "CustomFormatScore": "Pontuação do formato personalizado", "Activity": "Atividade", - "AddNew": "Adicionar Novo", + "AddNew": "Adicionar novo", "Backup": "Backup", - "Blocklist": "Lista de Bloqueio", + "Blocklist": "Lista de bloqueio", "Calendar": "Calendário", "Connect": "Conectar", "CustomFormats": "Formatos personalizados", - "CutoffUnmet": "Corte Não Alcançado", + "CutoffUnmet": "Corte não atingido", "DownloadClients": "Clientes de download", "Events": "Eventos", "General": "Geral", "History": "Histórico", - "ImportLists": "Listas de importação", + "ImportLists": "Importar listas", "Indexers": "Indexadores", "AbsoluteEpisodeNumbers": "Número(s) absoluto(s) do episódio", "AirDate": "Data de exibição", "Daily": "Diário", "Details": "Detalhes", - "AllTitles": "Todos os Títulos", + "AllTitles": "Todos os títulos", "Version": "Versão", - "ApplyTagsHelpTextHowToApplyDownloadClients": "Como aplicar tags aos clientes de download selecionados", - "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar tags às listas de importação selecionadas", - "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar tags aos indexadores selecionados", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Como aplicar etiquetas aos clientes de download selecionados", + "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar etiquetas às listas de importação selecionadas", + "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", "ApplyTagsHelpTextHowToApplySeries": "Como aplicar tags à série selecionada", "EpisodeInfo": "Info do Episódio", "EpisodeNumbers": "Número(s) do(s) Episódio(s)", @@ -210,18 +210,18 @@ "Actions": "Ações", "AppDataDirectory": "Diretório AppData", "AptUpdater": "Usar apt para instalar atualizações", - "BackupNow": "Fazer Backup Agora", + "BackupNow": "Fazer backup agora", "Backups": "Backups", - "BeforeUpdate": "Antes de atualizar", + "BeforeUpdate": "Antes da atualização", "CancelPendingTask": "Tem certeza de que deseja cancelar esta tarefa pendente?", "Clear": "Limpar", "CurrentlyInstalled": "Atualmente instalado", - "DeleteBackup": "Excluir Backup", + "DeleteBackup": "Excluir backup", "DeleteBackupMessageText": "Tem certeza de que deseja excluir o backup '{name}'?", "Discord": "Discord", - "DiskSpace": "Espaço em Disco", + "DiskSpace": "Espaço em disco", "Docker": "Docker", - "DockerUpdater": "Atualize o contêiner docker para receber a atualização", + "DockerUpdater": "Atualize o contêiner do Docker para receber a atualização", "Donations": "Doações", "DotNetVersion": ".NET", "Download": "Baixar", @@ -230,26 +230,26 @@ "Exception": "Exceção", "ExternalUpdater": "O {appName} está configurado para usar um mecanismo de atualização externo", "FailedToFetchUpdates": "Falha ao buscar atualizações", - "FeatureRequests": "Solicitações de recursos", + "FeatureRequests": "Solicitação de recursos", "Filename": "Nome do arquivo", "Fixed": "Corrigido", "Forums": "Fóruns", - "FreeSpace": "Espaço Livre", + "FreeSpace": "Espaço livre", "From": "De", - "GeneralSettings": "Configurações Gerais", - "Health": "Saúde", - "HomePage": "Página Inicial", + "GeneralSettings": "Configurações gerais", + "Health": "Integridade", + "HomePage": "Página inicial", "OnLatestVersion": "A versão mais recente do {appName} já está instalada", "InstallLatest": "Instalar o mais recente", "Interval": "Intervalo", "IRC": "IRC", - "LastDuration": "Última Duração", - "LastExecution": "Última Execução", - "LastWriteTime": "Hora da Última Gravação", + "LastDuration": "Última duração", + "LastExecution": "Última execução", + "LastWriteTime": "Hora da última gravação", "Location": "Localização", "LogFilesLocation": "Os arquivos de log estão localizados em: {location}", - "Logs": "Registros", - "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Veja Github Commit History para mais detalhes", + "Logs": "Logs", + "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commit do Github para saber mais", "Manual": "Manual", "Message": "Mensagem", "Mode": "Modo", @@ -305,14 +305,14 @@ "IRCLinkText": "#sonarr na Libera", "LiberaWebchat": "Libera Webchat", "All": "Todos", - "AudioInfo": "Info do Áudio", - "AudioLanguages": "Idiomas do Áudio", + "AudioInfo": "Informações do áudio", + "AudioLanguages": "Idiomas do áudio", "Certification": "Certificação", "Component": "Componente", "ContinuingOnly": "Continuando apenas", "Date": "Data", "Deleted": "Excluído", - "DownloadClient": "Cliente de Download", + "DownloadClient": "Cliente de download", "EndedOnly": "Terminado Apenas", "Episode": "Episódio", "EpisodeAirDate": "Data de exibição do episódio", @@ -320,7 +320,7 @@ "EpisodeProgress": "Progresso do episódio", "EpisodeTitle": "Título do episódio", "Error": "Erro", - "EventType": "Tipo de Evento", + "EventType": "Tipo de evento", "Formats": "Formatos", "Genres": "Gêneros", "Grabbed": "Obtido", @@ -364,22 +364,22 @@ "RejectionCount": "Número de rejeição", "SubtitleLanguages": "Idiomas das Legendas", "UnmonitoredOnly": "Somente Não Monitorados", - "AddAutoTag": "Adicionar tag automática", - "AddCondition": "Adicionar Condição", + "AddAutoTag": "Adicionar etiqueta automática", + "AddCondition": "Adicionar condição", "Conditions": "Condições", - "CloneAutoTag": "Clonar Tag Automática", - "DeleteAutoTag": "Excluir Tag Automática", - "DeleteAutoTagHelpText": "Tem certeza de que deseja excluir a tag automática '{name}'?", - "EditAutoTag": "Editar Tag Automática", + "CloneAutoTag": "Clonar etiqueta automática", + "DeleteAutoTag": "Excluir etiqueta automática", + "DeleteAutoTagHelpText": "Tem certeza de que deseja excluir a etiqueta automática '{name}'?", + "EditAutoTag": "Editar etiqueta automática", "Negate": "Negar", "Save": "Salvar", - "AddRootFolder": "Adicionar Pasta Raiz", - "AutoTagging": "Tagging Automática", - "DeleteRootFolder": "Excluir Pasta Raiz", + "AddRootFolder": "Adicionar pasta raiz", + "AutoTagging": "Etiquetas automáticas", + "DeleteRootFolder": "Excluir pasta raiz", "DeleteRootFolderMessageText": "Tem certeza de que deseja excluir a pasta raiz '{path}'?", "RemoveTagsAutomaticallyHelpText": "Remover tags automaticamente se as condições não forem encontradas", "RootFolders": "Pastas Raiz", - "AllResultsAreHiddenByTheAppliedFilter": "Todos os resultados são ocultados pelo filtro aplicado", + "AllResultsAreHiddenByTheAppliedFilter": "Todos os resultados estão ocultos pelo filtro aplicado", "Folder": "Pasta", "InteractiveImport": "Importação interativa", "LastUsed": "Usado por último", @@ -391,179 +391,179 @@ "SelectFolder": "Selecionar Pasta", "Unavailable": "Indisponível", "UnmappedFolders": "Pastas não mapeadas", - "AutoTaggingNegateHelpText": "se marcada, a regra de etiqueta automática não será aplicada se esta condição {implementationName} corresponder.", - "AutoTaggingRequiredHelpText": "Esta condição {implementationName} deve corresponder para que a regra de etiqueta automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} será suficiente.", + "AutoTaggingNegateHelpText": "Se marcada, a regra de etiquetas automáticas não será aplicada se corresponder à condição {implementationName}.", + "AutoTaggingRequiredHelpText": "Esta condição, {implementationName}, deve corresponder para que a regra de etiqueta automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} será suficiente.", "SomeResultsAreHiddenByTheAppliedFilter": "Alguns resultados estão ocultos pelo filtro aplicado", - "UnableToLoadAutoTagging": "Não foi possível carregar a marcação automática", + "UnableToLoadAutoTagging": "Não foi possível carregar as etiquetas automáticas", "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {indexerNames}.", - "AddConditionError": "Não foi possível adicionar uma nova condição, por favor, tente novamente.", - "AddConnection": "Adicionar Conexão", - "AddCustomFormat": "Adicionar Formato Personalizado", + "AddConditionError": "Não foi possível adicionar uma nova condição, tente novamente.", + "AddConnection": "Adicionar conexão", + "AddCustomFormat": "Adicionar formato personalizado", "AddCustomFormatError": "Não foi possível adicionar um novo formato personalizado. Tente novamente.", - "AddDelayProfile": "Adicionar Perfil de Atraso", - "AddDownloadClient": "Adicionar Cliente de Download", - "AddDownloadClientError": "Não foi possível adicionar um novo cliente de download, tente novamente.", - "AddExclusion": "Adicionar Exclusão", - "AddImportList": "Adicionar Lista de Importação", - "AddImportListExclusion": "Adicionar Exclusão de Lista de Importação", - "AddImportListExclusionError": "Não foi possível adicionar uma nova exclusão de lista de importação, tente novamente.", - "AddIndexer": "Adicionar Indexador", + "AddDelayProfile": "Adicionar perfil de atraso", + "AddDownloadClient": "Adicionar cliente de download", + "AddDownloadClientError": "Não foi possível adicionar um novo cliente de download. Tente novamente.", + "AddExclusion": "Adicionar exclusão", + "AddImportList": "Adicionar lista de importação", + "AddImportListExclusion": "Adicionar exclusão à lista de importação", + "AddImportListExclusionError": "Não foi possível adicionar uma nova exclusão à lista de importação. Tente novamente.", + "AddIndexer": "Adicionar indexador", "AddIndexerError": "Não foi possível adicionar um novo indexador. Tente novamente.", "AddList": "Adicionar Lista", "AddListError": "Não foi possível adicionar uma nova lista, tente novamente.", - "AddListExclusionError": "Não foi possível adicionar uma nova exclusão de lista, tente novamente.", + "AddListExclusionError": "Não foi possível adicionar uma nova exclusão à lista. Tente novamente.", "AddNewRestriction": "Adicionar nova restrição", - "AddNotificationError": "Não foi possível adicionar uma nova notificação, tente novamente.", + "AddNotificationError": "Não foi possível adicionar uma nova notificação. Tente novamente.", "AddQualityProfile": "Adicionar perfil de qualidade", - "AddQualityProfileError": "Não foi possível adicionar uma nova notificação, tente novamente.", - "AddReleaseProfile": "Adicionar um Perfil de Lançamento", - "AddRemotePathMapping": "Adicionar Mapeamento de Caminho Remoto", - "AddRemotePathMappingError": "Não foi possível adicionar um novo mapeamento de caminho remoto, tente novamente.", - "AfterManualRefresh": "Depois da Atualização Manual", + "AddQualityProfileError": "Não foi possível adicionar um novo perfil de qualidade. Tente novamente.", + "AddReleaseProfile": "Adicionar perfil de lançamento", + "AddRemotePathMapping": "Adicionar mapeamento de caminho remoto", + "AddRemotePathMappingError": "Não foi possível adicionar um novo mapeamento de caminho remoto. Tente novamente.", + "AfterManualRefresh": "Após a atualização manual", "Always": "Sempre", "AnalyseVideoFiles": "Analisar arquivos de vídeo", - "Analytics": "Analítica", - "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas do {appName} WebUI você usa, relatórios de erros, bem como sistema operacional e versão de tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", + "Analytics": "Análises", + "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas da interface Web do {appName} você usa, relatórios de erros, a versão do sistema operacional e do tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", "AnimeEpisodeFormat": "Formato do Episódio de Anime", - "ApiKey": "Chave API", - "ApplicationURL": "URL do Aplicativo", - "ApplicationUrlHelpText": "A URL externa deste aplicativo, incluindo http(s)://, porta e base da URL", + "ApiKey": "Chave da API", + "ApplicationURL": "URL do aplicativo", + "ApplicationUrlHelpText": "A URL externa deste aplicativo, incluindo http(s)://, porta e URL base", "AuthBasic": "Básico (pop-up do navegador)", "AuthForm": "Formulário (página de login)", "Authentication": "Autenticação", - "AuthenticationMethodHelpText": "Exigir Nome de Usuário e Senha para acessar {appName}", + "AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar o {appName}", "AuthenticationRequired": "Autenticação exigida", - "AutoRedownloadFailedHelpText": "Procurar automaticamente e tente baixar uma versão diferente", - "AutoTaggingLoadError": "Não foi possível carregar tagging automática", + "AutoRedownloadFailedHelpText": "Procurar e tentar baixar automaticamente um lançamento diferente", + "AutoTaggingLoadError": "Não foi possível carregar as etiquetas automáticas", "Automatic": "Automático", - "AutomaticSearch": "Pesquisa Automática", + "AutomaticSearch": "Pesquisa automática", "BackupFolderHelpText": "Os caminhos relativos estarão no diretório AppData do {appName}", "BackupIntervalHelpText": "Intervalo entre backups automáticos", - "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão limpos automaticamente", + "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão excluídos automaticamente", "BackupsLoadError": "Não foi possível carregar os backups", - "BindAddress": "Fixar Endereço", + "BindAddress": "Vincular endereço", "BindAddressHelpText": "Endereço IP válido, localhost ou '*' para todas as interfaces", "BlocklistLoadError": "Não foi possível carregar a lista de bloqueio", "Branch": "Ramificação", - "BranchUpdate": "Ramificação a ser usada para atualizar o {appName}", + "BranchUpdate": "Ramificação para atualizar o {appName}", "BranchUpdateMechanism": "Ramificação usada pelo mecanismo externo de atualização", - "BrowserReloadRequired": "Necessário Recarregar o Navegador", + "BrowserReloadRequired": "É necessário recarregar o navegador", "BuiltIn": "Embutido", "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação do formato personalizado", - "BypassDelayIfAboveCustomFormatScoreHelpText": "Ativar a opção de ignorar quando a versão tiver uma pontuação maior que a pontuação mínima configurada do formato personalizado", - "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação mínima de formato personalizado", - "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Pontuação mínima de formato personalizado necessária para ignorar o atraso do protocolo preferido", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Ignorar quando o lançamento tiver uma pontuação mais alta que a pontuação mínima configurada do formato personalizado", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação mínima do formato personalizado", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Pontuação mínima do formato personalizado necessária para ignorar o atraso do protocolo preferido", "BypassDelayIfHighestQuality": "Ignorar se a qualidade é mais alta", - "BypassDelayIfHighestQualityHelpText": "Ignorar o atraso quando o lançamento tiver a qualidade mais alta habilitada no perfil de qualidade com o protocolo preferencial", + "BypassDelayIfHighestQualityHelpText": "Ignorar o atraso quando o lançamento tiver a qualidade mais alta habilitada no perfil de qualidade com o protocolo preferido", "BypassProxyForLocalAddresses": "Ignorar proxy para endereços locais", "CalendarLoadError": "Não foi possível carregar o calendário", - "CertificateValidation": "Validação de Certificado", - "CertificateValidationHelpText": "Altere a rigidez da validação da certificação HTTPS. Não mude a menos que você entenda os riscos.", - "ChangeFileDate": "Alterar Data do Arquivo", - "ChangeFileDateHelpText": "Alterar a data do arquivo na importação/rescan", - "ChmodFolder": "chmod Pasta", + "CertificateValidation": "Validação de certificado", + "CertificateValidationHelpText": "Alterar a rigidez da validação da certificação HTTPS. Não mude a menos que você entenda os riscos.", + "ChangeFileDate": "Alterar data do arquivo", + "ChangeFileDateHelpText": "Alterar a data do arquivo ao importar/verificar novamente", + "ChmodFolder": "Fazer chmod na pasta", "ChmodFolderHelpText": "Octal, aplicado durante a importação/renomeação de pastas e arquivos de mídia (sem bits de execução)", - "ChmodFolderHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download defina as permissões corretamente.", + "ChmodFolderHelpTextWarning": "Isso só funciona se o usuário que executa o {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download defina as permissões corretamente.", "ChownGroup": "Fazer chown em grupo", "ChownGroupHelpText": "Nome do grupo ou gid. Use gid para sistemas de arquivos remotos.", - "ChownGroupHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download use o mesmo grupo que {appName}.", - "ClientPriority": "Prioridade do Cliente", + "ChownGroupHelpTextWarning": "Isso só funciona se o usuário que executa o {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download use o mesmo grupo que o {appName}.", + "ClientPriority": "Prioridade do cliente", "Clone": "Clonar", - "CloneIndexer": "Clonar Indexador", - "CloneProfile": "Clonar Perfil", + "CloneIndexer": "Clonar indexador", + "CloneProfile": "Clonar perfil", "CollectionsLoadError": "Não foi possível carregar as coleções", "ColonReplacement": "Substituto para dois-pontos", "ColonReplacementFormatHelpText": "Mude como o {appName} lida com a substituição do dois-pontos", - "CompletedDownloadHandling": "Gerenciamento de Downloads Completos", + "CompletedDownloadHandling": "Gerenciamento de downloads concluídos", "Condition": "Condição", - "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões Regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam escape com um `\\`", - "ConnectSettings": "Configurações de Conexão", + "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões Regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam de escape com um `\\`", + "ConnectSettings": "Configurações de conexão", "ConnectSettingsSummary": "Notificações, conexões com servidores/players de mídia e scripts personalizados", "Connections": "Conexões", - "CopyToClipboard": "Copiar para Área de Transferência", - "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo propagados. Você pode desativar temporariamente a propagação e usar a função de renomeação do {appName} como solução alternativa.", + "CopyToClipboard": "Copiar para a área de transferência", + "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo semeados. Você pode desabilitar temporariamente a semeadura e usar a função de renomeação do {appName} como uma solução alternativa.", "CreateEmptySeriesFolders": "Criar Pastas de Séries Vazias", "CreateEmptySeriesFoldersHelpText": "Crie pastas de série ausentes durante a verificação de disco", - "CreateGroup": "Criar Grupo", - "Custom": "Personalizar", + "CreateGroup": "Criar grupo", + "Custom": "Personalizado", "CustomFormat": "Formato personalizado", - "CustomFormatUnknownCondition": "Condição de Formato Personalizado desconhecida '{implementation}'", - "CustomFormatUnknownConditionOption": "Opção desconhecida '{key}' para a condição '{implementation}'", - "CustomFormatsLoadError": "Não foi possível carregar Formatos Personalizados", - "CustomFormatsSettings": "Configurações de Formatos Personalizados", - "CustomFormatsSettingsSummary": "Configurações e Formatos Personalizados", + "CustomFormatUnknownCondition": "Condição de formato personalizado '{implementation}' desconhecida", + "CustomFormatUnknownConditionOption": "Opção '{key}' desconhecida para a condição '{implementation}'", + "CustomFormatsLoadError": "Não foi possível carregar os formatos personalizados", + "CustomFormatsSettings": "Configurações de formatos personalizados", + "CustomFormatsSettingsSummary": "Formatos personalizados e configurações", "DailyEpisodeFormat": "Formato do episódio diário", "Cutoff": "Corte", "Dash": "Traço", "Dates": "Datas", "Debug": "Depuração", "DefaultDelayProfileSeries": "Este é o perfil padrão. Aplica-se a todas as séries que não possuem um perfil explícito.", - "DelayMinutes": "{delay} Minutos", - "DelayProfile": "Perfil de Atraso", - "DefaultCase": "Padrão Maiúscula ou Minúscula", + "DelayMinutes": "{delay} minutos", + "DelayProfile": "Perfil de atraso", + "DefaultCase": "Padrão maiúscula ou minúscula", "DelayProfileSeriesTagsHelpText": "Aplica-se a séries com pelo menos uma tag correspondente", - "DelayProfiles": "Perfis de Atraso", - "DelayProfilesLoadError": "Não foi possível carregar perfis de atraso", - "DeleteDelayProfile": "Excluir Perfil de Atraso", - "DeleteDownloadClient": "Excluir Cliente de Download", + "DelayProfiles": "Perfis de atraso", + "DelayProfilesLoadError": "Não foi possível carregar os perfis de atraso", + "DeleteDelayProfile": "Excluir perfil de atraso", + "DeleteDownloadClient": "Excluir cliente de download", "DeleteDownloadClientMessageText": "Tem certeza de que deseja excluir o cliente de download '{name}'?", - "DeleteEmptyFolders": "Excluir Pastas Vazias", + "DeleteEmptyFolders": "Excluir pastas vazias", "DeleteEmptySeriesFoldersHelpText": "Excluir pastas vazias de séries e temporadas durante a verificação de disco e quando os arquivos de episódios são excluídos", - "DeleteImportList": "Excluir Lista de Importação", - "DeleteImportListExclusion": "Excluir Exclusão da Lista de Importação", + "DeleteImportList": "Excluir lista de importação", + "DeleteImportListExclusion": "Excluir exclusão da lista de importação", "DeleteImportListExclusionMessageText": "Tem certeza de que deseja excluir esta exclusão da lista de importação?", - "DeleteIndexer": "Excluir Indexador", + "DeleteIndexer": "Excluir indexador", "DeleteIndexerMessageText": "Tem certeza de que deseja excluir o indexador '{name}'?", - "DeleteNotification": "Excluir Notificação", + "DeleteNotification": "Excluir notificação", "DeleteNotificationMessageText": "Tem certeza de que deseja excluir a notificação '{name}'?", - "DeleteQualityProfile": "Excluir Perfil de Qualidade", + "DeleteQualityProfile": "Excluir perfil de qualidade", "DeleteQualityProfileMessageText": "Tem certeza de que deseja excluir o perfil de qualidade '{name}'?", - "DeleteReleaseProfile": "Excluir Perfil de Lançamento", - "DeleteRemotePathMapping": "Excluir Mapeamento de Caminho Remoto", + "DeleteReleaseProfile": "Excluir perfil de lançamento", + "DeleteRemotePathMapping": "Excluir mapeamento de caminho remoto", "DeleteRemotePathMappingMessageText": "Tem certeza de que deseja excluir este mapeamento de caminho remoto?", - "DeleteSpecification": "Excluir Especificação", + "DeleteSpecification": "Excluir especificação", "DeleteSpecificationHelpText": "Tem certeza de que deseja excluir a especificação '{name}'?", - "DeleteTag": "Excluir Etiqueta", - "DeleteTagMessageText": "Tem certeza de que deseja excluir a tag '{label}'?", + "DeleteTag": "Excluir etiqueta", + "DeleteTagMessageText": "Tem certeza de que deseja excluir a etiqueta '{label}'?", "DisabledForLocalAddresses": "Desabilitado para endereços locais", - "DoNotPrefer": "Não Preferir", - "DoNotUpgradeAutomatically": "Não Atualizar Automaticamente", - "DoneEditingGroups": "Concluir Edição de Grupos", + "DoNotPrefer": "Não preferir", + "DoNotUpgradeAutomatically": "Não atualizar automaticamente", + "DoneEditingGroups": "Concluir edição de grupos", "DownloadClientOptionsLoadError": "Não foi possível carregar as opções do cliente de download", - "DownloadClientSettings": "Configurações do Cliente de Download", - "DownloadClientsLoadError": "Não foi possível carregar clientes de download", + "DownloadClientSettings": "Configurações do cliente de download", + "DownloadClientsLoadError": "Não foi possível carregar os clientes de download", "DownloadClientsSettingsSummary": "Clientes de download, gerenciamento de download e mapeamentos de caminhos remotos", - "DownloadPropersAndRepacks": "Propers e Repacks", - "DownloadPropersAndRepacksHelpText": "Se deve ou não atualizar automaticamente para Propers/Repacks", - "DownloadPropersAndRepacksHelpTextCustomFormat": "Use 'Não Preferir' para classificar por pontuação de formato personalizado em Propers/Repacks", + "DownloadPropersAndRepacks": "Propers e repacks", + "DownloadPropersAndRepacksHelpText": "Se deve ou não atualizar automaticamente para propers/repacks", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Use \"Não preferir\" para classificar pela pontuação do formato personalizado em propers/repacks", "DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para atualizações automáticas para propers/repacks", "Duplicate": "Duplicado", - "EditCustomFormat": "Editar Formato Personalizado", - "EditDelayProfile": "Editar Perfil de Atraso", - "EditGroups": "Editar Grupos", - "EditImportListExclusion": "Editar Exclusão de Lista de Importação", - "EditListExclusion": "Editar Exclusão da Lista", + "EditCustomFormat": "Editar formato personalizado", + "EditDelayProfile": "Editar perfil de atraso", + "EditGroups": "Editar grupos", + "EditImportListExclusion": "Editar exclusão de lista de importação", + "EditListExclusion": "Editar exclusão da lista", "EditMetadata": "Editar {metadataType} Metadados", - "EditQualityProfile": "Editar Perfil de Qualidade", - "EditReleaseProfile": "Editar Perfil de Lançamento", - "EditRemotePathMapping": "Editar Mapeamento do Caminho Remoto", - "EditRestriction": "Editar Restrição", + "EditQualityProfile": "Editar perfil de qualidade", + "EditReleaseProfile": "Editar perfil de lançamento", + "EditRemotePathMapping": "Editar mapeamento de caminho remoto", + "EditRestriction": "Editar restrição", "Enable": "Habilitar", - "EnableAutomaticAdd": "Habilitar Adição Automática", + "EnableAutomaticAdd": "Habilitar adição automática", "EnableAutomaticAddSeriesHelpText": "Adicione séries desta lista ao {appName} quando as sincronizações forem realizadas por meio da interface do usuário ou pelo {appName}", - "EnableAutomaticSearchHelpText": "Será usado quando pesquisas automáticas forem realizadas por meio da interface do usuário ou pelo {appName}", - "EnableAutomaticSearchHelpTextWarning": "Será usado quando a pesquisa interativa for usada", + "EnableAutomaticSearchHelpText": "Será usado ao realizar pesquisas automáticas pela interface ou pelo {appName}", + "EnableAutomaticSearchHelpTextWarning": "Será usado com a pesquisa interativa", "EnableCompletedDownloadHandlingHelpText": "Importar automaticamente downloads concluídos do cliente de download", "EnableHelpText": "Habilitar criação de arquivo de metadados para este tipo de metadados", - "EnableInteractiveSearchHelpText": "Será usado quando a pesquisa interativa for usada", - "EnableColorImpairedMode": "Habilitar Modo para Deficientes Visuais", + "EnableInteractiveSearchHelpText": "Será usado com a pesquisa interativa", + "EnableColorImpairedMode": "Habilitar modo para daltonismo", "EnableInteractiveSearchHelpTextWarning": "A pesquisa não é compatível com este indexador", - "EnableMediaInfoHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", + "EnableMediaInfoHelpText": "Extraia informações do vídeo, como resolução, duração e informações do codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as verificações.", "EnableMetadataHelpText": "Habilitar criação de arquivo de metadados para este tipo de metadados", - "EnableProfile": "Habilitar Perfil", + "EnableProfile": "Habilitar perfil", "EnableProfileHelpText": "Marque para habilitar o perfil de lançamento", "EnableRss": "Habilitar RSS", - "EnableRssHelpText": "Será usado quando o {appName} procurar periodicamente lançamentos via RSS Sync", + "EnableRssHelpText": "Será usado quando o {appName} procurar periodicamente por lançamentos via RSS Sync", "EnableSsl": "Habilitar SSL", "EnableSslHelpText": "Requer reinicialização em execução como administrador para entrar em vigor", "EpisodeNaming": "Nomenclatura do Episódio", @@ -571,11 +571,11 @@ "EpisodeTitleRequired": "Título do Episódio Obrigatório", "Example": "Exemplo", "Extend": "Estender", - "ExtraFileExtensionsHelpTextsExamples": "Exemplos: '.sub, .nfo' or 'sub,nfo'", - "FileManagement": "Gerenciamento de Arquivo", + "ExtraFileExtensionsHelpTextsExamples": "Exemplos: \".sub, .nfo\" ou \"sub,nfo\"", + "FileManagement": "Gerenciamento de arquivo", "FileNameTokens": "Tokens de nome de arquivo", - "FileNames": "Nomes de Arquivo", - "FirstDayOfWeek": "Primeiro Dia da Semana", + "FileNames": "Nomes de arquivo", + "FirstDayOfWeek": "Primeiro dia da semana", "Folders": "Pastas", "GeneralSettingsLoadError": "Não foi possível carregar as configurações gerais", "GeneralSettingsSummary": "Porta, SSL, nome de usuário/senha, proxy, análises e atualizações", @@ -583,32 +583,32 @@ "Here": "aqui", "HourShorthand": "h", "HttpHttps": "HTTP(S)", - "IgnoredAddresses": "Endereços Ignorados", + "IgnoredAddresses": "Endereços ignorados", "Images": "Imagens", "Import": "Importar", - "ImportCustomFormat": "Importar Formato Personalizado", + "ImportCustomFormat": "Importar formato personalizado", "Host": "Host", - "Hostname": "Hostname", - "ImportExtraFiles": "Importar Arquivos Extras", + "Hostname": "Nome do host", + "ImportExtraFiles": "Importar arquivos adicionais", "ImportExtraFilesEpisodeHelpText": "Importar arquivos extras correspondentes (legendas, nfo, etc) após importar um arquivo de episódio", - "ImportList": "Importar Lista", - "ImportListExclusions": "Importar Lista de Exclusões", + "ImportList": "Importar lista", + "ImportListExclusions": "Importar exclusões de lista", "ImportListExclusionsLoadError": "Não foi possível carregar as exclusões da lista de importação", "ImportListSettings": "Configurações de Importar listas", "ImportListsLoadError": "Não foi possível carregar Importar listas", "ImportListsSettingsSummary": "Importe de outra instância do {appName} ou listas do Trakt e gerencie exclusões de listas", - "ImportScriptPath": "Caminho para importar script", + "ImportScriptPath": "Caminho para script de importação", "ImportScriptPathHelpText": "O caminho para o script a ser usado para importar", "ImportUsingScript": "Importar usando script", "ImportUsingScriptHelpText": "Copiar arquivos para importar usando um script (p. ex. para transcodificação)", "Importing": "Importando", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear", "IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}", - "IncludeHealthWarnings": "Incluir Alertas de Saúde", + "IncludeHealthWarnings": "Incluir avisos de integridade", "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 ao capturar lançamentos como desempate para lançamentos iguais, {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 como um desempate ao capturar lançamentos, o {appName} ainda usará todos os indexadores habilitados para a sincronização RSS e pesquisa", "IndexerSettings": "Configurações do indexador", "IndexersLoadError": "Não foi possível carregar os indexadores", "IndexersSettingsSummary": "Indexadores e opções de indexador", @@ -619,18 +619,18 @@ "LanguagesLoadError": "Não foi possível carregar os idiomas", "ListExclusionsLoadError": "Não foi possível carregar as exclusões de lista", "ListOptionsLoadError": "Não foi possível carregar as opções de lista", - "ListQualityProfileHelpText": "Os itens da lista de Perfil de Qualidade que serão adicionados com", - "ListRootFolderHelpText": "Os itens da lista da pasta raiz que serão adicionados", + "ListQualityProfileHelpText": "Os itens da lista de Perfil de qualidade serão adicionados com", + "ListRootFolderHelpText": "Os itens da lista da pasta raiz serão adicionados a", "ListTagsHelpText": "Tags que serão adicionadas ao importar esta lista", "ListWillRefreshEveryInterval": "A lista será atualizada a cada {refreshInterval}", "ListsLoadError": "Não foi possível carregar as listas", "LocalAirDate": "Data de exibição local", "LocalPath": "Caminho local", - "LogLevel": "Nível de registro", - "LogLevelTraceHelpTextWarning": "O registro em log deve ser habilitado apenas temporariamente", + "LogLevel": "Nível de registro em log", + "LogLevelTraceHelpTextWarning": "O registro em log para rastreamento deve ser habilitado apenas temporariamente", "Logging": "Registro em log", "LongDateFormat": "Formato longo de data", - "Lowercase": "Minúscula", + "Lowercase": "Minúsculas", "ManualImportItemsLoadError": "Não foi possível carregar itens de importação manual", "Max": "Máx.", "MaximumLimits": "Limites máximos", @@ -650,9 +650,9 @@ "MetadataSourceSettings": "Configurações da fonte de metadados", "MetadataSourceSettingsSeriesSummary": "Informações sobre onde o {appName} obtém informações sobre séries e episódios", "Min": "Mín.", - "MinimumAge": "Idade miníma", - "MinimumAgeHelpText": "Somente Usenet: idade mínima, em minutos, dos NZBs antes de serem baixados. Use isso para dar aos novos lançamentos tempo para se propagar para seu provedor de Usenet.", - "MinimumCustomFormatScore": "Pontuação mínima de formato personalizado", + "MinimumAge": "Idade mínima", + "MinimumAgeHelpText": "Somente Usenet: idade mínima, em minutos, dos NZBs antes de serem obtidos. Use esta opção para dar aos novos lançamentos tempo para propagarem para seu provedor de Usenet.", + "MinimumCustomFormatScore": "Pontuação mínima do formato personalizado", "MinimumCustomFormatScoreHelpText": "Pontuação mínima de formato personalizado permitida para download", "MinimumFreeSpace": "Mínimo de espaço livre", "MinimumLimits": "Limites mínimos", @@ -667,7 +667,7 @@ "MultiEpisodeStyle": "Estilo de multiepisódio", "MustContain": "Deve conter", "MustNotContain": "Não deve conter", - "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais termos (não diferenciar maiúsculas e minúsculas)", + "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais destes termos (não diferencia maiúsculas e minúsculas)", "NamingSettings": "Configurações de nomenclatura", "NamingSettingsLoadError": "Não foi possível carregar as configurações de nomenclatura", "Never": "Nunca", @@ -693,7 +693,7 @@ "OnlyTorrent": "Só Torrent", "OnlyUsenet": "Só Usenet", "OpenBrowserOnStart": "Abrir navegador ao iniciar", - "OpenBrowserOnStartHelpText": " Abra um navegador da Web e navegue até a página inicial do {appName} no início do aplicativo.", + "OpenBrowserOnStartHelpText": " Abrir um navegador e navegar até a página inicial do {appName} ao iniciar o aplicativo.", "OptionalName": "Nome opcional", "Original": "Original", "Other": "Outro", @@ -735,11 +735,11 @@ "RecyclingBinHelpText": "Os arquivos irão para cá quando excluídos, em vez de serem excluídos permanentemente", "AbsoluteEpisodeNumber": "Número Absoluto do Episódio", "AddAutoTagError": "Não foi possível adicionar uma nova etiqueta automática, tente novamente.", - "AnalyseVideoFilesHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", + "AnalyseVideoFilesHelpText": "Extraia informações do vídeo, como resolução, duração e informações do codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as verificações.", "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", - "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação de endereços locais.", + "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, o {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação para endereços locais.", "CopyUsingHardlinksSeriesHelpText": "Os links rígidos permitem que o {appName} importe torrents de propagação para a pasta da série sem ocupar espaço extra em disco ou copiar todo o conteúdo do arquivo. Links rígidos só funcionarão se a origem e o destino estiverem no mesmo volume", - "CustomFormatHelpText": "O {appName} pontua cada lançamento usando a soma das pontuações para corresponder aos formatos personalizados. Se um novo lançamento melhorar a pontuação, com a mesma, ou melhor, qualidade, o {appName} o baixará.", + "CustomFormatHelpText": "O {appName} pontua cada lançamento usando a soma das pontuações para formatos personalizados correspondentes. Se um novo lançamento tiver melhor pontuação, com a mesma qualidade ou melhor, o {appName} o obterá.", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DeleteDelayProfileMessageText": "Tem certeza de que deseja excluir este perfil de atraso?", "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista '{name}'?", @@ -747,7 +747,7 @@ "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", "EpisodeTitleRequiredHelpText": "Impeça a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for TBA", "External": "Externo", - "ExtraFileExtensionsHelpText": "Lista separada por vírgulas de arquivos extras para importar (.nfo será importado como .nfo-orig)", + "ExtraFileExtensionsHelpText": "Lista separada por vírgulas de arquivos adicionais a importar (.nfo será importado como .nfo-orig)", "HistoryLoadError": "Não foi possível carregar o histórico", "IndexerTagSeriesHelpText": "Usar este indexador apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages suporta um sufixo `:EN+DE` permitindo filtrar os idiomas incluídos no nome do arquivo. Use `-DE` para excluir idiomas específicos. Anexar `+` (por exemplo, `:EN+`) resultará em `[EN]`/`[EN+--]`/`[--]` dependendo dos idiomas excluídos. Por exemplo, `{MediaInfo Full:EN+DE}`.", @@ -763,7 +763,7 @@ "ProxyUsernameHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.", "QualityDefinitionsLoadError": "Não foi possível carregar as definições de qualidade", "QualityProfileInUseSeriesListCollection": "Não é possível excluir um perfil de qualidade anexado a uma série, lista ou coleção", - "EnableColorImpairedModeHelpText": "Estilo alterado para permitir que usuários com deficiência de cor distingam melhor as informações codificadas por cores", + "EnableColorImpairedModeHelpText": "Estilo alterado para permitir que usuários com daltonismo distingam melhor as informações codificadas por cores", "RegularExpression": "Expressão Regular", "RegularExpressionsCanBeTested": "Expressões regulares podem ser testadas [aqui]({url}).", "RegularExpressionsTutorialLink": "Mais detalhes sobre expressões regulares podem ser encontrados [aqui]({url}).", @@ -792,7 +792,7 @@ "ResetAPIKey": "Redefinir chave de API", "ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave de API?", "ResetDefinitions": "Redefinir definições", - "RestartLater": "Vou reiniciar mais tarde", + "RestartLater": "Reiniciarei mais tarde", "RestartNow": "Reiniciar Agora", "RestartRequiredHelpTextWarning": "Requer reinicialização para entrar em vigor", "RestartRequiredToApplyChanges": "{appName} requer reinicialização para aplicar as alterações. Deseja reiniciar agora?", @@ -851,7 +851,7 @@ "SupportedAutoTaggingProperties": "{appName} oferece suporte às propriedades a seguir para regras de codificação automática", "SupportedCustomConditions": "O {appName} oferece suporte a condições personalizadas nas propriedades de lançamento abaixo.", "SupportedDownloadClients": "O {appName} suporta muitos clientes populares de download de torrent e usenet.", - "SupportedDownloadClientsMoreInfo": "Para obter mais informações sobre os clientes de download individuais, clique nos botões de mais informações.", + "SupportedDownloadClientsMoreInfo": "Para saber mais sobre os clientes de download individuais, clique nos botões Mais informações.", "SupportedImportListsMoreInfo": "Para obter mais informações sobre as listas de importação individuais, clique nos botões de mais informações.", "SupportedIndexers": "O {appName} suporta qualquer indexador que use o padrão Newznab, bem como outros indexadores listados abaixo.", "SupportedIndexersMoreInfo": "Para obter mais informações sobre os indexadores individuais, clique nos botões de mais informações.", @@ -935,17 +935,17 @@ "AllSeriesInRootFolderHaveBeenImported": "Todas as séries em {path} foram importadas", "AlreadyInYourLibrary": "Já está na sua biblioteca", "Anime": "Anime", - "CancelProcessing": "Cancelar Processamento", + "CancelProcessing": "Cancelar processamento", "ChooseAnotherFolder": "Escolha outra pasta", "CouldNotFindResults": "Não foi possível encontrar nenhum resultado para '{term}'", "Existing": "Existente", "ImportCountSeries": "Importar {selectedCount} Séries", - "ImportErrors": "Erros de Importação", + "ImportErrors": "Erros de importação", "ImportExistingSeries": "Importar Série Existente", "ImportSeries": "Importar Séries", "LibraryImportSeriesHeader": "Importar as séries que você já possui", "LibraryImportTips": "Algumas dicas para garantir que a importação ocorra sem problemas:", - "LibraryImportTipsDontUseDownloadsFolder": "Não use para importar downloads de seu cliente. Isso se aplica apenas a bibliotecas organizadas existentes, e não a arquivos desorganizados.", + "LibraryImportTipsDontUseDownloadsFolder": "Não use para importar downloads de seu cliente. Isso aplica-se apenas a bibliotecas organizadas existentes, e não a arquivos desorganizados.", "LibraryImportTipsQualityInEpisodeFilename": "Certifique-se de que seus arquivos incluam a qualidade nos nomes de arquivo. Por exemplo: \"episódio.s02e15.bluray.mkv\"", "Monitor": "Monitorar", "MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais", @@ -987,7 +987,7 @@ "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 usando {appName} manualmente ou por outra ferramenta por meio da API", "DownloadFailed": "Download Falhou", - "DestinationRelativePath": "Caminho Relativo de Destino", + "DestinationRelativePath": "Caminho de destino relativo", "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", "DownloadFailedEpisodeTooltip": "O download do episódio falhou", "DownloadIgnored": "Download ignorado", @@ -999,10 +999,10 @@ "EpisodeFileDeletedTooltip": "Arquivo do episódio excluído", "EpisodeFileRenamed": "Arquivo do Episódio Renomeado", "GrabId": "Obter ID", - "GrabSelected": "Baixar Selecionado", + "GrabSelected": "Baixar selecionado", "EpisodeGrabbedTooltip": "Episódio retirado de {indexer} e enviado para {downloadClient}", "ImportedTo": "Importado para", - "InfoUrl": "URL da info", + "InfoUrl": "URL de informações", "MarkAsFailed": "Marcar como falha", "NoHistoryFound": "Nenhum histórico encontrado", "Ok": "Ok", @@ -1036,7 +1036,7 @@ "Agenda": "Programação", "AnEpisodeIsDownloading": "Um episódio está baixando", "CalendarLegendEpisodeMissingTooltip": "O episódio foi ao ar e está faltando no disco", - "CalendarFeed": "{appName} Feed do Calendário", + "CalendarFeed": "Feed de calendário do {appName}", "CalendarLegendEpisodeDownloadedTooltip": "O episódio foi baixado e classificado", "CalendarLegendEpisodeDownloadingTooltip": "O episódio está sendo baixado no momento", "CalendarLegendSeriesFinaleTooltip": "Final de série ou temporada", @@ -1044,33 +1044,33 @@ "CalendarLegendSeriesPremiereTooltip": "Estreia de série ou temporada", "CalendarLegendEpisodeUnairedTooltip": "Episódio ainda não foi ao ar", "CalendarLegendEpisodeUnmonitoredTooltip": "Episódio não monitorado", - "CalendarOptions": "Opções de Calendário", - "CheckDownloadClientForDetails": "verifique o cliente de download para mais detalhes", + "CalendarOptions": "Opções do calendário", + "CheckDownloadClientForDetails": "verifique o cliente de download para saber mais", "CollapseMultipleEpisodes": "Agrupar Múltiplos Episódios", "CollapseMultipleEpisodesHelpText": "Agrupar múltiplos episódios que vão ao ar no mesmo dia", "Day": "Dia", "DeletedReasonUpgrade": "O arquivo foi excluído para importar uma atualização", - "DestinationPath": "Caminho de Destino", + "DestinationPath": "Caminho de destino", "Reason": "Razão", "UnknownEventTooltip": "Evento desconhecido", "EpisodeImportedTooltip": "Episódio baixado com sucesso e retirado do cliente de download", "EpisodeIsDownloading": "Episódio está baixando", "FinaleTooltip": "Final de série ou temporada", "Forecast": "Previsão", - "FullColorEvents": "Eventos em Cores", + "FullColorEvents": "Eventos em cores", "Global": "Global", - "ICalFeedHelpText": "Copie esta URL para seu(s) cliente(s) ou clique para se inscrever se seu navegador for compatível com webcal", + "ICalFeedHelpText": "Copie este URL para seu(s) cliente(s) ou clique para se inscrever se seu navegador for compatível com webcal", "ICalIncludeUnmonitoredEpisodesHelpText": "Incluir episódios não monitorados no feed iCal", "ICalSeasonPremieresOnlyHelpText": "Apenas o primeiro episódio de uma temporada estará no feed", "ICalShowAsAllDayEventsHelpText": "Os eventos aparecerão como eventos de dia inteiro em seu calendário", - "IconForCutoffUnmet": "Ícone para Corte Não Atendido", + "IconForCutoffUnmet": "Ícone para limite não atendido", "IconForCutoffUnmetHelpText": "Mostrar ícone para arquivos quando o limite não foi atingido", "IconForFinales": "Ícone para Finais", "IconForSpecials": "Ícone para Especiais", "IconForSpecialsHelpText": "Mostrar ícone para episódios especiais (temporada 0)", "ImportFailed": "Falha na importação: {sourceTitle}", "EpisodeMissingAbsoluteNumber": "O episódio não tem um número de episódio absoluto", - "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Agenda", + "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Programação", "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", "QualityCutoffNotMet": "Corte da Qualidade ainda não foi alcançado", @@ -1079,28 +1079,28 @@ "RemoveQueueItemConfirmation": "Tem certeza de que deseja remover '{sourceTitle}' da fila?", "Absolute": "Absoluto", "TableOptionsButton": "Botão de Opções de Tabela", - "AddConditionImplementation": "Adicionar Condição - {implementationName}", - "AddImportListImplementation": "Adicionar Lista de importação - {implementationName}", + "AddConditionImplementation": "Adicionar condição - {implementationName}", + "AddImportListImplementation": "Adicionar lista de importação - {implementationName}", "AddANewPath": "Adicionar um novo caminho", - "AddConnectionImplementation": "Adicionar Conexão - {implementationName}", + "AddConnectionImplementation": "Adicionar conexão - {implementationName}", "AddCustomFilter": "Adicionar Filtro Personalizado", - "AddDownloadClientImplementation": "Adicionar Cliente de Download - {implementationName}", - "AddIndexerImplementation": "Adicionar Indexador - {implementationName}", + "AddDownloadClientImplementation": "Adicionar cliente de download - {implementationName}", + "AddIndexerImplementation": "Adicionar indexador - {implementationName}", "AddToDownloadQueue": "Adicionar à fila de download", "AddedToDownloadQueue": "Adicionado à fila de download", "Airs": "Vai ao ar em", "AirsDateAtTimeOn": "{date} às {time} em {networkLabel}", "AnimeEpisodeTypeFormat": "Número absoluto do episódio ({format})", - "AppUpdatedVersion": "{appName} foi atualizado para a versão `{version}`, para obter as alterações mais recentes, você precisará recarregar {appName} ", - "ChooseImportMode": "Escolha o Modo de Importação", + "AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName} ", + "ChooseImportMode": "Escolha o modo de importação", "ClickToChangeEpisode": "Clique para alterar o episódio", - "ConnectionLostReconnect": "{appName} tentará se conectar automaticamente ou você pode clicar em recarregar abaixo.", + "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.", "CountSelectedFiles": "{selectedCount} arquivos selecionados", "DefaultNotFoundMessage": "Você deve estar perdido, nada para ver aqui.", "DeleteEpisodeFile": "Excluir Arquivo do Episódio", "DeleteSelectedEpisodeFilesHelpText": "Tem certeza de que deseja excluir os arquivos de episódios selecionados?", - "EditConditionImplementation": "Editar Condição - {implementationName}", - "EditIndexerImplementation": "Editar Indexador - {implementationName}", + "EditConditionImplementation": "Editar condição - {implementationName}", + "EditIndexerImplementation": "Editar indexador - {implementationName}", "ErrorLoadingItem": "Ocorreu um erro ao carregar este item", "FailedToLoadQualityProfilesFromApi": "Falha ao carregar perfis de qualidade da API", "FailedToLoadUiSettingsFromApi": "Falha ao carregar as configurações de IU da API", @@ -1111,7 +1111,7 @@ "ICalFeed": "Feed do iCal", "ICalLink": "Link do iCal", "InteractiveImportLoadError": "Não foi possível carregar itens de importação manual", - "InteractiveImportNoFilesFound": "Nenhum arquivo de vídeo foi encontrado na pasta selecionada", + "InteractiveImportNoFilesFound": "Nenhum arquivo de vídeo encontrado na pasta selecionada", "InteractiveImportNoSeason": "A temporada deve ser escolhida para cada arquivo selecionado", "InteractiveSearchResultsSeriesFailedErrorMessage": "A pesquisa falhou porque {message}. Tente atualizar as informações da série e verifique se as informações necessárias estão presentes antes de pesquisar novamente.", "KeyboardShortcutsFocusSearchBox": "Selecionar a caixa de pesquisa", @@ -1121,20 +1121,20 @@ "AirsTbaOn": "A ser anunciado em {networkLabel}", "AirsTimeOn": "{time} em {networkLabel}", "AirsTomorrowOn": "Amanhã às {time} em {networkLabel}", - "AllFiles": "Todos os Arquivos", + "AllFiles": "Todos os arquivos", "Any": "Quaisquer", - "AppUpdated": "{appName} Atualizado", - "AutomaticUpdatesDisabledDocker": "As atualizações automáticas não têm suporte direto ao usar o mecanismo de atualização do Docker. Você precisará atualizar a imagem do contêiner fora de {appName} ou usar um script", + "AppUpdated": "{appName} atualizado", + "AutomaticUpdatesDisabledDocker": "As atualizações automáticas não têm suporte direto ao usar o mecanismo de atualização do Docker. Você precisará atualizar a imagem do contêiner fora do {appName} ou usar um script", "ClickToChangeLanguage": "Clique para alterar o idioma", "ClickToChangeQuality": "Clique para alterar a qualidade", "ClickToChangeReleaseGroup": "Clique para alterar o grupo de lançamento", "ClickToChangeSeason": "Clique para mudar a temporada", "ClickToChangeSeries": "Clique para mudar de série", - "ConnectionLost": "Conexão Perdida", - "ConnectionLostToBackend": "{appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLost": "Conexão perdida", + "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", "Continuing": "Continuando", "CountSelectedFile": "{selectedCount} arquivo selecionado", - "CustomFilters": "Filtros Personalizados", + "CustomFilters": "Filtros personalizados", "DailyEpisodeTypeFormat": "Data ({format})", "Default": "Padrão", "DeleteEpisodeFileMessage": "Tem certeza de que deseja excluir '{path}'?", @@ -1142,8 +1142,8 @@ "DeleteSelectedEpisodeFiles": "Excluir Arquivos de Episódios Selecionados", "Donate": "Doar", "EditConnectionImplementation": "Editar Conexão - {implementationName}", - "EditDownloadClientImplementation": "Editar Cliente de Download - {implementationName}", - "EditImportListImplementation": "Editar Lista de Importação - {implementationName}", + "EditDownloadClientImplementation": "Editar cliente de download - {implementationName}", + "EditImportListImplementation": "Editar lista de importação - {implementationName}", "EpisodeDownloaded": "Episódio Baixado", "EpisodeHasNotAired": "Episódio não foi ao ar", "EpisodeHistoryLoadError": "Não foi possível carregar o histórico de episódios", @@ -1185,20 +1185,20 @@ "FilterNotInNext": "não no próximo", "FilterSeriesPlaceholder": "Filtrar séries", "FilterStartsWith": "começa com", - "GrabRelease": "Baixar Lançamento", - "HardlinkCopyFiles": "Hardlink/Copiar Arquivos", + "GrabRelease": "Baixar lançamento", + "HardlinkCopyFiles": "Criar hardlink/Copiar arquivos", "InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado", - "InteractiveImportNoImportMode": "Defina um modo de importação", - "InteractiveImportNoLanguage": "Defina um idioma para cada arquivo selecionado", - "InteractiveImportNoQuality": "Defina a qualidade para cada arquivo selecionado", + "InteractiveImportNoImportMode": "Selecione um modo de importação", + "InteractiveImportNoLanguage": "Selecione um idioma para cada arquivo selecionado", + "InteractiveImportNoQuality": "Selecione a qualidade para cada arquivo selecionado", "InteractiveImportNoSeries": "A série deve ser escolhida para cada arquivo selecionado", - "KeyboardShortcuts": "Atalhos do Teclado", + "KeyboardShortcuts": "Atalhos do teclado", "KeyboardShortcutsCloseModal": "Fechar pop-up atual", "KeyboardShortcutsConfirmModal": "Aceitar o pop-up de confirmação", "KeyboardShortcutsOpenModal": "Abrir este pop-up", "Local": "Local", "Logout": "Sair", - "ManualGrab": "Baixar Manualmente", + "ManualGrab": "Baixar manualmente", "ManualImport": "Importação manual", "Mapping": "Mapeamento", "MarkAsFailedConfirmation": "Tem certeza de que deseja marcar \"{sourceTitle}\" como em falha?", @@ -1310,7 +1310,7 @@ "ShowUnknownSeriesItemsHelpText": "Mostrar itens sem uma série na fila, isso pode incluir séries, filmes removidos ou qualquer outra coisa na categoria do {appName}", "Test": "Teste", "Level": "Nível", - "AddListExclusion": "Adicionar Exclusão de Lista", + "AddListExclusion": "Adicionar exclusão à lista", "AddListExclusionSeriesHelpText": "Impedir que o {appName} adicione séries por listas", "EditSeriesModalHeader": "Editar - {title}", "EditSelectedSeries": "Editar Séries Selecionadas", @@ -1354,10 +1354,10 @@ "Files": "Arquivos", "HistorySeason": "Exibir histórico para esta temporada", "HistoryModalHeaderSeason": "Histórico - {season}", - "InteractiveSearchModalHeader": "Pesquisa Interativa", + "InteractiveSearchModalHeader": "Pesquisa interativa", "InteractiveSearchModalHeaderSeason": "Pesquisa interativa - {season}", "InteractiveSearchSeason": "Pesquisa interativa para todos os episódios desta temporada", - "InvalidUILanguage": "Sua UI está definida com um idioma inválido, corrija-a e salve suas configurações", + "InvalidUILanguage": "Sua interface está definida com um idioma inválido, corrija-o e salve suas configurações", "Large": "Grande", "Links": "Links", "ManageEpisodes": "Gerenciar episódios", @@ -1430,7 +1430,7 @@ "EndedSeriesDescription": "Não se espera mais episódios ou temporadas", "EpisodeFilesLoadError": "Não foi possível carregar os arquivos do episódio", "AddedDate": "Adicionado: {date}", - "AllSeriesAreHiddenByTheAppliedFilter": "Todos os resultados são ocultados pelo filtro aplicado", + "AllSeriesAreHiddenByTheAppliedFilter": "Todos os resultados estão ocultos pelo filtro aplicado", "AlternateTitles": "Títulos alternativos", "AuthenticationMethod": "Método de autenticação", "AuthenticationMethodHelpTextWarning": "Selecione um método de autenticação válido", @@ -1469,32 +1469,32 @@ "FormatShortTimeSpanSeconds": "{seconds} segundo(s)", "FormatTimeSpanDays": "{days}d {time}", "Yesterday": "Ontem", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "O cliente de download {downloadClientName} está configurado para remover downloads concluídos. Isso pode resultar na remoção dos downloads do seu cliente antes que {appName} possa importá-los.", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Procure e tente baixar automaticamente uma versão diferente quando a versão com falha for obtida da pesquisa interativa", - "AutoRedownloadFailed": "Falha no Novo Download", - "AutoRedownloadFailedFromInteractiveSearch": "Falha no Novo Download da Pesquisa Interativa", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "O cliente de download {downloadClientName} está configurado para remover os downloads concluídos. Isso pode resultar na remoção dos downloads do seu cliente antes que o {appName} possa importá-los.", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Procurar e tentar baixar automaticamente um lançamento diferente quando for obtido um lançamento com falha na pesquisa interativa", + "AutoRedownloadFailed": "Falha no novo download", + "AutoRedownloadFailedFromInteractiveSearch": "Falha no novo download usando a pesquisa interativa", "ImportListSearchForMissingEpisodes": "Pesquisar Episódios Ausentes", "ImportListSearchForMissingEpisodesHelpText": "Depois que a série for adicionada ao {appName}, procure automaticamente episódios ausentes", "QueueFilterHasNoItems": "O filtro de fila selecionado não possui itens", - "BlackholeFolderHelpText": "Pasta na qual {appName} armazenará o arquivo {extension}", - "Destination": "Destinação", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL json do deluge, consulte {url}", + "BlackholeFolderHelpText": "Pasta na qual o {appName} armazenará o arquivo {extension}", + "Destination": "Destino", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL do JSON do Deluge, consulte {url}", "DownloadClientDelugeValidationLabelPluginFailure": "Falha na configuração do rótulo", - "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} não conseguiu adicionar o rótulo ao {clientName}.", - "DownloadClientDownloadStationProviderMessage": "{appName} não consegue se conectar ao Download Station se a autenticação de dois fatores estiver habilitada em sua conta DSM", - "DownloadClientDownloadStationValidationFolderMissingDetail": "A pasta '{downloadDir}' não existe, ela deve ser criada manualmente dentro da Pasta Compartilhada '{sharedFolder}'.", - "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "O Diskstation não possui uma pasta compartilhada com o nome '{sharedFolder}', tem certeza de que a especificou corretamente?", - "DownloadClientFreeboxSettingsAppIdHelpText": "ID do aplicativo fornecido ao criar acesso à API Freebox (ou seja, 'app_id')", - "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é '{port}'", - "DownloadClientFreeboxUnableToReachFreeboxApi": "Não foi possível acessar a API Freebox. Verifique a configuração de 'URL da API' para URL base e versão.", - "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração NzbGet KeepHistory deve ser menor que 25.000", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0. O que impede que {appName} veja os downloads concluídos.", - "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {clientName}, como {url}", - "DownloadClientSettingsUseSslHelpText": "Use conexão segura ao conectar-se a {clientName}", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local de transmissão padrão", - "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL rpc {clientName}, por exemplo, {url}, o padrão é '{defaultUrl}'", - "DownloadClientValidationAuthenticationFailureDetail": "Por favor verifique seu nome de usuário e senha. Verifique também se o host que executa {appName} não está impedido de acessar {clientName} pelas limitações da WhiteList na configuração de {clientName}.", - "DownloadClientValidationSslConnectFailureDetail": "{appName} não consegue se conectar a {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar {appName} e {clientName} para não usar SSL.", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "O {appName} não conseguiu adicionar o rótulo ao {clientName}.", + "DownloadClientDownloadStationProviderMessage": "O {appName} não consegue se conectar ao Download Station se a autenticação de dois fatores estiver habilitada em sua conta do DSM", + "DownloadClientDownloadStationValidationFolderMissingDetail": "A pasta \"{downloadDir}\" não existe. Você deve criá-la manualmente dentro da Pasta compartilhada \"{sharedFolder}\".", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "O Diskstation não possui uma Pasta compartilhada com o nome \"{sharedFolder}\", tem certeza de que a especificou corretamente?", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID do aplicativo fornecida ao criar acesso à API do Freebox (ou seja, \"app_id\")", + "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é \"{port}\"", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Não foi possível acessar a API do Freebox. Verifique o URL base e a versão na configuração \"URL da API\".", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração KeepHistory do NzbGet deve ser menor que 25.000", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0, que impede que o {appName} veja os downloads concluídos.", + "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL do {clientName}, como {url}", + "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se ao {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar os downloads, deixe em branco para usar o local padrão do Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL de chamada remota do {clientName}, por exemplo, {url}. O padrão é \"{defaultUrl}\"", + "DownloadClientValidationAuthenticationFailureDetail": "Verifique o nome de usuário e a senha. Verifique também se o host que executa o {appName} não está impedido o acesso ao {clientName} pelas limitações da Lista de permissões na configuração do {clientName}.", + "DownloadClientValidationSslConnectFailureDetail": "O {appName} não consegue se conectar ao {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar o {appName} e o {clientName} para não usar SSL.", "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status de descompactação: {unpackStatus} - Status de movimentação: {moveStatus} - Status do script: {scriptStatus} - Status de exclusão: {deleteStatus} - Status de marcação: {markStatus}", "PostImportCategory": "Categoria Pós-Importação", "SecretToken": "Token Secreto", @@ -1503,49 +1503,49 @@ "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnet se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", "UnknownDownloadState": "Estado de download desconhecido: {state}", "UsenetBlackhole": "Blackhole para Usenet", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de semeação", - "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desabilitado para este torrent ou 'Layout de conteúdo de torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "As categorias não são suportadas até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", - "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} não poderá realizar o tratamento de download concluído conforme configurado. Você pode corrigir isso no qBittorrent ('Ferramentas -> Opções...' no menu) alterando 'Opções -> BitTorrent -> Limitação da proporção de compartilhamento' de 'Removê-los' para 'Pausá-los'", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o endpoint XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes do download' afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda 'Abortar trabalhos que não podem ser concluídos', pois é mais eficaz.", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a classificação de filmes para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", - "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefere que cada download tenha uma pasta separada. Com * anexado à pasta/caminho, o Sabnzbd não criará essas pastas de trabalho. Vá para Sabnzbd para consertar.", - "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório [categoria] no diretório de saída.", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que torrents forçados não obedecem às restrições de semeadura", + "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download base do cliente, é possível que \"Manter pasta de nível superior\" esteja desabilitado para este torrent ou \"Layout de conteúdo de torrent\" NÃO esteja definido como 'Original' ou 'Criar subpasta'?", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Não há categorias até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "O qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "O {appName} não poderá realizar o tratamento de download concluído conforme configurado. Para corrigir isso, no qBittorrent, acesse \"Ferramentas -> Opções... -> BitTorrent -> Limites de Semeadura\", e altere a opção de \"Remover\" para \"Parar\"", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o ponto de extremidade do XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar \"Verificar antes do download\" afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda \"Abortar trabalhos que não podem ser concluídos\", pois é mais eficaz.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a classificação de filmes para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "O {appName} prefere que cada download tenha uma pasta separada. Com * anexado à pasta/caminho, o Sabnzbd não criará essas pastas de trabalho. Conserte isso no Sabnzbd.", + "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica ao {appName} evita conflitos com downloads não relacionados que não sejam do {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório \"[categoria]\" no diretório de saída.", "XmlRpcPath": "Caminho RPC XML", "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.", "DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta etiquetas após a importação de um download.", - "BlackholeWatchFolder": "Pasta de Monitoramento", - "BlackholeWatchFolderHelpText": "Pasta da qual {appName} deve importar downloads concluídos", + "BlackholeWatchFolder": "Pasta de monitoramento", + "BlackholeWatchFolderHelpText": "Pasta da qual o {appName} deve importar os downloads concluídos", "Category": "Categoria", "Directory": "Diretório", "DownloadClientDelugeTorrentStateError": "Deluge está relatando um erro", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin rótulo habilitado no {clientName} para usar categorias.", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin de rótulo habilitado no {clientName} para usar categorias.", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", - "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporta de {minVersion} a {maxVersion}", + "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporte às versões de {minVersion} a {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "A pasta não existe", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nenhum destino padrão", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do DownloadStation em BT/HTTP/FTP/NZB -> Localização.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do Download Station em BT/HTTP/FTP/NZB -> Localização.", "DownloadClientDownloadStationValidationSharedFolderMissing": "A pasta compartilhada não existe", - "DownloadClientFloodSettingsAdditionalTags": "Etiquetas Adicionais", - "DownloadClientFloodSettingsPostImportTags": "Etiquetas Pós-Importação", - "DownloadClientFloodSettingsRemovalInfo": "{appName} cuidará da remoção automática de torrents com base nos critérios de propagação atuais em Configurações -> Indexadores", - "DownloadClientFloodSettingsStartOnAdd": "Comece ao Adicionar", - "DownloadClientFloodSettingsUrlBaseHelpText": "Adiciona um prefixo à API Flood, como {url}", - "DownloadClientFreeboxApiError": "A API Freebox retornou um erro: {errorDescription}", - "DownloadClientFreeboxAuthenticationError": "A autenticação na API Freebox falhou. Motivo: {errorDescription}", - "DownloadClientFreeboxNotLoggedIn": "Não logado", + "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionais", + "DownloadClientFloodSettingsPostImportTags": "Etiquetas pós-importação", + "DownloadClientFloodSettingsRemovalInfo": "O {appName} cuidará da remoção automática de torrents com base nos critérios de semeadura atuais em Configurações -> Indexadores", + "DownloadClientFloodSettingsStartOnAdd": "Começar ao adicionar", + "DownloadClientFloodSettingsUrlBaseHelpText": "Adiciona um prefixo à API do Flood, como {url}", + "DownloadClientFreeboxApiError": "A API do Freebox retornou um erro: {errorDescription}", + "DownloadClientFreeboxAuthenticationError": "A autenticação na API do Freebox falhou. Motivo: {errorDescription}", + "DownloadClientFreeboxNotLoggedIn": "Não conectado", "DownloadClientFreeboxSettingsApiUrl": "URL da API", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API Freebox com a versão da API, por exemplo, '{url}', o padrão é '{defaultApiUrl}'", - "DownloadClientFreeboxSettingsAppId": "ID do App", - "DownloadClientFreeboxSettingsAppToken": "Token do App", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API Freebox (ou seja, 'app_token')", - "DownloadClientFreeboxSettingsHostHelpText": "Nome do host ou endereço IP do host do Freebox, o padrão é '{url}' (só funcionará se estiver na mesma rede)", - "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API Freebox. Verifique as configurações de 'Host', 'Porta' ou 'Usar SSL'. (Erro: {exceptionMessage})", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API do Freebox com a versão da API, por exemplo, \"{url}\", o padrão é \"{defaultApiUrl}\"", + "DownloadClientFreeboxSettingsAppId": "ID do aplicativo", + "DownloadClientFreeboxSettingsAppToken": "Token do aplicativo", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API do Freebox (ou seja, \"app_token\")", + "DownloadClientFreeboxSettingsHostHelpText": "Nome ou endereço IP do host do Freebox, o padrão é \"{url}\" (só funcionará se estiver na mesma rede)", + "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API do Freebox. Verifique as configurações \"Host\", \"Porta\" ou \"Usar SSL\". (Erro: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "O download contém vários arquivos e não está em uma pasta de trabalho: {outputPath}", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos NzbGet versão 16.0", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos a versão 16.0 do NzbGet", "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de rótulo não ativado", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "A configuração KeepHistory do NzbGet está muito alta.", "DownloadClientNzbgetValidationKeepHistoryZero": "A configuração KeepHistory do NzbGet deve ser maior que 0", @@ -1553,57 +1553,57 @@ "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisará estar acessível no XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeiro e Último Primeiro", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última peças primeiro (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem Sequencial", - "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixe em ordem sequencial (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções - UI da Web - 'Usar HTTPS em vez de HTTP' em qBittorrent.", - "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent não pode resolver o link magnético com DHT desativado", - "DownloadClientQbittorrentTorrentStateError": "qBittorrent está relatando um erro", - "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está baixando metadados", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Priorizar o primeiro e o último", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última partes antes (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem sequencial", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixar em ordem sequencial (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usar uma conexão segura. Consulte Opções -> Interface de Usuário da Web -> \"Usar HTTPS ao invés do HTTP\" no qBittorrent.", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "O qBittorrent não consegue resolver o link magnético com DHT desativado", + "DownloadClientQbittorrentTorrentStateError": "O qBittorrent está relatando um erro", + "DownloadClientQbittorrentTorrentStateMetadata": "O qBittorrent está baixando os metadados", "DownloadClientQbittorrentTorrentStateStalled": "O download está parado sem conexões", "DownloadClientQbittorrentTorrentStateUnknown": "Estado de download desconhecido: {state}", "DownloadClientQbittorrentValidationCategoryAddFailure": "Falha na configuração da categoria", - "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} não conseguiu adicionar o rótulo ao qBittorrent.", - "DownloadClientQbittorrentValidationCategoryRecommended": "Categoria é recomendada", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} não tentará importar downloads concluídos sem uma categoria.", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "O {appName} não conseguiu adicionar o rótulo ao qBittorrent.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Recomenda-se usar uma categoria", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "O {appName} não tentará importar downloads concluídos sem uma categoria.", "DownloadClientQbittorrentValidationCategoryUnsupported": "A categoria não é suportada", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila Não Habilitada", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "O Fila de Torrent não está habilitado nas configurações do qBittorrent. Habilite-o no qBittorrent ou selecione ‘Último’ como prioridade.", - "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de seed. {appName} lidará com a remoção automática de torrents com base nos critérios de propagação atuais em Configurações->Indexadores somente quando Remover Concluído estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização rTorrent, que pode ser usada em scripts rTorrent para personalizar o comportamento.", - "DownloadClientRTorrentSettingsAddStopped": "Adicionar Parado", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitando, irá adicionar torrents e magnets ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila não habilitada", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "A fila de torrents não está habilitada nas configurações do qBittorrent. Habilite-a no qBittorrent ou selecione \"Último\" como prioridade.", + "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de semeadura. O {appName} lidará com a remoção automática de torrents com base nos critérios de semeadura atuais em Configurações -> Indexadores somente quando Remover concluído estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização do rTorrent, que pode ser usada em scripts do rTorrent para personalizar o comportamento.", + "DownloadClientRTorrentSettingsAddStopped": "Adicionar parado", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitar esta opção adicionará os torrents e links magnéticos ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", - "DownloadClientRTorrentSettingsUrlPath": "Caminho da URL", - "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção ‘Verificar antes do download’ no Sabnbzd", + "DownloadClientRTorrentSettingsUrlPath": "Caminho do URL", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção \"Verificar antes do download\" no Sabnbzd", "DownloadClientSabnzbdValidationDevelopVersion": "Versão de desenvolvimento do Sabnzbd, assumindo a versão 3.0.0 ou superior.", - "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} pode não ser compatível com novos recursos adicionados ao SABnzbd ao executar versões de desenvolvimento.", - "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desativar Classificação por Data", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a classificação por data para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", - "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar Classificação de Filmes", - "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar Classificação de TV", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desativar a classificação de TV para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "O {appName} pode não ser compatível com novos recursos adicionados ao SABnzbd ao executar versões de desenvolvimento.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desativar classificação por data", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a classificação por data para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar classificação de filmes", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar classificação de séries", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desabilitar a classificação de séries para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", "DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar pastas de trabalho", - "DownloadClientSabnzbdValidationUnknownVersion": "Versão Desconhecida: {rawVersion}", - "DownloadClientSettingsAddPaused": "Adicionar Pausado", - "DownloadClientSettingsCategoryHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado.", + "DownloadClientSabnzbdValidationUnknownVersion": "Versão desconhecida: {rawVersion}", + "DownloadClientSettingsAddPaused": "Adicionar pausado", + "DownloadClientSettingsCategoryHelpText": "Adicionar uma categoria específica ao {appName} evita conflitos com downloads não relacionados que não sejam do {appName}. Usar uma categoria é opcional, mas altamente recomendado.", "DownloadClientSettingsDestinationHelpText": "Especifica manualmente o destino do download, deixe em branco para usar o padrão", - "DownloadClientSettingsInitialState": "Estado Inicial", - "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados a {clientName}", + "DownloadClientSettingsInitialState": "Estado inicial", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados ao {clientName}", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar há mais de 14 dias", - "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para {appName} definir após importar o download. {appName} não removerá torrents nessa categoria mesmo que a propagação seja concluída. Deixe em branco para manter a mesma categoria.", + "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para o {appName} definir após importar o download. O {appName} não removerá torrents nessa categoria mesmo que a semeadura esteja concluída. Deixe em branco para manter a mesma categoria.", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar nos últimos 14 dias", - "DownloadClientSettingsOlderPriority": "Priorizar Mais Antigos", - "DownloadClientSettingsRecentPriority": "Priorizar Recentes", - "DownloadClientUTorrentTorrentStateError": "uTorrent está relatando um erro", - "DownloadClientValidationApiKeyIncorrect": "Chave de API incorreta", - "DownloadClientValidationApiKeyRequired": "Chave de API necessária", - "DownloadClientValidationAuthenticationFailure": "Falha de Autenticação", + "DownloadClientSettingsOlderPriority": "Priorizar mais antigos", + "DownloadClientSettingsRecentPriority": "Priorizar recentes", + "DownloadClientUTorrentTorrentStateError": "O uTorrent está relatando um erro", + "DownloadClientValidationApiKeyIncorrect": "Chave da API incorreta", + "DownloadClientValidationApiKeyRequired": "Chave da API necessária", + "DownloadClientValidationAuthenticationFailure": "Falha de autenticação", "DownloadClientValidationCategoryMissing": "A categoria não existe", - "DownloadClientValidationCategoryMissingDetail": "A categoria inserida não existe em {clientName}. Crie-o primeiro em {clientName}.", - "DownloadClientValidationErrorVersion": "A versão de {clientName} deve ser pelo menos {requiredVersion}. A versão informada é {reportedVersion}", + "DownloadClientValidationCategoryMissingDetail": "A categoria inserida não existe no {clientName}. Crie-a primeiro no {clientName}.", + "DownloadClientValidationErrorVersion": "A versão do {clientName} deve ser pelo menos {requiredVersion}. A versão informada é {reportedVersion}", "DownloadClientValidationGroupMissing": "O grupo não existe", - "DownloadClientValidationGroupMissingDetail": "O grupo inserido não existe em {clientName}. Crie-o primeiro em {clientName}.", + "DownloadClientValidationGroupMissingDetail": "O grupo inserido não existe no {clientName}. Crie-o primeiro no {clientName}.", "DownloadClientValidationSslConnectFailure": "Não é possível conectar através de SSL", "DownloadClientValidationTestNzbs": "Falha ao obter a lista de NZBs: {exceptionMessage}", "DownloadClientValidationTestTorrents": "Falha ao obter a lista de torrents: {exceptionMessage}", @@ -1692,8 +1692,8 @@ "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirme a nova senha", "MonitorNoNewSeasons": "Sem Novas Temporadas", "MonitorNewItems": "Monitorar Novos Itens", - "DownloadClientQbittorrentSettingsContentLayout": "Layout de Conteúdo", - "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Seja para usar o layout de conteúdo configurado do qBittorrent, o layout original do torrent ou sempre criar uma subpasta (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsContentLayout": "Layout de conteúdo", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Se devemos usar o layout de conteúdo configurado do qBittorrent, o layout original do torrent ou sempre criar uma subpasta (qBittorrent 4.3.2+)", "AddRootFolderError": "Não foi possível adicionar a pasta raiz", "NotificationsAppriseSettingsNotificationType": "Informar Tipo de Notificação", "NotificationsAppriseSettingsPasswordHelpText": "Senha de Autenticação Básica HTTP", @@ -1734,7 +1734,7 @@ "NotificationsEmbySettingsUpdateLibraryHelpText": "Atualizar Biblioteca ao Importar, Renomear ou Excluir", "NotificationsGotifySettingIncludeSeriesPoster": "Incluir Pôster da Série", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluir Pôster da Série na Mensagem", - "NotificationsGotifySettingsAppToken": "Token do Aplicativo", + "NotificationsGotifySettingsAppToken": "Token do aplicativo", "NotificationsGotifySettingsAppTokenHelpText": "O token do aplicativo gerado pelo Gotify", "NotificationsGotifySettingsPriorityHelpText": "Prioridade da notificação", "NotificationsGotifySettingsServer": "Servidor Gotify", @@ -1865,9 +1865,9 @@ "NotificationsTelegramSettingsSendSilentlyHelpText": "Envia a mensagem silenciosamente. Os usuários receberão uma notificação sem som", "EpisodeFileMissingTooltip": "Arquivo do episódio ausente", "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2", - "DownloadClientPriorityHelpText": "Prioridade do Cliente de Download de 1 (mais alta) a 50 (mais baixa). Padrão: 1. Round-Robin é usado para clientes com a mesma prioridade.", - "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar Hashes de Torrent Bloqueados Durante a Captura", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Se um torrent for bloqueado por hash, ele pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", + "DownloadClientPriorityHelpText": "Prioridade do cliente de download de 1 (mais alta) a 50 (mais baixa). Padrão: 1. Usamos uma distribuição equilibrada para clientes com a mesma prioridade.", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar hashes de torrent bloqueados durante a captura", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Se um torrent for bloqueado por hash, pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", "ImportListsSimklSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Séries Mais Assistidas por Semana", "ImportListsTraktSettingsPopularListTypeTopYearShows": "Séries Mais Assistidas por Ano", @@ -1919,7 +1919,7 @@ "ImportListsAniListSettingsImportWatching": "Importar Assistindo", "ImportListsCustomListSettingsUrl": "URL da Lista", "ImportListsCustomListSettingsUrlHelpText": "O URL da lista de séries", - "ImportListsCustomListValidationAuthenticationFailure": "Falha de Autenticação", + "ImportListsCustomListValidationAuthenticationFailure": "Falha de autenticação", "ImportListsCustomListValidationConnectionError": "Não foi possível fazer a solicitação para esse URL. Código de status: {exceptionStatusCode}", "ImportListsImdbSettingsListId": "ID da Lista", "ImportListsImdbSettingsListIdHelpText": "ID da lista IMDb (por exemplo, ls12345678)", @@ -2002,42 +2002,42 @@ "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')", "NotificationsEmailSettingsUseEncryption": "Usar Criptografia", "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia", - "IgnoreDownloadsHint": "Impede que {appName} processe ainda mais esses downloads", + "IgnoreDownloadsHint": "Impede que o {appName} processe ainda mais esses downloads", "RemoveFromDownloadClientHint": "Remove download e arquivo(s) do cliente de download", "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.", "BlocklistMultipleOnlyHint": "Adicionar à lista de bloqueio sem procurar por substitutos", "BlocklistOnly": "Apenas adicionar à lista de bloqueio", "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar à lista de bloqueio", - "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente por {appName} via RSS ou Pesquisa Automática", + "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente pelo {appName} via RSS ou Pesquisa automática", "ChangeCategoryHint": "Altera o download para a \"Categoria pós-importação\" do cliente de download", "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", "DatabaseMigration": "Migração de banco de dados", "DoNotBlocklistHint": "Remover sem colocar na lista de bloqueio", "ChangeCategory": "Alterar categoria", "DoNotBlocklist": "Não coloque na lista de bloqueio", - "IgnoreDownloads": "Ignorar Downloads", - "IgnoreDownloadHint": "Impede que {appName} processe ainda mais este download", + "IgnoreDownloads": "Ignorar downloads", + "IgnoreDownloadHint": "Impede que o {appName} processe ainda mais este download", "RemoveMultipleFromDownloadClientHint": "Remove downloads e arquivos do cliente de download", "RemoveQueueItemRemovalMethod": "Método de Remoção", "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remover do Cliente de Download' removerá os downloads e os arquivos do cliente de download.", "BlocklistAndSearch": "Adicionar à lista de bloqueio e pesquisar", "BlocklistAndSearchHint": "Iniciar uma pesquisa por um substituto após adicionar à lista de bloqueio", "BlocklistOnlyHint": "Adicionar à lista de bloqueio sem procurar por um substituto", - "IgnoreDownload": "Ignorar Download", + "IgnoreDownload": "Ignorar download", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais", "KeepAndTagSeries": "Manter e Etiquetar Séries", "KeepAndUnmonitorSeries": "Manter e Desmonitorar Séries", - "ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se elas caírem ou não aparecerem na(s) sua(s) lista(s)", + "ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se saírem de sua(s) lista(s) ou não aparecerem nela(s)", "ListSyncTag": "Etiqueta de Sincronização de Lista", "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", - "LogOnly": "Só Registro", - "CleanLibraryLevel": "Limpar Nível da Biblioteca", + "LogOnly": "Só registro em log", + "CleanLibraryLevel": "Limpar nível da biblioteca", "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente.", "ClickToChangeIndexerFlags": "Clique para alterar os sinalizadores do indexador", "SelectIndexerFlags": "Selecionar Sinalizadores do Indexador", "SetIndexerFlagsModalTitle": "{modalTitle} - Definir Sinalizadores do Indexador", "CustomFormatsSpecificationFlag": "Sinalizar", - "IndexerFlags": "Sinalizadores do Indexador", + "IndexerFlags": "Sinalizadores do indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado", @@ -2119,5 +2119,17 @@ "NotificationsGotifySettingsMetadataLinksHelpText": "Adicionar links aos metadados da série ao enviar notificações", "NotificationsGotifySettingsPreferredMetadataLink": "Link de Metadados Preferido", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Link de metadados para clientes que suportam apenas um único link", - "FolderNameTokens": "Tokens de Nome de Pasta" + "FolderNameTokens": "Tokens de Nome de Pasta", + "MetadataPlexSettingsEpisodeMappings": "Mapeamentos dos Episódios", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Incluir mapeamentos de episódios para todos os arquivos no arquivo .plexmatch", + "FailedToFetchSettings": "Falha ao obter configurações", + "DownloadClientUnavailable": "Cliente de download indisponível", + "RecentFolders": "Pastas Recentes", + "Warning": "Cuidado", + "Delay": "Atraso", + "ManageFormats": "Gerenciar Formatos", + "FavoriteFolderAdd": "Adicionar Pasta Favorita", + "FavoriteFolderRemove": "Remover Pasta Favorita", + "FavoriteFolders": "Pastas Favoritas", + "Fallback": "Reserva" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index a77c947a0..7ea8e1ef1 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -1,6 +1,6 @@ { "Backup": "Copie de rezervă", - "CloneCondition": "Clonați condiție", + "CloneCondition": "Clonează condiție", "ApplyChanges": "Aplicați modificări", "AutomaticAdd": "Adăugare automată", "AllTitles": "Toate titlurile", @@ -21,7 +21,7 @@ "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}. {errorMessage}", - "CloneCustomFormat": "Clonați format personalizat", + "CloneCustomFormat": "Clonează format personalizat", "Close": "Închide", "Delete": "Șterge", "Added": "Adăugat", @@ -205,5 +205,9 @@ "OnFileImport": "La import fișier", "FileNameTokens": "Jetoane pentru nume de fișier", "FolderNameTokens": "Jetoane pentru nume de folder", - "OnFileUpgrade": "La actualizare fișier" + "OnFileUpgrade": "La actualizare fișier", + "CloneIndexer": "Clonează Indexer", + "CloneProfile": "Clonează Profil", + "DownloadClientUnavailable": "Client de descărcare indisponibil", + "Clone": "Clonează" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 95eadfe44..bc7adb611 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -878,5 +878,26 @@ "Install": "Kur", "InstallMajorVersionUpdate": "Güncellemeyi Kur", "InstallMajorVersionUpdateMessage": "Bu güncelleştirme yeni bir ana sürüm yükleyecek ve sisteminizle uyumlu olmayabilir. Bu güncelleştirmeyi yüklemek istediğinizden emin misiniz?", - "InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin." + "InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin.", + "AnimeEpisodeTypeDescription": "Kesin bölüm numarası kullanılarak yayınlanan bölümler", + "AnimeEpisodeTypeFormat": "Kesin bölüm numarası ({format})", + "Warning": "Uyarı", + "AirsTimeOn": "{time} {networkLabel} üzerinde", + "AirsTomorrowOn": "Yarın {time}'da {networkLabel}'da", + "AppUpdatedVersion": "{appName} `{version}` sürümüne güncellendi, en son değişiklikleri almak için {appName} uygulamasını yeniden başlatmanız gerekiyor ", + "ApplyTags": "Etiketleri Uygula", + "AnalyseVideoFiles": "Video dosyalarını analiz edin", + "Anime": "Anime", + "AnimeEpisodeFormat": "Anime Bölüm Formatı", + "AudioInfo": "Ses Bilgisi", + "AuthBasic": "Temel (Tarayıcı Açılır Penceresi)", + "ApplyTagsHelpTextHowToApplySeries": "Seçili serilere etiketler nasıl uygulanır?", + "AptUpdater": "Güncellemeyi yüklemek için apt'ı kullanın", + "AnalyseVideoFilesHelpText": "Çözünürlük, çalışma zamanı ve kodek bilgileri gibi video bilgilerini dosyalardan çıkarın. Bu, {appName} uygulamasının taramalar sırasında yüksek disk veya ağ etkinliğine neden olabilecek dosyanın bölümlerini okumasını gerektirir.", + "Delay": "Gecikme", + "Fallback": "Geri Çek", + "ManageFormats": "Biçimleri Yönet", + "FavoriteFolderAdd": "Favori Klasör Ekle", + "FavoriteFolderRemove": "Favori Klasörü Kaldır", + "FavoriteFolders": "Favori Klasörler" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index fb90a8b5c..fd54bd66d 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1953,5 +1953,6 @@ "InstallMajorVersionUpdateMessageLink": "请查看 [{domain}]({url}) 以获取更多信息。", "Install": "安装", "InstallMajorVersionUpdate": "安装更新", - "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?" + "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?", + "Fallback": "备选" } From 675e3cd38a14ea33c27f2d66a4be2bf802e17d88 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:59:25 +0200 Subject: [PATCH 638/762] New: Labels support for Transmission 4.0 Closes #7300 --- .../Extensions/IEnumerableExtensions.cs | 5 - .../TransmissionTests/TransmissionFixture.cs | 10 +- .../TransmissionFixtureBase.cs | 5 +- .../Clients/Transmission/Transmission.cs | 48 ++++++- .../Clients/Transmission/TransmissionBase.cs | 62 ++++++--- .../Clients/Transmission/TransmissionProxy.cs | 123 +++++++++++++----- .../Transmission/TransmissionSettings.cs | 12 +- .../Transmission/TransmissionTorrent.cs | 3 + .../Download/Clients/Vuze/Vuze.cs | 5 +- 9 files changed, 200 insertions(+), 73 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index 4e592c575..6eb544c1d 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -148,10 +148,5 @@ namespace NzbDrone.Common.Extensions { return string.Join(separator, source.Select(predicate)); } - - public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer = null) - { - return new HashSet<T>(source, comparer); - } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs index 3bbb417b9..3f2780268 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs @@ -13,6 +13,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests [TestFixture] public class TransmissionFixture : TransmissionFixtureBase<Transmission> { + [SetUp] + public void Setup_Transmission() + { + Mocker.GetMock<ITransmissionProxy>() + .Setup(v => v.GetClientVersion(It.IsAny<TransmissionSettings>(), It.IsAny<bool>())) + .Returns("4.0.6"); + } + [Test] public void queued_item_should_have_required_properties() { @@ -272,7 +280,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests public void should_only_check_version_number(string version) { Mocker.GetMock<ITransmissionProxy>() - .Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>())) + .Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>(), true)) .Returns(version); Subject.Test().IsValid.Should().BeTrue(); diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs index 5cfb3acf6..5763d85a8 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs @@ -29,7 +29,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests Host = "127.0.0.1", Port = 2222, Username = "admin", - Password = "pass" + Password = "pass", + TvCategory = "" }; Subject.Definition = new DownloadClientDefinition(); @@ -152,7 +153,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests } Mocker.GetMock<ITransmissionProxy>() - .Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>())) + .Setup(s => s.GetTorrents(null, It.IsAny<TransmissionSettings>())) .Returns(torrents); } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs index 1cfc134c5..78b7ed984 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; @@ -15,6 +17,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission { public class Transmission : TransmissionBase { + public override string Name => "Transmission"; + public override bool SupportsLabels => HasClientVersion(4, 0); + public Transmission(ITransmissionProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, IHttpClient httpClient, @@ -28,9 +33,48 @@ namespace NzbDrone.Core.Download.Clients.Transmission { } + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + if (!SupportsLabels) + { + throw new NotSupportedException($"{Name} does not support marking items as imported"); + } + + // set post-import category + if (Settings.TvImportedCategory.IsNotNullOrWhiteSpace() && + Settings.TvImportedCategory != Settings.TvCategory) + { + var hash = downloadClientItem.DownloadId.ToLowerInvariant(); + var torrent = _proxy.GetTorrents(new[] { hash }, Settings).FirstOrDefault(); + + if (torrent == null) + { + _logger.Warn("Could not find torrent with hash \"{0}\" in Transmission.", hash); + return; + } + + try + { + var labels = torrent.Labels.ToHashSet(StringComparer.InvariantCultureIgnoreCase); + labels.Add(Settings.TvImportedCategory); + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + labels.Remove(Settings.TvCategory); + } + + _proxy.SetTorrentLabels(hash, labels, Settings); + } + catch (DownloadClientException ex) + { + _logger.Warn(ex, "Failed to set post-import torrent label \"{0}\" for {1} in Transmission.", Settings.TvImportedCategory, downloadClientItem.Title); + } + } + } + protected override ValidationFailure ValidateVersion() { - var versionString = _proxy.GetClientVersion(Settings); + var versionString = _proxy.GetClientVersion(Settings, true); _logger.Debug("Transmission version information: {0}", versionString); @@ -44,7 +88,5 @@ namespace NzbDrone.Core.Download.Clients.Transmission return null; } - - public override string Name => "Transmission"; } } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 87810d016..48f6be3be 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; @@ -18,6 +19,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission { public abstract class TransmissionBase : TorrentClientBase<TransmissionSettings> { + public abstract bool SupportsLabels { get; } + protected readonly ITransmissionProxy _proxy; public TransmissionBase(ITransmissionProxy proxy, @@ -37,7 +40,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public override IEnumerable<DownloadClientItem> GetItems() { var configFunc = new Lazy<TransmissionConfig>(() => _proxy.GetConfig(Settings)); - var torrents = _proxy.GetTorrents(Settings); + var torrents = _proxy.GetTorrents(null, Settings); var items = new List<DownloadClientItem>(); @@ -45,36 +48,45 @@ namespace NzbDrone.Core.Download.Clients.Transmission { var outputPath = new OsPath(torrent.DownloadDir); - if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) + if (Settings.TvCategory.IsNotNullOrWhiteSpace() && SupportsLabels && torrent.Labels is { Count: > 0 }) { - if (!new OsPath(Settings.TvDirectory).Contains(outputPath)) + if (!torrent.Labels.Contains(Settings.TvCategory, StringComparer.InvariantCultureIgnoreCase)) { continue; } } - else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + else { - var directories = outputPath.FullPath.Split('\\', '/'); - if (!directories.Contains(Settings.TvCategory)) + if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) { - continue; + if (!new OsPath(Settings.TvDirectory).Contains(outputPath)) + { + continue; + } + } + else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + var directories = outputPath.FullPath.Split('\\', '/'); + if (!directories.Contains(Settings.TvCategory)) + { + continue; + } } } outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath); - var item = new DownloadClientItem(); - item.DownloadId = torrent.HashString.ToUpper(); - item.Category = Settings.TvCategory; - item.Title = torrent.Name; - - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); - - item.OutputPath = GetOutputPath(outputPath, torrent); - item.TotalSize = torrent.TotalSize; - item.RemainingSize = torrent.LeftUntilDone; - item.SeedRatio = torrent.DownloadedEver <= 0 ? 0 : - (double)torrent.UploadedEver / torrent.DownloadedEver; + var item = new DownloadClientItem + { + DownloadId = torrent.HashString.ToUpper(), + Category = Settings.TvCategory, + Title = torrent.Name, + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace() && SupportsLabels), + OutputPath = GetOutputPath(outputPath, torrent), + TotalSize = torrent.TotalSize, + RemainingSize = torrent.LeftUntilDone, + SeedRatio = torrent.DownloadedEver <= 0 ? 0 : (double)torrent.UploadedEver / torrent.DownloadedEver + }; if (torrent.Eta >= 0) { @@ -300,7 +312,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission { try { - _proxy.GetTorrents(Settings); + _proxy.GetTorrents(null, Settings); } catch (Exception ex) { @@ -310,5 +322,15 @@ namespace NzbDrone.Core.Download.Clients.Transmission return null; } + + protected bool HasClientVersion(int major, int minor) + { + var rawVersion = _proxy.GetClientVersion(Settings); + + var versionResult = Regex.Match(rawVersion, @"(?<!\(|(\d|\.)+)(\d|\.)+(?!\)|(\d|\.)+)").Value; + var clientVersion = Version.Parse(versionResult); + + return clientVersion >= new Version(major, minor); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 45190fb16..d20076153 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NLog; @@ -12,15 +15,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission { public interface ITransmissionProxy { - List<TransmissionTorrent> GetTorrents(TransmissionSettings settings); + IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings); void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings); void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings); TransmissionConfig GetConfig(TransmissionSettings settings); string GetProtocolVersion(TransmissionSettings settings); - string GetClientVersion(TransmissionSettings settings); + string GetClientVersion(TransmissionSettings settings, bool force = false); void RemoveTorrent(string hash, bool removeData, TransmissionSettings settings); void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings); + void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings); } public class TransmissionProxy : ITransmissionProxy @@ -28,50 +32,66 @@ namespace NzbDrone.Core.Download.Clients.Transmission private readonly IHttpClient _httpClient; private readonly Logger _logger; - private ICached<string> _authSessionIDCache; + private readonly ICached<string> _authSessionIdCache; + private readonly ICached<string> _versionCache; public TransmissionProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) { _httpClient = httpClient; _logger = logger; - _authSessionIDCache = cacheManager.GetCache<string>(GetType(), "authSessionID"); + _authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authSessionID"); + _versionCache = cacheManager.GetCache<string>(GetType(), "versions"); } - public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings) + public IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings) { - var result = GetTorrentStatus(settings); + var result = GetTorrentStatus(hashStrings, settings); - var torrents = ((JArray)result.Arguments["torrents"]).ToObject<List<TransmissionTorrent>>(); + var torrents = ((JArray)result.Arguments["torrents"]).ToObject<ReadOnlyCollection<TransmissionTorrent>>(); return torrents; } public void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings) { - var arguments = new Dictionary<string, object>(); - arguments.Add("filename", torrentUrl); - arguments.Add("paused", settings.AddPaused); + var arguments = new Dictionary<string, object> + { + { "filename", torrentUrl }, + { "paused", settings.AddPaused } + }; if (!downloadDirectory.IsNullOrWhiteSpace()) { arguments.Add("download-dir", downloadDirectory); } + if (settings.TvCategory.IsNotNullOrWhiteSpace()) + { + arguments.Add("labels", new List<string> { settings.TvCategory }); + } + ProcessRequest("torrent-add", arguments, settings); } public void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings) { - var arguments = new Dictionary<string, object>(); - arguments.Add("metainfo", Convert.ToBase64String(torrentData)); - arguments.Add("paused", settings.AddPaused); + var arguments = new Dictionary<string, object> + { + { "metainfo", Convert.ToBase64String(torrentData) }, + { "paused", settings.AddPaused } + }; if (!downloadDirectory.IsNullOrWhiteSpace()) { arguments.Add("download-dir", downloadDirectory); } + if (settings.TvCategory.IsNotNullOrWhiteSpace()) + { + arguments.Add("labels", new List<string> { settings.TvCategory }); + } + ProcessRequest("torrent-add", arguments, settings); } @@ -82,8 +102,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission return; } - var arguments = new Dictionary<string, object>(); - arguments.Add("ids", new[] { hash }); + var arguments = new Dictionary<string, object> + { + { "ids", new List<string> { hash } } + }; if (seedConfiguration.Ratio != null) { @@ -97,6 +119,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission arguments.Add("seedIdleMode", 1); } + // Avoid extraneous request if no limits are to be set + if (arguments.All(arg => arg.Key == "ids")) + { + return; + } + ProcessRequest("torrent-set", arguments, settings); } @@ -107,11 +135,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission return config.RpcVersion; } - public string GetClientVersion(TransmissionSettings settings) + public string GetClientVersion(TransmissionSettings settings, bool force = false) { - var config = GetConfig(settings); + var cacheKey = $"version:{$"{GetBaseUrl(settings)}:{settings.Password}".SHA256Hash()}"; - return config.Version; + if (force) + { + _versionCache.Remove(cacheKey); + } + + return _versionCache.Get(cacheKey, () => GetConfig(settings).Version, TimeSpan.FromHours(6)); } public TransmissionConfig GetConfig(TransmissionSettings settings) @@ -124,21 +157,36 @@ namespace NzbDrone.Core.Download.Clients.Transmission public void RemoveTorrent(string hashString, bool removeData, TransmissionSettings settings) { - var arguments = new Dictionary<string, object>(); - arguments.Add("ids", new string[] { hashString }); - arguments.Add("delete-local-data", removeData); + var arguments = new Dictionary<string, object> + { + { "ids", new List<string> { hashString } }, + { "delete-local-data", removeData } + }; ProcessRequest("torrent-remove", arguments, settings); } public void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings) { - var arguments = new Dictionary<string, object>(); - arguments.Add("ids", new string[] { hashString }); + var arguments = new Dictionary<string, object> + { + { "ids", new List<string> { hashString } } + }; ProcessRequest("queue-move-top", arguments, settings); } + public void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings) + { + var arguments = new Dictionary<string, object> + { + { "ids", new List<string> { hash } }, + { "labels", labels.ToImmutableHashSet() } + }; + + ProcessRequest("torrent-set", arguments, settings); + } + private TransmissionResponse GetSessionVariables(TransmissionSettings settings) { // Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio. @@ -151,14 +199,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission return ProcessRequest("session-stats", null, settings); } - private TransmissionResponse GetTorrentStatus(TransmissionSettings settings) - { - return GetTorrentStatus(null, settings); - } - private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, TransmissionSettings settings) { - var fields = new string[] + var fields = new List<string> { "id", "hashString", // Unique torrent ID. Use this instead of the client id? @@ -179,11 +222,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission "seedIdleLimit", "seedIdleMode", "fileCount", - "file-count" + "file-count", + "labels" }; - var arguments = new Dictionary<string, object>(); - arguments.Add("fields", fields); + var arguments = new Dictionary<string, object> + { + { "fields", fields } + }; if (hashStrings != null) { @@ -195,9 +241,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission return result; } + private string GetBaseUrl(TransmissionSettings settings) + { + return HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); + } + private HttpRequestBuilder BuildRequest(TransmissionSettings settings) { - var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) + var requestBuilder = new HttpRequestBuilder(GetBaseUrl(settings)) .Resource("rpc") .Accept(HttpAccept.Json); @@ -212,11 +263,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission { var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); - var sessionId = _authSessionIDCache.Find(authKey); + var sessionId = _authSessionIdCache.Find(authKey); if (sessionId == null || reauthenticate) { - _authSessionIDCache.Remove(authKey); + _authSessionIdCache.Remove(authKey); var authLoginRequest = BuildRequest(settings).Build(); authLoginRequest.SuppressHttpError = true; @@ -244,7 +295,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission _logger.Debug("Transmission authentication succeeded."); - _authSessionIDCache.Set(authKey, sessionId); + _authSessionIdCache.Set(authKey, sessionId); } requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 4a34df4aa..17fc43bd3 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission Host = "localhost"; Port = 9091; UrlBase = "/transmission/"; + TvCategory = "tv-sonarr"; } [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] @@ -59,16 +60,19 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] + [FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] + public string TvImportedCategory { get; set; } + + [FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] public string TvDirectory { get; set; } - [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] + [FieldDefinition(9, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] + [FieldDefinition(10, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] + [FieldDefinition(11, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index 4e66b7a02..687bab40b 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Transmission @@ -11,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public long TotalSize { get; set; } public long LeftUntilDone { get; set; } public bool IsFinished { get; set; } + public IReadOnlyCollection<string> Labels { get; set; } = Array.Empty<string>(); public long Eta { get; set; } public TransmissionTorrentStatus Status { get; set; } public long SecondsDownloading { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index db12ea64f..26793e18c 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -15,6 +15,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze { private const int MINIMUM_SUPPORTED_PROTOCOL_VERSION = 14; + public override string Name => "Vuze"; + public override bool SupportsLabels => false; + public Vuze(ITransmissionProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, IHttpClient httpClient, @@ -67,7 +70,5 @@ namespace NzbDrone.Core.Download.Clients.Vuze return null; } - - public override string Name => "Vuze"; } } From ceeec091f85d0094e07537b7f62f18292655a710 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Nov 2024 16:22:32 -0800 Subject: [PATCH 639/762] Fixed: Normalize unicode characters when comparing paths for equality Closes #6657 --- src/NzbDrone.Common.Test/PathExtensionFixture.cs | 10 ++++++++++ src/NzbDrone.Common/Extensions/PathExtensions.cs | 4 ++++ src/NzbDrone.Common/PathEqualityComparer.cs | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index d06d2d9a8..870c30866 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using FluentAssertions; using Moq; using NUnit.Framework; @@ -434,5 +435,14 @@ namespace NzbDrone.Common.Test { parentPath.GetRelativePath(childPath).Should().Be(relativePath); } + + [Test] + public void should_be_equal_with_different_unicode_representations() + { + var path1 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormC); + var path2 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormD); + + path1.PathEquals(path2); + } } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 3245ca580..bbad26f60 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -54,6 +54,10 @@ namespace NzbDrone.Common.Extensions public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null) { + // Normalize paths to ensure unicode characters are represented the same way + firstPath = firstPath.Normalize(); + secondPath = secondPath?.Normalize(); + if (!comparison.HasValue) { comparison = DiskProviderBase.PathStringComparison; diff --git a/src/NzbDrone.Common/PathEqualityComparer.cs b/src/NzbDrone.Common/PathEqualityComparer.cs index 5b9c3aa1c..bd6fa430d 100644 --- a/src/NzbDrone.Common/PathEqualityComparer.cs +++ b/src/NzbDrone.Common/PathEqualityComparer.cs @@ -21,10 +21,10 @@ namespace NzbDrone.Common { if (OsInfo.IsWindows) { - return obj.CleanFilePath().ToLower().GetHashCode(); + return obj.CleanFilePath().Normalize().ToLower().GetHashCode(); } - return obj.CleanFilePath().GetHashCode(); + return obj.CleanFilePath().Normalize().GetHashCode(); } } } From 5bc943583c70e197b9666aae2f1978e52045cdb2 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 10 Nov 2024 15:22:16 -0800 Subject: [PATCH 640/762] Don't try to process items that didn't import in manual import --- .../MediaFiles/EpisodeImport/Manual/ManualImportService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 0e5d3c732..b3112bc24 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -567,7 +567,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _logger.ProgressTrace("Manually imported {0} files", imported.Count); } - var untrackedImports = imported.Where(i => importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList(); + var untrackedImports = imported.Where(i => i.Result == ImportResultType.Imported && importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList(); if (untrackedImports.Any()) { From 67a1ecb0fea4e6c7dfdb68fbe3ef30d4c22398d8 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:24:45 +0200 Subject: [PATCH 641/762] Console warnings for missing translations on development builds --- frontend/src/Calendar/Legend/Legend.js | 2 +- frontend/src/Utilities/String/translate.ts | 6 ++++++ frontend/typings/Globals.d.ts | 1 + src/NzbDrone.Core/Localization/Core/en.json | 6 +++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js index f6e970e8b..6413665d3 100644 --- a/frontend/src/Calendar/Legend/Legend.js +++ b/frontend/src/Calendar/Legend/Legend.js @@ -56,7 +56,7 @@ function Legend(props) { if (showCutoffUnmetIcon) { iconsToShow.push( <LegendIconItem - name={translate('Cutoff Not Met')} + name={translate('CutoffNotMet')} icon={icons.EPISODE_FILE} kind={kinds.WARNING} fullColorEvents={fullColorEvents} diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 76ab770ea..9a633040c 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -27,6 +27,12 @@ export default function translate( key: string, tokens: Record<string, string | number | boolean> = {} ) { + const { isProduction = true } = window.Sonarr; + + if (!isProduction && !(key in translations)) { + console.warn(`Missing translation for key: ${key}`); + } + const translation = translations[key] || key; tokens.appName = 'Sonarr'; diff --git a/frontend/typings/Globals.d.ts b/frontend/typings/Globals.d.ts index d11f99013..47c7a49da 100644 --- a/frontend/typings/Globals.d.ts +++ b/frontend/typings/Globals.d.ts @@ -7,5 +7,6 @@ interface Window { theme: string; urlBase: string; version: string; + isProduction: boolean; }; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a478ad5b3..aceda5965 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -237,6 +237,7 @@ "CollectionsLoadError": "Unable to load collections", "ColonReplacement": "Colon Replacement", "ColonReplacementFormatHelpText": "Change how {appName} handles colon replacement", + "Completed": "Completed", "CompletedDownloadHandling": "Completed Download Handling", "Component": "Component", "Condition": "Condition", @@ -302,6 +303,7 @@ "CustomFormatsSpecificationResolution": "Resolution", "CustomFormatsSpecificationSource": "Source", "Cutoff": "Cutoff", + "CutoffNotMet": "Cutoff Not Met", "CutoffUnmet": "Cutoff Unmet", "CutoffUnmetLoadError": "Error loading cutoff unmet items", "CutoffUnmetNoItems": "No cutoff unmet items", @@ -542,8 +544,8 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", - "DownloadClientUnavailable": "Download Client Unavailable", "DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error", + "DownloadClientUnavailable": "Download Client Unavailable", "DownloadClientValidationApiKeyIncorrect": "API Key Incorrect", "DownloadClientValidationApiKeyRequired": "API Key Required", "DownloadClientValidationAuthenticationFailure": "Authentication Failure", @@ -1155,6 +1157,7 @@ "MediaManagementSettingsSummary": "Naming, file management settings and root folders", "Medium": "Medium", "MegabytesPerMinute": "Megabytes Per Minute", + "Menu": "Menu", "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", @@ -1575,6 +1578,7 @@ "PreferredProtocol": "Preferred Protocol", "PreferredSize": "Preferred Size", "PrefixedRange": "Prefixed Range", + "Premiere": "Premiere", "Presets": "Presets", "PreviewRename": "Preview Rename", "PreviewRenameSeason": "Preview Rename for this season", From e28b7c3df611a0beb407085f976b381a338d5ed1 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 4 Nov 2024 16:33:53 -0800 Subject: [PATCH 642/762] Fixed: .plexmatch episodes on separate lines Closes #7362 --- .../Extras/Metadata/Consumers/Plex/PlexMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs index df56a72af..62adc4be0 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex episodeFormat = $"SP{episodesInFile.First():00}"; } - content.Append($"Episode: {episodeFormat}: {episodeFile.RelativePath}"); + content.AppendLine($"Episode: {episodeFormat}: {episodeFile.RelativePath}"); } } From 6677fd11168de6dbf78d03bfedf67b89dfe1df53 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 5 Nov 2024 22:00:26 -0800 Subject: [PATCH 643/762] New: Improve stored UI settings for multiple instances under the same host Closes #7368 --- frontend/src/Store/Middleware/createPersistState.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Middleware/createPersistState.js b/frontend/src/Store/Middleware/createPersistState.js index aa16ffa9e..234157b05 100644 --- a/frontend/src/Store/Middleware/createPersistState.js +++ b/frontend/src/Store/Middleware/createPersistState.js @@ -96,14 +96,22 @@ function merge(initialState, persistedState) { return computedState; } +const KEY = 'sonarr'; + const config = { slicer, serialize, merge, - key: 'sonarr' + key: window.Sonarr.instanceName.toLowerCase().replace(/ /g, '_') || KEY }; export default function createPersistState() { + // Migrate existing local storage value to new key if it does not already exist. + // Leave old value as-is in case there are multiple instances using the same key. + if (config.key !== KEY && localStorage.getItem(KEY) && !localStorage.getItem(config.key)) { + localStorage.setItem(config.key, localStorage.getItem(KEY)); + } + // Migrate existing local storage before proceeding const persistedState = JSON.parse(localStorage.getItem(config.key)); migrate(persistedState); From 78fb20282de73c0ea47375895a807235385d90e3 Mon Sep 17 00:00:00 2001 From: Gauthier <mail@gauthierth.fr> Date: Fri, 15 Nov 2024 04:01:05 +0100 Subject: [PATCH 644/762] New: Add headers setting in webhook connection --- .../src/Components/Form/FormInputGroup.tsx | 4 + .../src/Components/Form/KeyValueListInput.css | 21 ++++ .../Form/KeyValueListInput.css.d.ts | 10 ++ .../src/Components/Form/KeyValueListInput.tsx | 104 ++++++++++++++++++ .../Components/Form/KeyValueListInputItem.css | 35 ++++++ .../Form/KeyValueListInputItem.css.d.ts | 12 ++ .../Components/Form/KeyValueListInputItem.tsx | 89 +++++++++++++++ .../Components/Form/ProviderFieldFormGroup.js | 2 + frontend/src/Helpers/Props/inputTypes.ts | 2 + .../Reflection/ReflectionExtensions.cs | 3 +- .../Annotations/FieldDefinitionAttribute.cs | 3 +- src/NzbDrone.Core/Localization/Core/en.json | 1 + .../Notifications/Webhook/WebhookProxy.cs | 5 + .../Notifications/Webhook/WebhookSettings.cs | 7 +- 14 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 frontend/src/Components/Form/KeyValueListInput.css create mode 100644 frontend/src/Components/Form/KeyValueListInput.css.d.ts create mode 100644 frontend/src/Components/Form/KeyValueListInput.tsx create mode 100644 frontend/src/Components/Form/KeyValueListInputItem.css create mode 100644 frontend/src/Components/Form/KeyValueListInputItem.css.d.ts create mode 100644 frontend/src/Components/Form/KeyValueListInputItem.tsx diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 897f19bbd..fe860a7ca 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -10,6 +10,7 @@ import CaptchaInput from './CaptchaInput'; import CheckInput from './CheckInput'; import { FormInputButtonProps } from './FormInputButton'; import FormInputHelpText from './FormInputHelpText'; +import KeyValueListInput from './KeyValueListInput'; import NumberInput from './NumberInput'; import OAuthInput from './OAuthInput'; import PasswordInput from './PasswordInput'; @@ -47,6 +48,9 @@ function getComponent(type: InputType) { case inputTypes.DEVICE: return DeviceInput; + case inputTypes.KEY_VALUE_LIST: + return KeyValueListInput; + case inputTypes.MONITOR_EPISODES_SELECT: return MonitorEpisodesSelectInput; diff --git a/frontend/src/Components/Form/KeyValueListInput.css b/frontend/src/Components/Form/KeyValueListInput.css new file mode 100644 index 000000000..d86e6a512 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInput.css @@ -0,0 +1,21 @@ +.inputContainer { + composes: input from '~Components/Form/Input.css'; + + position: relative; + min-height: 35px; + height: auto; + + &.isFocused { + outline: 0; + border-color: var(--inputFocusBorderColor); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor); + } +} + +.hasError { + composes: hasError from '~Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from '~Components/Form/Input.css'; +} diff --git a/frontend/src/Components/Form/KeyValueListInput.css.d.ts b/frontend/src/Components/Form/KeyValueListInput.css.d.ts new file mode 100644 index 000000000..972f108c9 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInput.css.d.ts @@ -0,0 +1,10 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'hasError': string; + 'hasWarning': string; + 'inputContainer': string; + 'isFocused': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInput.tsx b/frontend/src/Components/Form/KeyValueListInput.tsx new file mode 100644 index 000000000..f5c6ac19b --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInput.tsx @@ -0,0 +1,104 @@ +import classNames from 'classnames'; +import React, { useCallback, useState } from 'react'; +import { InputOnChange } from 'typings/inputs'; +import KeyValueListInputItem from './KeyValueListInputItem'; +import styles from './KeyValueListInput.css'; + +interface KeyValue { + key: string; + value: string; +} + +export interface KeyValueListInputProps { + className?: string; + name: string; + value: KeyValue[]; + hasError?: boolean; + hasWarning?: boolean; + keyPlaceholder?: string; + valuePlaceholder?: string; + onChange: InputOnChange<KeyValue[]>; +} + +function KeyValueListInput({ + className = styles.inputContainer, + name, + value = [], + hasError = false, + hasWarning = false, + keyPlaceholder, + valuePlaceholder, + onChange, +}: KeyValueListInputProps): JSX.Element { + const [isFocused, setIsFocused] = useState(false); + + const handleItemChange = useCallback( + (index: number | null, itemValue: KeyValue) => { + const newValue = [...value]; + + if (index === null) { + newValue.push(itemValue); + } else { + newValue.splice(index, 1, itemValue); + } + + onChange({ name, value: newValue }); + }, + [value, name, onChange] + ); + + const handleRemoveItem = useCallback( + (index: number) => { + const newValue = [...value]; + newValue.splice(index, 1); + onChange({ name, value: newValue }); + }, + [value, name, onChange] + ); + + const onFocus = useCallback(() => setIsFocused(true), []); + + const onBlur = useCallback(() => { + setIsFocused(false); + + const newValue = value.reduce((acc: KeyValue[], v) => { + if (v.key || v.value) { + acc.push(v); + } + return acc; + }, []); + + if (newValue.length !== value.length) { + onChange({ name, value: newValue }); + } + }, [value, name, onChange]); + + return ( + <div + className={classNames( + className, + isFocused && styles.isFocused, + hasError && styles.hasError, + hasWarning && styles.hasWarning + )} + > + {[...value, { key: '', value: '' }].map((v, index) => ( + <KeyValueListInputItem + key={index} + index={index} + keyValue={v.key} + value={v.value} + keyPlaceholder={keyPlaceholder} + valuePlaceholder={valuePlaceholder} + isNew={index === value.length} + onChange={handleItemChange} + onRemove={handleRemoveItem} + onFocus={onFocus} + onBlur={onBlur} + /> + ))} + </div> + ); +} + +export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css new file mode 100644 index 000000000..ed82db459 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInputItem.css @@ -0,0 +1,35 @@ +.itemContainer { + display: flex; + margin-bottom: 3px; + border-bottom: 1px solid var(--inputBorderColor); + + &:last-child { + margin-bottom: 0; + border-bottom: 0; + } +} + +.keyInputWrapper { + flex: 1 0 0; +} + +.valueInputWrapper { + flex: 1 0 0; + min-width: 40px; +} + +.buttonWrapper { + flex: 0 0 22px; +} + +.keyInput, +.valueInput { + width: 100%; + border: none; + background-color: transparent; + color: var(--textColor); + + &::placeholder { + color: var(--helpTextColor); + } +} diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts new file mode 100644 index 000000000..aa0c1be13 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts @@ -0,0 +1,12 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'buttonWrapper': string; + 'itemContainer': string; + 'keyInput': string; + 'keyInputWrapper': string; + 'valueInput': string; + 'valueInputWrapper': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.tsx b/frontend/src/Components/Form/KeyValueListInputItem.tsx new file mode 100644 index 000000000..c63ad50a9 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInputItem.tsx @@ -0,0 +1,89 @@ +import React, { useCallback } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import TextInput from './TextInput'; +import styles from './KeyValueListInputItem.css'; + +interface KeyValueListInputItemProps { + index: number; + keyValue: string; + value: string; + keyPlaceholder?: string; + valuePlaceholder?: string; + isNew: boolean; + onChange: (index: number, itemValue: { key: string; value: string }) => void; + onRemove: (index: number) => void; + onFocus: () => void; + onBlur: () => void; +} + +function KeyValueListInputItem({ + index, + keyValue, + value, + keyPlaceholder = 'Key', + valuePlaceholder = 'Value', + isNew, + onChange, + onRemove, + onFocus, + onBlur, +}: KeyValueListInputItemProps): JSX.Element { + const handleKeyChange = useCallback( + ({ value: keyValue }: { value: string }) => { + onChange(index, { key: keyValue, value }); + }, + [index, value, onChange] + ); + + const handleValueChange = useCallback( + ({ value }: { value: string }) => { + onChange(index, { key: keyValue, value }); + }, + [index, keyValue, onChange] + ); + + const handleRemovePress = useCallback(() => { + onRemove(index); + }, [index, onRemove]); + + return ( + <div className={styles.itemContainer}> + <div className={styles.keyInputWrapper}> + <TextInput + className={styles.keyInput} + name="key" + value={keyValue} + placeholder={keyPlaceholder} + onChange={handleKeyChange} + onFocus={onFocus} + onBlur={onBlur} + /> + </div> + + <div className={styles.valueInputWrapper}> + <TextInput + className={styles.valueInput} + name="value" + value={value} + placeholder={valuePlaceholder} + onChange={handleValueChange} + onFocus={onFocus} + onBlur={onBlur} + /> + </div> + + <div className={styles.buttonWrapper}> + {isNew ? null : ( + <IconButton + name={icons.REMOVE} + tabIndex={-1} + onPress={handleRemovePress} + /> + )} + </div> + </div> + ); +} + +export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index 4fcf99cc0..a4f13dbd1 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.CHECK; case 'device': return inputTypes.DEVICE; + case 'keyValueList': + return inputTypes.KEY_VALUE_LIST; case 'password': return inputTypes.PASSWORD; case 'number': diff --git a/frontend/src/Helpers/Props/inputTypes.ts b/frontend/src/Helpers/Props/inputTypes.ts index d0ecc3553..a0c4c817c 100644 --- a/frontend/src/Helpers/Props/inputTypes.ts +++ b/frontend/src/Helpers/Props/inputTypes.ts @@ -2,6 +2,7 @@ export const AUTO_COMPLETE = 'autoComplete'; export const CAPTCHA = 'captcha'; export const CHECK = 'check'; export const DEVICE = 'device'; +export const KEY_VALUE_LIST = 'keyValueList'; export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const FLOAT = 'float'; @@ -31,6 +32,7 @@ export const all = [ CAPTCHA, CHECK, DEVICE, + KEY_VALUE_LIST, MONITOR_EPISODES_SELECT, MONITOR_NEW_ITEMS_SELECT, FLOAT, diff --git a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs index d832e0f27..54371a2bb 100644 --- a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs +++ b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs @@ -34,7 +34,8 @@ namespace NzbDrone.Common.Reflection || type == typeof(string) || type == typeof(DateTime) || type == typeof(Version) - || type == typeof(decimal); + || type == typeof(decimal) + || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)); } public static bool IsReadable(this PropertyInfo propertyInfo) diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 22088b01f..cc480337d 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -101,7 +101,8 @@ namespace NzbDrone.Core.Annotations TagSelect, RootFolder, QualityProfile, - SeriesTag + SeriesTag, + KeyValueList, } public enum HiddenType diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index aceda5965..d70350e67 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1436,6 +1436,7 @@ "NotificationsSettingsWebhookMethod": "Method", "NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice", "NotificationsSettingsWebhookUrl": "Webhook URL", + "NotificationsSettingsWebhookHeaders": "Headers", "NotificationsSignalSettingsGroupIdPhoneNumber": "Group ID / Phone Number", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Group ID / Phone Number of the receiver", "NotificationsSignalSettingsPasswordHelpText": "Password used to authenticate requests toward signal-api", diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs index 23a7fbdc8..a7a7025e7 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -43,6 +43,11 @@ namespace NzbDrone.Core.Notifications.Webhook request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password); } + foreach (var header in settings.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + _httpClient.Execute(request); } catch (HttpException ex) diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs index 565f454e2..51d91f7db 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -20,6 +21,7 @@ namespace NzbDrone.Core.Notifications.Webhook public WebhookSettings() { Method = Convert.ToInt32(WebhookMethod.POST); + Headers = new List<KeyValuePair<string, string>>(); } [FieldDefinition(0, Label = "NotificationsSettingsWebhookUrl", Type = FieldType.Url)] @@ -34,6 +36,9 @@ namespace NzbDrone.Core.Notifications.Webhook [FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } + [FieldDefinition(4, Label = "NotificationsSettingsWebhookHeaders", Type = FieldType.KeyValueList, Advanced = true)] + public IEnumerable<KeyValuePair<string, string>> Headers { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); From 88f4016fe0ed768f4206d04479156c45517f15b7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 6 Nov 2024 20:58:45 -0800 Subject: [PATCH 645/762] New: Parse original from release name when specified Closes #5805 --- .../ParserTests/LanguageParserFixture.cs | 10 ++++++++++ src/NzbDrone.Core/Parser/LanguageParser.cs | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index cbbe5bf1f..e07ea5b3a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -432,6 +432,16 @@ namespace NzbDrone.Core.Test.ParserTests result.Languages.Should().Contain(Language.English); } + [TestCase("Series.Title.S01E01.Original.1080P.WEB.H264-RlsGrp")] + [TestCase("Series.Title.S01E01.Orig.1080P.WEB.H264-RlsGrp")] + [TestCase("Series / S1E1-10 of 10 [2023, HEVC, HDR10, Dolby Vision, WEB-DL 2160p] [Hybrid] 3 XX + Original")] + public void should_parse_original_title_from_release_name(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Languages.Count.Should().Be(1); + result.Languages.Should().Contain(Language.Original); + } + [TestCase("Остання серія (Сезон 1) / The Last Series (Season 1) (2024) WEB-DLRip-AVC 2xUkr/Eng | Sub Ukr/Eng")] [TestCase("Справжня серія (Сезон 1-3) / True Series (Season 1-3) (2014-2019) BDRip-AVC 3xUkr/Eng | Ukr/Eng")] [TestCase("Серія (Сезон 1-3) / The Series (Seasons 1-3) (2019-2022) BDRip-AVC 4xUkr/Eng | Sub 2xUkr/Eng")] diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 00df92fb1..8e5a3f4f6 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; - private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)|(?<original>\b(?:orig|original)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b))(?:(?i)(?![\W|_|^]SUB))", @@ -470,6 +470,11 @@ namespace NzbDrone.Core.Parser { languages.Add(Language.Turkish); } + + if (match.Groups["original"].Success) + { + languages.Add(Language.Original); + } } return languages; From f739fd0900695e2ff312d13985c87d84ae00ea75 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 14 Nov 2024 19:01:38 -0800 Subject: [PATCH 646/762] Fixed: Allow files to be moved from Torrent Blackhole even when remove is disabled --- .../Blackhole/TorrentBlackholeFixture.cs | 2 +- .../Download/Clients/Blackhole/TorrentBlackhole.cs | 5 ++--- .../Download/Clients/Blackhole/UsenetBlackhole.cs | 10 ++++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs index a5642ed29..cd7311505 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole VerifyCompleted(result); - result.CanBeRemoved.Should().BeFalse(); + result.CanBeRemoved.Should().BeTrue(); result.CanMoveFiles.Should().BeFalse(); } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index eca8dc2f2..0e774f9a0 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -104,9 +104,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole Status = item.Status }; - queueItem.CanMoveFiles = queueItem.CanBeRemoved = - queueItem.DownloadClientInfo.RemoveCompletedDownloads && - !Settings.ReadOnly; + queueItem.CanMoveFiles = !Settings.ReadOnly; + queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads; yield return queueItem; } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index e1eb75905..2e2b316c7 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod)) { - yield return new DownloadClientItem + var queueItem = new DownloadClientItem { DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, @@ -72,10 +72,12 @@ namespace NzbDrone.Core.Download.Clients.Blackhole OutputPath = item.OutputPath, Status = item.Status, - - CanBeRemoved = true, - CanMoveFiles = true }; + + queueItem.CanMoveFiles = true; + queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads; + + yield return queueItem; } } From 202190d032257b3cd19e42606385db7052b2aae4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 5 Nov 2024 21:36:26 -0800 Subject: [PATCH 647/762] New: Replace 'Ben the Man' release group parsing with 'Ben the Men' Closes #7365 --- .../ParserTests/ReleaseGroupParserFixture.cs | 4 +++- src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 6376b5d27..4ac5141bf 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -89,7 +89,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] [TestCase("Series.Title.S08E03.720p.WEB.DL.AAC2.0.H.264.KCRT", "KCRT")] - [TestCase("S02E05 2160p WEB-DL DV HDR ENG DDP5.1 Atmos H265 MP4-BEN THE MAN", "BEN THE MAN")] + [TestCase("Series Title S02E05 2160p WEB-DL DV HDR ENG DDP5.1 Atmos H265 MP4-BEN THE MEN", "BEN THE MEN")] + [TestCase("Series Title S02E05 2160p AMZN WEB-DL DV HDR10 PLUS DDP5 1 Atmos H265 MKV-BEN THE MEN-xpost", "BEN THE MEN")] + [TestCase("Series.S01E05.1080p.WEB-DL.DDP5.1.H264-BEN.THE.MEN", "BEN.THE.MEN")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 01ab47b5d..836e52ca1 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -564,7 +564,7 @@ namespace NzbDrone.Core.Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN THE MAN)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN[_. ]THE[_. ]MEN)\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); From 936cf699ff3c09a462de5a06da95d59d4687c89e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 7 Nov 2024 21:48:00 -0800 Subject: [PATCH 648/762] Improve LanguageSelectInput --- .../src/Components/Form/FormInputGroup.tsx | 4 + .../Form/Select/LanguageSelectInput.tsx | 100 +++++++++++++----- frontend/src/Settings/UI/UISettings.js | 7 +- .../createFilteredLanguagesSelector.ts | 28 +++++ 4 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index fe860a7ca..15e16a2e8 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -19,6 +19,7 @@ import DownloadClientSelectInput from './Select/DownloadClientSelectInput'; import EnhancedSelectInput from './Select/EnhancedSelectInput'; import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput'; import IndexerSelectInput from './Select/IndexerSelectInput'; +import LanguageSelectInput from './Select/LanguageSelectInput'; import MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput'; import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput'; import ProviderDataSelectInput from './Select/ProviderOptionSelectInput'; @@ -51,6 +52,9 @@ function getComponent(type: InputType) { case inputTypes.KEY_VALUE_LIST: return KeyValueListInput; + case inputTypes.LANGUAGE_SELECT: + return LanguageSelectInput; + case inputTypes.MONITOR_EPISODES_SELECT: return MonitorEpisodesSelectInput; diff --git a/frontend/src/Components/Form/Select/LanguageSelectInput.tsx b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx index 80efde065..3c9bbc150 100644 --- a/frontend/src/Components/Form/Select/LanguageSelectInput.tsx +++ b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx @@ -1,43 +1,95 @@ -import React, { useMemo } from 'react'; -import { EnhancedSelectInputChanged } from 'typings/inputs'; +import React, { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import Language from 'Language/Language'; +import createFilteredLanguagesSelector from 'Store/Selectors/createFilteredLanguagesSelector'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput, { EnhancedSelectInputValue, } from './EnhancedSelectInput'; -interface LanguageSelectInputProps { +interface LanguageSelectInputOnChangeProps { name: string; - value: number; - values: EnhancedSelectInputValue<number>[]; - onChange: (change: EnhancedSelectInputChanged<number>) => void; + value: number | string | Language; } -function LanguageSelectInput({ - values, +interface LanguageSelectInputProps { + name: string; + value: number | string | Language; + includeNoChange: boolean; + includeNoChangeDisabled?: boolean; + includeMixed: boolean; + onChange: (payload: LanguageSelectInputOnChangeProps) => void; +} + +export default function LanguageSelectInput({ + value, + includeNoChange, + includeNoChangeDisabled, + includeMixed, onChange, ...otherProps }: LanguageSelectInputProps) { - const mappedValues = useMemo(() => { - const minId = values.reduce( - (min: number, v) => (v.key < 1 ? v.key : min), - values[0].key + const { items } = useSelector(createFilteredLanguagesSelector(true)); + + const values = useMemo(() => { + const result: EnhancedSelectInputValue<number | string>[] = items.map( + (item) => { + return { + key: item.id, + value: item.name, + }; + } ); - return values.map(({ key, value }) => { - return { - key, - value, - dividerAfter: minId < 1 ? key === minId : false, - }; - }); - }, [values]); + if (includeNoChange) { + result.unshift({ + key: 'noChange', + value: translate('NoChange'), + isDisabled: includeNoChangeDisabled, + }); + } + + if (includeMixed) { + result.unshift({ + key: 'mixed', + value: `(${translate('Mixed')})`, + isDisabled: true, + }); + } + + return result; + }, [includeNoChange, includeNoChangeDisabled, includeMixed, items]); + + const selectValue = + typeof value === 'number' || typeof value === 'string' ? value : value.id; + + const handleChange = useCallback( + (payload: LanguageSelectInputOnChangeProps) => { + if (typeof value === 'number') { + onChange(payload); + } else { + const language = items.find((i) => i.id === payload.value); + + onChange({ + ...payload, + value: language + ? { + id: language.id, + name: language.name, + } + : ({ id: payload.value } as Language), + }); + } + }, + [value, items, onChange] + ); return ( <EnhancedSelectInput {...otherProps} - values={mappedValues} - onChange={onChange} + value={selectValue} + values={values} + onChange={handleChange} /> ); } - -export default LanguageSelectInput; diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index 963967f15..00c5037d9 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -66,10 +66,10 @@ class UISettings extends Component { isFetching, error, settings, + languages, hasSettings, onInputChange, onSavePress, - languages, ...otherProps } = this.props; @@ -213,9 +213,8 @@ class UISettings extends Component { <FormGroup> <FormLabel>{translate('UiLanguage')}</FormLabel> <FormInputGroup - type={inputTypes.SELECT} + type={inputTypes.LANGUAGE_SELECT} name="uiLanguage" - values={languages} helpText={translate('UiLanguageHelpText')} helpTextWarning={translate('BrowserReloadRequired')} onChange={onInputChange} @@ -244,8 +243,8 @@ UISettings.propTypes = { isFetching: PropTypes.bool.isRequired, error: PropTypes.object, settings: PropTypes.object.isRequired, - hasSettings: PropTypes.bool.isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired, + hasSettings: PropTypes.bool.isRequired, onSavePress: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts b/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts new file mode 100644 index 000000000..f5c87388e --- /dev/null +++ b/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts @@ -0,0 +1,28 @@ +import { createSelector } from 'reselect'; +import { LanguageSettingsAppState } from 'App/State/SettingsAppState'; +import Language from 'Language/Language'; +import createLanguagesSelector from './createLanguagesSelector'; + +export default function createFilteredLanguagesSelector(filterUnknown = false) { + const filterItems = ['Any', 'Original']; + + if (filterUnknown) { + filterItems.push('Unknown'); + } + + return createSelector(createLanguagesSelector(), (languages) => { + const { isFetching, isPopulated, error, items } = + languages as LanguageSettingsAppState; + + const filteredLanguages = items.filter( + (lang: Language) => !filterItems.includes(lang.name) + ); + + return { + isFetching, + isPopulated, + error, + items: filteredLanguages, + }; + }); +} From 3e99917e9d2ba486355e80ccba9e199f73a4f5af Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 7 Nov 2024 21:55:10 -0800 Subject: [PATCH 649/762] Fixed: Closing on click outside select input and styling on Library Import --- .../Import/ImportSeriesFooter.css | 10 +--------- .../Form/Select/EnhancedSelectInput.tsx | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesFooter.css b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesFooter.css index 415155274..d0c6e98ae 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesFooter.css +++ b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesFooter.css @@ -1,18 +1,10 @@ .inputContainer { margin-right: 20px; min-width: 150px; - - div { - margin-top: 10px; - - &:first-child { - margin-top: 0; - } - } } .label { - margin-bottom: 3px; + margin-bottom: 10px; font-weight: bold; } diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx index b47f8da3d..f3b547082 100644 --- a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx @@ -192,7 +192,7 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( const { top, bottom } = data.offsets.reference; const windowHeight = window.innerHeight; - if (/^botton/.test(data.placement)) { + if (/^bottom/.test(data.placement)) { data.styles.maxHeight = windowHeight - bottom; } else { data.styles.maxHeight = top; @@ -233,18 +233,12 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( }, [handleWindowClick]); const handlePress = useCallback(() => { - if (isOpen) { - removeListener(); - } else { - addListener(); - } - if (!isOpen && onOpen) { onOpen(); } setIsOpen(!isOpen); - }, [isOpen, setIsOpen, addListener, removeListener, onOpen]); + }, [isOpen, setIsOpen, onOpen]); const handleSelect = useCallback( (newValue: ArrayElement<V>) => { @@ -411,6 +405,16 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( } }); + useEffect(() => { + if (isOpen) { + addListener(); + } else { + removeListener(); + } + + return removeListener; + }, [isOpen, addListener, removeListener]); + return ( <div> <Manager> From ca0bb14027f3409014e7cf9ffa8e04e577001d77 Mon Sep 17 00:00:00 2001 From: Elias Benbourenane <eliasbenbourenane@gmail.com> Date: Thu, 14 Nov 2024 22:27:56 -0500 Subject: [PATCH 650/762] Allow `GetFileSize` to follow symlinks --- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index e0ac2bdc2..07b6775cb 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -189,6 +189,18 @@ namespace NzbDrone.Common.Disk } var fi = new FileInfo(path); + + // If the file is a symlink, resolve the target path and get the size of the target file. + if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + var targetPath = fi.ResolveLinkTarget(true)?.FullName; + + if (targetPath != null) + { + fi = new FileInfo(targetPath); + } + } + return fi.Length; } From dcbef6b7b7d14ebc5ea93d02c80ac41dc0bb4b20 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 24 Nov 2024 13:23:00 +0000 Subject: [PATCH 651/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: 4kwins <hanszimmerme@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Mizuyoru_TW <mizuyoru.tw@gmail.com> Co-authored-by: Stanislav <stasstrochewskij@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_TW/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 9 +++++++-- src/NzbDrone.Core/Localization/Core/pt_BR.json | 10 +++++++--- src/NzbDrone.Core/Localization/Core/ru.json | 3 ++- src/NzbDrone.Core/Localization/Core/tr.json | 2 +- src/NzbDrone.Core/Localization/Core/zh_CN.json | 3 ++- src/NzbDrone.Core/Localization/Core/zh_TW.json | 9 ++++++++- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index e6fc85ab3..60e0b6f25 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1016,7 +1016,7 @@ "ImportListStatusAllUnavailableHealthCheckMessage": "Ninguna lista está disponible debido a fallos", "ImportLists": "Listas de Importación", "ImportListsAniListSettingsImportHiatusHelpText": "Medios: Series en hiatus", - "ImportListsAniListSettingsImportCompleted": "Importación Completados", + "ImportListsAniListSettingsImportCompleted": "Importar Completados", "ImportListsAniListSettingsImportCancelledHelpText": "Medios: Series que están canceladas", "ImportListsCustomListSettingsName": "Lista personalizada", "ImportListsCustomListSettingsUrl": "URL de la lista", @@ -2130,5 +2130,10 @@ "FavoriteFolders": "Carpetas favoritas", "ManageFormats": "Gestionar formatos", "FavoriteFolderAdd": "Añadir carpeta favorita", - "FavoriteFolderRemove": "Eliminar carpeta favorita" + "FavoriteFolderRemove": "Eliminar carpeta favorita", + "NotificationsSettingsWebhookHeaders": "Cabeceras", + "Completed": "Completado", + "CutoffNotMet": "Límite no alcanzado", + "Menu": "Menú", + "Premiere": "Estreno" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8c52145f6..efd8cf626 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -55,7 +55,7 @@ "Added": "Adicionado", "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "RemoveCompletedDownloads": "Remover downloads concluídos", - "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData", + "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na Atualização", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {downloadClientName}. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads na pasta raiz {rootFolderPath}. Você não deve baixar para uma pasta raiz.", "DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria {appName}. Você deve desativar a classificação em seu cliente de download para evitar problemas de importação.", @@ -419,7 +419,7 @@ "AddReleaseProfile": "Adicionar perfil de lançamento", "AddRemotePathMapping": "Adicionar mapeamento de caminho remoto", "AddRemotePathMappingError": "Não foi possível adicionar um novo mapeamento de caminho remoto. Tente novamente.", - "AfterManualRefresh": "Após a atualização manual", + "AfterManualRefresh": "Após a Atualização Manual", "Always": "Sempre", "AnalyseVideoFiles": "Analisar arquivos de vídeo", "Analytics": "Análises", @@ -2131,5 +2131,9 @@ "FavoriteFolderAdd": "Adicionar Pasta Favorita", "FavoriteFolderRemove": "Remover Pasta Favorita", "FavoriteFolders": "Pastas Favoritas", - "Fallback": "Reserva" + "Fallback": "Reserva", + "CutoffNotMet": "Corte Não Alcançado", + "Premiere": "Estreia", + "Completed": "Completado", + "Menu": "Menu" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 4618e8db5..5a673192b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -2088,5 +2088,6 @@ "SeasonsMonitoredAll": "Все", "LogSizeLimit": "Ограничение размера журнала", "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", - "IndexerHDBitsSettingsMediums": "Mediums" + "IndexerHDBitsSettingsMediums": "Mediums", + "CountCustomFormatsSelected": "{count} пользовательских форматов выбрано" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index bc7adb611..0d34bb52f 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -607,7 +607,7 @@ "TheLogLevelDefault": "Günlük düzeyi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/settings/general) bölümünden değiştirilebilir", "NotificationsTwitterSettingsAccessToken": "Erişim Jetonu", "AutoRedownloadFailedHelpText": "Otomatik olarak farklı bir Yayın arayın ve indirmeye çalışın", - "Queue": "Sırada", + "Queue": "Kuyruk", "RemoveFromQueue": "Kuyruktan kaldır", "TorrentDelayTime": "Torrent Gecikmesi: {torrentDelay}", "Yes": "Evet", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index fd54bd66d..450776524 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1954,5 +1954,6 @@ "Install": "安装", "InstallMajorVersionUpdate": "安装更新", "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?", - "Fallback": "备选" + "Fallback": "备选", + "FailedToFetchSettings": "设置同步失败" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index 3ee542f24..2d69ba69b 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -29,5 +29,12 @@ "ApplyTagsHelpTextHowToApplyDownloadClients": "如何將標籤套用在被選擇的下載客戶端", "AddRootFolderError": "無法加進根目錄", "ApplyTagsHelpTextAdd": "加入:將標籤加入已存在的標籤清單", - "ApplyTagsHelpTextHowToApplyImportLists": "如何套用標籤在所選擇的輸入清單" + "ApplyTagsHelpTextHowToApplyImportLists": "如何套用標籤在所選擇的輸入清單", + "Version": "版本", + "UpdateAppDirectlyLoadError": "無法直接更新 {appName},", + "Uptime": "上線時間", + "AddCustomFilter": "新增自定義過濾器", + "UnselectAll": "取消全選", + "Any": "任何", + "UpdateAvailableHealthCheckMessage": "可用的新版本: {version}" } From 91c5e6f12292e522ceb9094825525fb3684b97c6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 24 Nov 2024 15:20:44 -0800 Subject: [PATCH 652/762] Fixed: Custom Format upgrading not respecting 'Upgrades Allowed' --- .../UpgradeDiskSpecificationFixture.cs | 34 +++++++++++++++++++ .../Specifications/UpgradableSpecification.cs | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 66a38f076..658810bbc 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.CustomFormats; @@ -365,5 +366,38 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } + + [Test] + public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_format_cutoff_is_above_current_score() + { + var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 }; + + GivenProfile(new QualityProfile + { + Cutoff = Quality.SDTV.Id, + MinFormatScore = 0, + CutoffFormatScore = 10000, + Items = Qualities.QualityFixture.GetDefaultQualities(), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"), + UpgradeAllowed = false + }); + + _parseResultSingle.Series.QualityProfile.Value.FormatItems = new List<ProfileFormatItem> + { + new ProfileFormatItem + { + Format = customFormat, + Score = 50 + } + }; + + GivenFileQuality(new QualityModel(Quality.WEBDL1080p)); + GivenNewQuality(new QualityModel(Quality.WEBDL1080p)); + + GivenOldCustomFormats(new List<CustomFormat>()); + GivenNewCustomFormats(new List<CustomFormat> { customFormat }); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index 916560f8c..ceaf3815c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -135,8 +135,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications private bool CustomFormatCutoffNotMet(QualityProfile profile, List<CustomFormat> currentFormats) { var score = profile.CalculateCustomFormatScore(currentFormats); + var cutoff = profile.UpgradeAllowed ? profile.CutoffFormatScore : profile.MinFormatScore; - return score < profile.CutoffFormatScore; + return score < cutoff; } public bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, List<CustomFormat> currentFormats, QualityModel newQuality = null) From 8b38ccfb633f59936820d9865f349ca3b1527751 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 26 Nov 2024 17:11:01 -0800 Subject: [PATCH 653/762] Bump version to 4.0.11 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67fd6f9e0..1083b1a98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.10 + VERSION: 4.0.11 jobs: backend: From b51a49097941e5f306cae5785c63985b319784fd Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Nov 2024 16:50:56 -0800 Subject: [PATCH 654/762] Rename SizeLeft and TimeLeft queue item properties Closes #7392 --- .../Download/Pending/PendingReleaseService.cs | 10 +++++----- src/NzbDrone.Core/Queue/Queue.cs | 4 ++-- src/NzbDrone.Core/Queue/QueueService.cs | 8 ++++---- src/Sonarr.Api.V3/Queue/QueueController.cs | 10 +++++----- src/Sonarr.Api.V3/Queue/QueueResource.cs | 20 ++++++++++++++----- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 47d8d0755..67cc05842 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -359,11 +359,11 @@ namespace NzbDrone.Core.Download.Pending ect = ect.AddMinutes(_configService.RssSyncInterval); } - var timeleft = ect.Subtract(DateTime.UtcNow); + var timeLeft = ect.Subtract(DateTime.UtcNow); - if (timeleft.TotalSeconds < 0) + if (timeLeft.TotalSeconds < 0) { - timeleft = TimeSpan.Zero; + timeLeft = TimeSpan.Zero; } string downloadClientName = null; @@ -385,9 +385,9 @@ namespace NzbDrone.Core.Download.Pending Quality = pendingRelease.RemoteEpisode.ParsedEpisodeInfo.Quality, Title = pendingRelease.Title, Size = pendingRelease.RemoteEpisode.Release.Size, - Sizeleft = pendingRelease.RemoteEpisode.Release.Size, + SizeLeft = pendingRelease.RemoteEpisode.Release.Size, RemoteEpisode = pendingRelease.RemoteEpisode, - Timeleft = timeleft, + TimeLeft = timeLeft, EstimatedCompletionTime = ect, Added = pendingRelease.Added, Status = Enum.TryParse(pendingRelease.Reason.ToString(), out QueueStatus outValue) ? outValue : QueueStatus.Unknown, diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index c38749678..c52fac49f 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -18,8 +18,8 @@ namespace NzbDrone.Core.Queue public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } - public decimal Sizeleft { get; set; } - public TimeSpan? Timeleft { get; set; } + public decimal SizeLeft { get; set; } + public TimeSpan? TimeLeft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public DateTime? Added { get; set; } public QueueStatus Status { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 7142bd03c..1735c8e7a 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -67,8 +67,8 @@ namespace NzbDrone.Core.Queue Quality = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Quality ?? new QualityModel(Quality.Unknown), Title = Parser.Parser.RemoveFileExtension(trackedDownload.DownloadItem.Title), Size = trackedDownload.DownloadItem.TotalSize, - Sizeleft = trackedDownload.DownloadItem.RemainingSize, - Timeleft = trackedDownload.DownloadItem.RemainingTime, + SizeLeft = trackedDownload.DownloadItem.RemainingSize, + TimeLeft = trackedDownload.DownloadItem.RemainingTime, Status = Enum.TryParse(trackedDownload.DownloadItem.Status.ToString(), out QueueStatus outValue) ? outValue : QueueStatus.Unknown, TrackedDownloadStatus = trackedDownload.Status, TrackedDownloadState = trackedDownload.State, @@ -86,9 +86,9 @@ namespace NzbDrone.Core.Queue queue.Id = HashConverter.GetHashInt31($"trackedDownload-{trackedDownload.DownloadClient}-{trackedDownload.DownloadItem.DownloadId}-ep{episode?.Id ?? 0}"); - if (queue.Timeleft.HasValue) + if (queue.TimeLeft.HasValue) { - queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value); + queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.TimeLeft.Value); } return queue; diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 6c7438203..dc4740853 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -219,8 +219,8 @@ namespace Sonarr.Api.V3.Queue if (pagingSpec.SortKey == "timeleft") { ordered = ascending - ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) - : fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); + ? fullQueue.OrderBy(q => q.TimeLeft, new TimeleftComparer()) + : fullQueue.OrderByDescending(q => q.TimeLeft, new TimeleftComparer()); } else if (pagingSpec.SortKey == "estimatedCompletionTime") { @@ -271,7 +271,7 @@ namespace Sonarr.Api.V3.Queue ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); } - ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - (q.Sizeleft / q.Size * 100)); + ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - (q.SizeLeft / q.Size * 100)); pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); pagingSpec.TotalRecords = fullQueue.Count; @@ -312,9 +312,9 @@ namespace Sonarr.Api.V3.Queue return q => q.Size; case "progress": // Avoid exploding if a download's size is 0 - return q => 100 - (q.Sizeleft / Math.Max(q.Size * 100, 1)); + return q => 100 - (q.SizeLeft / Math.Max(q.Size * 100, 1)); default: - return q => q.Timeleft; + return q => q.TimeLeft; } } diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index 5a0e47e20..06e614aeb 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -26,8 +26,8 @@ namespace Sonarr.Api.V3.Queue public int CustomFormatScore { get; set; } public decimal Size { get; set; } public string Title { get; set; } - public decimal Sizeleft { get; set; } - public TimeSpan? Timeleft { get; set; } + public decimal SizeLeft { get; set; } + public TimeSpan? TimeLeft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public DateTime? Added { get; set; } public QueueStatus Status { get; set; } @@ -42,6 +42,11 @@ namespace Sonarr.Api.V3.Queue public string Indexer { get; set; } public string OutputPath { get; set; } public bool EpisodeHasFile { get; set; } + + [Obsolete] + public decimal Sizeleft { get; set; } + [Obsolete] + public TimeSpan? Timeleft { get; set; } } public static class QueueResourceMapper @@ -70,8 +75,8 @@ namespace Sonarr.Api.V3.Queue CustomFormatScore = customFormatScore, Size = model.Size, Title = model.Title, - Sizeleft = model.Sizeleft, - Timeleft = model.Timeleft, + SizeLeft = model.SizeLeft, + TimeLeft = model.TimeLeft, EstimatedCompletionTime = model.EstimatedCompletionTime, Added = model.Added, Status = model.Status, @@ -85,7 +90,12 @@ namespace Sonarr.Api.V3.Queue DownloadClientHasPostImportCategory = model.DownloadClientHasPostImportCategory, Indexer = model.Indexer, OutputPath = model.OutputPath, - EpisodeHasFile = model.Episode?.HasFile ?? false + EpisodeHasFile = model.Episode?.HasFile ?? false, + + #pragma warning disable CS0612 + Sizeleft = model.SizeLeft, + Timeleft = model.TimeLeft, + #pragma warning restore CS0612 }; } From dba3a8243988d3e9870b841696303191e1703a0d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Nov 2024 16:51:28 -0800 Subject: [PATCH 655/762] Fixed: Prevent lack of internet from stopping all health checks from running --- .../HealthCheck/Checks/SystemTimeCheck.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs index 48a38184b..10498b831 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs @@ -23,19 +23,26 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var request = _cloudRequestBuilder.Create() - .Resource("/time") - .Build(); - - var response = _client.Execute(request); - var result = Json.Deserialize<ServiceTimeResponse>(response.Content); - var systemTime = DateTime.UtcNow; - - // +/- more than 1 day - if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1) + try { - _logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc); - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeHealthCheckMessage"), "#system-time-off"); + var request = _cloudRequestBuilder.Create() + .Resource("/time") + .Build(); + + var response = _client.Execute(request); + var result = Json.Deserialize<ServiceTimeResponse>(response.Content); + var systemTime = DateTime.UtcNow; + + // +/- more than 1 day + if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1) + { + _logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc); + return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeHealthCheckMessage"), "#system-time-off"); + } + } + catch (Exception e) + { + _logger.Warn(e, "Unable to verify system time"); } return new HealthCheck(GetType()); From 5034d83062f67c48614374ff34197ddb4e0b0cb8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Nov 2024 17:00:00 -0800 Subject: [PATCH 656/762] Fixed: Kometa and Kodi metadata failing with duplicate episode files Closes #7381 --- .../Extras/Metadata/Consumers/Kometa/KometaMetadata.cs | 10 +++++++++- .../Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs index 7085b3ddf..d994cef35 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -131,7 +131,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa try { - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + var firstEpisode = episodeFile.Episodes.Value.FirstOrDefault(); + + if (firstEpisode == null) + { + _logger.Debug("Episode file has no associated episodes, potentially a duplicate file"); + return new List<ImageFileResult>(); + } + + var screenshot = firstEpisode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); if (screenshot == null) { diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 5450a16f3..e66cc89b5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -421,7 +421,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc try { - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + var firstEpisode = episodeFile.Episodes.Value.FirstOrDefault(); + + if (firstEpisode == null) + { + _logger.Debug("Episode file has no associated episodes, potentially a duplicate file"); + return new List<ImageFileResult>(); + } + + var screenshot = firstEpisode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); if (screenshot == null) { From 12c1eb86f212f90f139d20a296ce1b15f756e05d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 21 Nov 2024 17:08:50 -0800 Subject: [PATCH 657/762] Fixed: New episodes in season follow season's monitored status Closes #7401 --- .../TvTests/RefreshEpisodeServiceFixture.cs | 60 +++++++++++++++++++ src/NzbDrone.Core/Tv/RefreshEpisodeService.cs | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 7779dfb54..ba21feca9 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -396,5 +396,65 @@ namespace NzbDrone.Core.Test.TvTests _insertedEpisodes.Any(e => e.AbsoluteEpisodeNumberAdded).Should().BeFalse(); } + + [Test] + public void should_monitor_new_episode_if_season_is_monitored() + { + var series = GetSeries(); + series.Seasons = new List<Season>(); + series.Seasons.Add(new Season { SeasonNumber = 1, Monitored = true }); + + var episodes = Builder<Episode>.CreateListOfSize(2) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + var existingEpisode = new Episode + { + SeasonNumber = episodes[0].SeasonNumber, + EpisodeNumber = episodes[0].EpisodeNumber, + Monitored = true + }; + + Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) + .Returns(new List<Episode> { existingEpisode }); + + Subject.RefreshEpisodeInfo(series, episodes); + + _updatedEpisodes.Should().HaveCount(1); + _insertedEpisodes.Should().HaveCount(1); + _insertedEpisodes.Should().OnlyContain(e => e.Monitored == true); + } + + [Test] + public void should_not_monitor_new_episode_if_season_is_not_monitored() + { + var series = GetSeries(); + series.Seasons = new List<Season>(); + series.Seasons.Add(new Season { SeasonNumber = 1, Monitored = false }); + + var episodes = Builder<Episode>.CreateListOfSize(2) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + var existingEpisode = new Episode + { + SeasonNumber = episodes[0].SeasonNumber, + EpisodeNumber = episodes[0].EpisodeNumber, + Monitored = true + }; + + Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>())) + .Returns(new List<Episode> { existingEpisode }); + + Subject.RefreshEpisodeInfo(series, episodes); + + _updatedEpisodes.Should().HaveCount(1); + _insertedEpisodes.Should().HaveCount(1); + _insertedEpisodes.Should().OnlyContain(e => e.Monitored == false); + } } } diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index 708a5f4c9..18b242835 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -145,7 +145,7 @@ namespace NzbDrone.Core.Tv private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons, Series series) { - if ((episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) || series.MonitorNewItems == NewItemMonitorTypes.None) + if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) { return false; } From 183b8b574a4dd948b5fada94d0e645b87710f223 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 26 Nov 2024 17:36:53 -0800 Subject: [PATCH 658/762] Deluge communication improvements Closes #7318 --- .../Download/Clients/Deluge/Deluge.cs | 31 ++++++----- .../Download/Clients/Deluge/DelugeProxy.cs | 52 +++++++------------ 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index a8d03166e..6b204ac0d 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge public class Deluge : TorrentClientBase<DelugeSettings> { private readonly IDelugeProxy _proxy; + private bool _hasAttemptedReconnecting; public Deluge(IDelugeProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, @@ -128,14 +129,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge foreach (var torrent in torrents) { - // Silently ignore torrents with no hash - if (torrent.Hash.IsNullOrWhiteSpace()) - { - continue; - } - - // Ignore torrents without a name, but track to log a single warning for all invalid torrents. - if (torrent.Name.IsNullOrWhiteSpace()) + // Ignore torrents without a hash or name, but track to log a single warning + // for all invalid torrents as well as reconnect to the Daemon. + if (torrent.Hash.IsNullOrWhiteSpace() || torrent.Name.IsNullOrWhiteSpace()) { ignoredCount++; continue; @@ -199,9 +195,20 @@ namespace NzbDrone.Core.Download.Clients.Deluge items.Add(item); } - if (ignoredCount > 0) + if (ignoredCount > 0 && _hasAttemptedReconnecting) { - _logger.Warn("{0} torrent(s) were ignored because they did not have a title. Check Deluge and remove any invalid torrents"); + if (_hasAttemptedReconnecting) + { + _logger.Warn("{0} torrent(s) were ignored because they did not have a hash or title. Deluge may have disconnected from it's daemon. If you continue to see this error, check Deluge for invalid torrents.", ignoredCount); + } + else + { + _proxy.ReconnectToDaemon(Settings); + } + } + else + { + _hasAttemptedReconnecting = false; } return items; @@ -322,9 +329,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge return null; } - var enabledPlugins = _proxy.GetEnabledPlugins(Settings); + var methods = _proxy.GetMethods(Settings); - if (!enabledPlugins.Contains("Label")) + if (!methods.Any(m => m.StartsWith("label."))) { return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginInactive")) { diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index ea670cfd6..2879aefaf 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -18,8 +18,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge Dictionary<string, object> GetConfig(DelugeSettings settings); DelugeTorrent[] GetTorrents(DelugeSettings settings); DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings); - string[] GetAvailablePlugins(DelugeSettings settings); - string[] GetEnabledPlugins(DelugeSettings settings); + string[] GetMethods(DelugeSettings settings); string[] GetAvailableLabels(DelugeSettings settings); DelugeLabel GetLabelOptions(DelugeSettings settings); void SetTorrentLabel(string hash, string label, DelugeSettings settings); @@ -30,6 +29,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings); bool RemoveTorrent(string hash, bool removeData, DelugeSettings settings); void MoveTorrentToTopInQueue(string hash, DelugeSettings settings); + void ReconnectToDaemon(DelugeSettings settings); } public class DelugeProxy : IDelugeProxy @@ -51,25 +51,14 @@ namespace NzbDrone.Core.Download.Clients.Deluge public string GetVersion(DelugeSettings settings) { - try + var methods = GetMethods(settings); + + if (methods.Contains("daemon.get_version")) { - var response = ProcessRequest<string>(settings, "daemon.info"); - - return response; + return ProcessRequest<string>(settings, "daemon.get_version"); } - catch (DownloadClientException ex) - { - if (ex.Message.Contains("Unknown method")) - { - // Deluge v2 beta replaced 'daemon.info' with 'daemon.get_version'. - // It may return or become official, for now we just retry with the get_version api. - var response = ProcessRequest<string>(settings, "daemon.get_version"); - return response; - } - - throw; - } + return ProcessRequest<string>(settings, "daemon.info"); } public Dictionary<string, object> GetConfig(DelugeSettings settings) @@ -101,6 +90,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge return GetTorrents(response); } + public string[] GetMethods(DelugeSettings settings) + { + var response = ProcessRequest<string[]>(settings, "system.listMethods"); + + return response; + } + public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings) { dynamic options = new ExpandoObject(); @@ -159,20 +155,6 @@ namespace NzbDrone.Core.Download.Clients.Deluge ProcessRequest<object>(settings, "core.queue_top", (object)new string[] { hash }); } - public string[] GetAvailablePlugins(DelugeSettings settings) - { - var response = ProcessRequest<string[]>(settings, "core.get_available_plugins"); - - return response; - } - - public string[] GetEnabledPlugins(DelugeSettings settings) - { - var response = ProcessRequest<string[]>(settings, "core.get_enabled_plugins"); - - return response; - } - public string[] GetAvailableLabels(DelugeSettings settings) { var response = ProcessRequest<string[]>(settings, "label.get_labels"); @@ -223,6 +205,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge ProcessRequest<object>(settings, "label.set_torrent", hash, label); } + public void ReconnectToDaemon(DelugeSettings settings) + { + ProcessRequest<string>(settings, "web.disconnect"); + ConnectDaemon(BuildRequest(settings)); + } + private JsonRpcRequestBuilder BuildRequest(DelugeSettings settings) { var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); From e361f18837d98c089f7dc9c0190221ca8e2cf225 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 22 Nov 2024 18:39:35 -0800 Subject: [PATCH 659/762] New: Support for new SABnzbd history retention values Closes #7373 --- .../SabnzbdTests/SabnzbdFixture.cs | 31 +++++++++++ .../Download/Clients/Sabnzbd/Sabnzbd.cs | 53 ++++++++++++++----- .../Clients/Sabnzbd/SabnzbdCategory.cs | 2 + 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 9b74e81e8..d83bb7f6d 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -478,6 +478,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue(); } + [TestCase("all", 0)] + [TestCase("days-archive", 15)] + [TestCase("days-delete", 15)] + public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number) + { + _config.Misc.history_retention_option = option; + _config.Misc.history_retention_number = number; + + var downloadClientInfo = Subject.GetStatus(); + + downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse(); + } + + [TestCase("number-archive", 10)] + [TestCase("number-delete", 10)] + [TestCase("number-archive", 0)] + [TestCase("number-delete", 0)] + [TestCase("days-archive", 3)] + [TestCase("days-delete", 3)] + [TestCase("all-archive", 0)] + [TestCase("all-delete", 0)] + public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number) + { + _config.Misc.history_retention_option = option; + _config.Misc.history_retention_number = number; + + var downloadClientInfo = Subject.GetStatus(); + + downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue(); + } + [TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")] [TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")] [TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")] diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index b0a4e2495..df33b256d 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -278,20 +278,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; } - if (config.Misc.history_retention.IsNullOrWhiteSpace()) - { - status.RemovesCompletedDownloads = false; - } - else if (config.Misc.history_retention.EndsWith("d")) - { - int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1), - out var daysRetention); - status.RemovesCompletedDownloads = daysRetention < 14; - } - else - { - status.RemovesCompletedDownloads = config.Misc.history_retention != "0"; - } + status.RemovesCompletedDownloads = RemovesCompletedDownloads(config); return status; } @@ -548,6 +535,44 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return categories.Contains(category); } + private bool RemovesCompletedDownloads(SabnzbdConfig config) + { + var retention = config.Misc.history_retention; + var option = config.Misc.history_retention_option; + var number = config.Misc.history_retention_number; + + switch (option) + { + case "all": + return false; + case "number-archive": + case "number-delete": + return true; + case "days-archive": + case "days-delete": + return number < 14; + case "all-archive": + case "all-delete": + return true; + } + + // TODO: Remove these checks once support for SABnzbd < 4.3 is removed + + if (retention.IsNullOrWhiteSpace()) + { + return false; + } + + if (retention.EndsWith("d")) + { + int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1), + out var daysRetention); + return daysRetention < 14; + } + + return retention != "0"; + } + private bool ValidatePath(DownloadClientItem downloadClientItem) { var downloadItemOutputPath = downloadClientItem.OutputPath; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index aa04edc5d..740b34ddb 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs @@ -32,6 +32,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public bool enable_date_sorting { get; set; } public bool pre_check { get; set; } public string history_retention { get; set; } + public string history_retention_option { get; set; } + public int history_retention_number { get; set; } } public class SabnzbdCategory From 2f62494adc51c41450258309393139cbeb3c6277 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Nov 2024 16:47:47 -0800 Subject: [PATCH 660/762] Convert EditSeriesModal to TypeScript --- frontend/src/App/State/AppSectionState.ts | 11 +- frontend/src/App/State/SeriesAppState.ts | 2 + frontend/src/Series/Details/SeriesDetails.js | 4 +- frontend/src/Series/Edit/EditSeriesModal.js | 26 -- frontend/src/Series/Edit/EditSeriesModal.tsx | 34 +++ .../Series/Edit/EditSeriesModalConnector.js | 40 --- .../src/Series/Edit/EditSeriesModalContent.js | 240 ----------------- .../Series/Edit/EditSeriesModalContent.tsx | 248 ++++++++++++++++++ .../Index/Overview/SeriesIndexOverview.tsx | 4 +- .../Index/Posters/SeriesIndexPoster.tsx | 4 +- .../src/Series/Index/Table/SeriesIndexRow.tsx | 4 +- .../EditImportListExclusionModalContent.tsx | 2 +- .../EditReleaseProfileModalContent.tsx | 15 +- .../src/Store/Selectors/selectSettings.js | 109 -------- .../src/Store/Selectors/selectSettings.ts | 167 ++++++++++++ .../src/Utilities/Object/getErrorMessage.ts | 18 +- frontend/src/typings/Field.ts | 2 +- frontend/src/typings/pending.ts | 49 +++- 18 files changed, 529 insertions(+), 450 deletions(-) delete mode 100644 frontend/src/Series/Edit/EditSeriesModal.js create mode 100644 frontend/src/Series/Edit/EditSeriesModal.tsx delete mode 100644 frontend/src/Series/Edit/EditSeriesModalConnector.js delete mode 100644 frontend/src/Series/Edit/EditSeriesModalContent.js create mode 100644 frontend/src/Series/Edit/EditSeriesModalContent.tsx delete mode 100644 frontend/src/Store/Selectors/selectSettings.js create mode 100644 frontend/src/Store/Selectors/selectSettings.ts diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index edf2b2d9d..fa55c8e38 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,11 +1,16 @@ import Column from 'Components/Table/Column'; import { SortDirection } from 'Helpers/Props/sortDirections'; +import { ValidationFailure } from 'typings/pending'; import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { - responseJSON: { - message: string; - }; + status?: number; + responseJSON: + | { + message: string | undefined; + } + | ValidationFailure[] + | undefined; } export interface AppSectionDeleteState { diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts index 1f8a3427b..5da5987dd 100644 --- a/frontend/src/App/State/SeriesAppState.ts +++ b/frontend/src/App/State/SeriesAppState.ts @@ -59,6 +59,8 @@ interface SeriesAppState deleteOptions: { addImportListExclusion: boolean; }; + + pendingChanges: Partial<Series>; } export default SeriesAppState; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 116ce5d2f..211b40dd5 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -24,7 +24,7 @@ import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'He import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; -import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; +import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import SeriesHistoryModal from 'Series/History/SeriesHistoryModal'; import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal'; import SeriesPoster from 'Series/SeriesPoster'; @@ -709,7 +709,7 @@ class SeriesDetails extends Component { onModalClose={this.onSeriesHistoryModalClose} /> - <EditSeriesModalConnector + <EditSeriesModal isOpen={isEditSeriesModalOpen} seriesId={id} onModalClose={this.onEditSeriesModalClose} diff --git a/frontend/src/Series/Edit/EditSeriesModal.js b/frontend/src/Series/Edit/EditSeriesModal.js deleted file mode 100644 index 3edfbb7d0..000000000 --- a/frontend/src/Series/Edit/EditSeriesModal.js +++ /dev/null @@ -1,26 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import EditSeriesModalContentConnector from './EditSeriesModalContentConnector'; - -function EditSeriesModal({ isOpen, onModalClose, ...otherProps }) { - return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > - <EditSeriesModalContentConnector - {...otherProps} - onModalClose={onModalClose} - /> - </Modal> - ); -} - -EditSeriesModal.propTypes = { - ...EditSeriesModalContentConnector.propTypes, - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSeriesModal; diff --git a/frontend/src/Series/Edit/EditSeriesModal.tsx b/frontend/src/Series/Edit/EditSeriesModal.tsx new file mode 100644 index 000000000..5aabeb556 --- /dev/null +++ b/frontend/src/Series/Edit/EditSeriesModal.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { EditSeriesModalContentProps } from './EditSeriesModalContent'; +import EditSeriesModalContentConnector from './EditSeriesModalContentConnector'; + +interface EditSeriesModalProps extends EditSeriesModalContentProps { + isOpen: boolean; +} + +function EditSeriesModal({ + isOpen, + onModalClose, + ...otherProps +}: EditSeriesModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section: 'series' })); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + <Modal isOpen={isOpen} onModalClose={handleModalClose}> + <EditSeriesModalContentConnector + {...otherProps} + onModalClose={handleModalClose} + /> + </Modal> + ); +} + +export default EditSeriesModal; diff --git a/frontend/src/Series/Edit/EditSeriesModalConnector.js b/frontend/src/Series/Edit/EditSeriesModalConnector.js deleted file mode 100644 index ed1e5c6a6..000000000 --- a/frontend/src/Series/Edit/EditSeriesModalConnector.js +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditSeriesModal from './EditSeriesModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditSeriesModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'series' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - <EditSeriesModal - {...this.props} - onModalClose={this.onModalClose} - /> - ); - } -} - -EditSeriesModalConnector.propTypes = { - ...EditSeriesModal.propTypes, - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(EditSeriesModalConnector); diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.js b/frontend/src/Series/Edit/EditSeriesModalContent.js deleted file mode 100644 index 537d8990b..000000000 --- a/frontend/src/Series/Edit/EditSeriesModalContent.js +++ /dev/null @@ -1,240 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent'; -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 Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -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 Popover from 'Components/Tooltip/Popover'; -import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; -import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal'; -import translate from 'Utilities/String/translate'; -import styles from './EditSeriesModalContent.css'; - -class EditSeriesModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isConfirmMoveModalOpen: false - }; - } - - // - // Listeners - - onCancelPress = () => { - this.setState({ isConfirmMoveModalOpen: false }); - }; - - onSavePress = () => { - const { - isPathChanging, - onSavePress - } = this.props; - - if (isPathChanging && !this.state.isConfirmMoveModalOpen) { - this.setState({ isConfirmMoveModalOpen: true }); - } else { - this.setState({ isConfirmMoveModalOpen: false }); - - onSavePress(false); - } - }; - - onMoveSeriesPress = () => { - this.setState({ isConfirmMoveModalOpen: false }); - - this.props.onSavePress(true); - }; - - // - // Render - - render() { - const { - title, - item, - isSaving, - originalPath, - onInputChange, - onModalClose, - onDeleteSeriesPress, - ...otherProps - } = this.props; - - const { - monitored, - monitorNewItems, - seasonFolder, - qualityProfileId, - seriesType, - path, - tags - } = item; - - return ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('EditSeriesModalHeader', { title })} - </ModalHeader> - - <ModalBody> - <Form {...otherProps}> - <FormGroup> - <FormLabel>{translate('Monitored')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="monitored" - helpText={translate('MonitoredEpisodesHelpText')} - {...monitored} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel> - {translate('MonitorNewSeasons')} - <Popover - anchor={ - <Icon - className={styles.labelIcon} - name={icons.INFO} - /> - } - title={translate('MonitorNewSeasons')} - body={<SeriesMonitorNewItemsOptionsPopoverContent />} - position={tooltipPositions.RIGHT} - /> - </FormLabel> - - <FormInputGroup - type={inputTypes.MONITOR_NEW_ITEMS_SELECT} - name="monitorNewItems" - helpText={translate('MonitorNewSeasonsHelpText')} - {...monitorNewItems} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('UseSeasonFolder')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="seasonFolder" - helpText={translate('UseSeasonFolderHelpText')} - {...seasonFolder} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('QualityProfile')}</FormLabel> - - <FormInputGroup - type={inputTypes.QUALITY_PROFILE_SELECT} - name="qualityProfileId" - {...qualityProfileId} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('SeriesType')}</FormLabel> - - <FormInputGroup - type={inputTypes.SERIES_TYPE_SELECT} - name="seriesType" - {...seriesType} - helpText={translate('SeriesTypesHelpText')} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('Path')}</FormLabel> - - <FormInputGroup - type={inputTypes.PATH} - name="path" - {...path} - onChange={onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('Tags')}</FormLabel> - - <FormInputGroup - type={inputTypes.TAG} - name="tags" - {...tags} - onChange={onInputChange} - /> - </FormGroup> - </Form> - </ModalBody> - - <ModalFooter> - <Button - className={styles.deleteButton} - kind={kinds.DANGER} - onPress={onDeleteSeriesPress} - > - {translate('Delete')} - </Button> - - <Button - onPress={onModalClose} - > - {translate('Cancel')} - </Button> - - <SpinnerButton - isSpinning={isSaving} - onPress={this.onSavePress} - > - {translate('Save')} - </SpinnerButton> - </ModalFooter> - - <MoveSeriesModal - originalPath={originalPath} - destinationPath={path.value} - isOpen={this.state.isConfirmMoveModalOpen} - onModalClose={this.onCancelPress} - onSavePress={this.onSavePress} - onMoveSeriesPress={this.onMoveSeriesPress} - /> - </ModalContent> - ); - } -} - -EditSeriesModalContent.propTypes = { - seriesId: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - item: PropTypes.object.isRequired, - isSaving: PropTypes.bool.isRequired, - isPathChanging: PropTypes.bool.isRequired, - originalPath: PropTypes.string.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onDeleteSeriesPress: PropTypes.func.isRequired -}; - -export default EditSeriesModalContent; diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Edit/EditSeriesModalContent.tsx new file mode 100644 index 000000000..f1a7ffca4 --- /dev/null +++ b/frontend/src/Series/Edit/EditSeriesModalContent.tsx @@ -0,0 +1,248 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent'; +import AppState from 'App/State/AppState'; +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 Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 Popover from 'Components/Tooltip/Popover'; +import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; +import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal'; +import useSeries from 'Series/useSeries'; +import { saveSeries, setSeriesValue } from 'Store/Actions/seriesActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditSeriesModalContent.css'; + +export interface EditSeriesModalContentProps { + seriesId: number; + onModalClose: () => void; + onDeleteSeriesPress: () => void; +} +function EditSeriesModalContent({ + seriesId, + onModalClose, + onDeleteSeriesPress, +}: EditSeriesModalContentProps) { + const dispatch = useDispatch(); + const { + title, + monitored, + monitorNewItems, + seasonFolder, + qualityProfileId, + seriesType, + path, + tags, + } = useSeries(seriesId)!; + const { isSaving, saveError, pendingChanges } = useSelector( + (state: AppState) => state.series + ); + + const isPathChanging = pendingChanges.path && path !== pendingChanges.path; + + const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); + + const { settings, ...otherSettings } = useMemo(() => { + return selectSettings( + { + monitored, + monitorNewItems, + seasonFolder, + qualityProfileId, + seriesType, + path, + tags, + }, + pendingChanges, + saveError + ); + }, [ + monitored, + monitorNewItems, + seasonFolder, + qualityProfileId, + seriesType, + path, + tags, + pendingChanges, + saveError, + ]); + + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error actions aren't typed + dispatch(setSeriesValue({ name, value })); + }, + [dispatch] + ); + + const handleCancelPress = useCallback(() => { + setIsConfirmMoveModalOpen(false); + }, []); + + const handleSavePress = useCallback(() => { + if (isPathChanging && !isConfirmMoveModalOpen) { + setIsConfirmMoveModalOpen(true); + } else { + setIsConfirmMoveModalOpen(false); + + dispatch( + saveSeries({ + id: seriesId, + moveFiles: false, + }) + ); + } + }, [seriesId, isPathChanging, isConfirmMoveModalOpen, dispatch]); + + const handleMoveSeriesPress = useCallback(() => { + setIsConfirmMoveModalOpen(false); + + dispatch( + saveSeries({ + id: seriesId, + moveFiles: true, + }) + ); + }, [seriesId, dispatch]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('EditSeriesModalHeader', { title })}</ModalHeader> + + <ModalBody> + <Form {...otherSettings}> + <FormGroup> + <FormLabel>{translate('Monitored')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="monitored" + helpText={translate('MonitoredEpisodesHelpText')} + {...settings.monitored} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel> + {translate('MonitorNewSeasons')} + <Popover + anchor={<Icon className={styles.labelIcon} name={icons.INFO} />} + title={translate('MonitorNewSeasons')} + body={<SeriesMonitorNewItemsOptionsPopoverContent />} + position={tooltipPositions.RIGHT} + /> + </FormLabel> + + <FormInputGroup + type={inputTypes.MONITOR_NEW_ITEMS_SELECT} + name="monitorNewItems" + helpText={translate('MonitorNewSeasonsHelpText')} + {...settings.monitorNewItems} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('UseSeasonFolder')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="seasonFolder" + helpText={translate('UseSeasonFolderHelpText')} + {...settings.seasonFolder} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('QualityProfile')}</FormLabel> + + <FormInputGroup + type={inputTypes.QUALITY_PROFILE_SELECT} + name="qualityProfileId" + {...settings.qualityProfileId} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('SeriesType')}</FormLabel> + + <FormInputGroup + type={inputTypes.SERIES_TYPE_SELECT} + name="seriesType" + {...settings.seriesType} + helpText={translate('SeriesTypesHelpText')} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('Path')}</FormLabel> + + <FormInputGroup + type={inputTypes.PATH} + name="path" + {...settings.path} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('Tags')}</FormLabel> + + <FormInputGroup + type={inputTypes.TAG} + name="tags" + {...settings.tags} + onChange={handleInputChange} + /> + </FormGroup> + </Form> + </ModalBody> + + <ModalFooter> + <Button + className={styles.deleteButton} + kind={kinds.DANGER} + onPress={onDeleteSeriesPress} + > + {translate('Delete')} + </Button> + + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <SpinnerErrorButton + error={saveError} + isSpinning={isSaving} + onPress={handleSavePress} + > + {translate('Save')} + </SpinnerErrorButton> + </ModalFooter> + + <MoveSeriesModal + originalPath={path} + destinationPath={pendingChanges.path} + isOpen={isConfirmMoveModalOpen} + onModalClose={handleCancelPress} + onSavePress={handleSavePress} + onMoveSeriesPress={handleMoveSeriesPress} + /> + </ModalContent> + ); +} + +export default EditSeriesModalContent; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx index 5be820f87..dc2312193 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx @@ -9,7 +9,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; -import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; +import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar'; import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect'; import { Statistics } from 'Series/Series'; @@ -252,7 +252,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) { </div> </div> - <EditSeriesModalConnector + <EditSeriesModal isOpen={isEditSeriesModalOpen} seriesId={seriesId} onModalClose={onEditSeriesModalClose} diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index 148fefc91..8e8b128aa 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -9,7 +9,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; -import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; +import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar'; import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect'; import { Statistics } from 'Series/Series'; @@ -268,7 +268,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { showTags={showTags} /> - <EditSeriesModalConnector + <EditSeriesModal isOpen={isEditSeriesModalOpen} seriesId={seriesId} onModalClose={onEditSeriesModalClose} diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 727e5b4d0..ec03f63a9 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -15,7 +15,7 @@ import Column from 'Components/Table/Column'; import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; -import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; +import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import createSeriesIndexItemSelector from 'Series/Index/createSeriesIndexItemSelector'; import { Statistics } from 'Series/Series'; import SeriesBanner from 'Series/SeriesBanner'; @@ -492,7 +492,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { return null; })} - <EditSeriesModalConnector + <EditSeriesModal isOpen={isEditSeriesModalOpen} seriesId={seriesId} onModalClose={onEditSeriesModalClose} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index 2fb7da1b7..33947cc8f 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -39,7 +39,7 @@ function createImportListExclusionSelector(id?: number) { importListExclusions; const mapping = id - ? items.find((i) => i.id === id) + ? items.find((i) => i.id === id)! : newImportListExclusion; const settings = selectSettings(mapping, pendingChanges, saveError); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 930064974..36ad57c47 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -19,14 +19,15 @@ import { setReleaseProfileValue, } from 'Store/Actions/Settings/releaseProfiles'; import selectSettings from 'Store/Selectors/selectSettings'; -import { PendingSection } from 'typings/pending'; import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import translate from 'Utilities/String/translate'; import styles from './EditReleaseProfileModalContent.css'; const tagInputDelimiters = ['Tab', 'Enter']; -const newReleaseProfile = { +const newReleaseProfile: ReleaseProfile = { + id: 0, + name: '', enabled: true, required: [], ignored: [], @@ -41,8 +42,12 @@ function createReleaseProfileSelector(id?: number) { const { items, isFetching, error, isSaving, saveError, pendingChanges } = releaseProfiles; - const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile; - const settings = selectSettings(mapping, pendingChanges, saveError); + const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile; + const settings = selectSettings<ReleaseProfile>( + mapping, + pendingChanges, + saveError + ); return { id, @@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) { error, isSaving, saveError, - item: settings.settings as PendingSection<ReleaseProfile>, + item: settings.settings, ...settings, }; } diff --git a/frontend/src/Store/Selectors/selectSettings.js b/frontend/src/Store/Selectors/selectSettings.js deleted file mode 100644 index e3db2bf9d..000000000 --- a/frontend/src/Store/Selectors/selectSettings.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'lodash'; - -function getValidationFailures(saveError) { - if (!saveError || saveError.status !== 400) { - return []; - } - - return _.cloneDeep(saveError.responseJSON); -} - -function mapFailure(failure) { - return { - errorMessage: failure.errorMessage, - infoLink: failure.infoLink, - detailedDescription: failure.detailedDescription, - - // TODO: Remove these renamed properties - message: failure.errorMessage, - link: failure.infoLink, - detailedMessage: failure.detailedDescription - }; -} - -function selectSettings(item, pendingChanges, saveError) { - const validationFailures = getValidationFailures(saveError); - - // Merge all settings from the item along with pending - // changes to ensure any settings that were not included - // with the item are included. - const allSettings = Object.assign({}, item, pendingChanges); - - const settings = _.reduce(allSettings, (result, value, key) => { - if (key === 'fields') { - return result; - } - - // Return a flattened value - if (key === 'implementationName') { - result.implementationName = item[key]; - - return result; - } - - const setting = { - value: item[key], - errors: _.map(_.remove(validationFailures, (failure) => { - return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning; - }), mapFailure), - - warnings: _.map(_.remove(validationFailures, (failure) => { - return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning; - }), mapFailure) - }; - - if (pendingChanges.hasOwnProperty(key)) { - setting.previousValue = setting.value; - setting.value = pendingChanges[key]; - setting.pending = true; - } - - result[key] = setting; - return result; - }, {}); - - const fields = _.reduce(item.fields, (result, f) => { - const field = Object.assign({ pending: false }, f); - const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name); - - if (hasPendingFieldChange) { - field.previousValue = field.value; - field.value = pendingChanges.fields[field.name]; - field.pending = true; - } - - field.errors = _.map(_.remove(validationFailures, (failure) => { - return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning; - }), mapFailure); - - field.warnings = _.map(_.remove(validationFailures, (failure) => { - return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning; - }), mapFailure); - - result.push(field); - return result; - }, []); - - if (fields.length) { - settings.fields = fields; - } - - const validationErrors = _.filter(validationFailures, (failure) => { - return !failure.isWarning; - }); - - const validationWarnings = _.filter(validationFailures, (failure) => { - return failure.isWarning; - }); - - return { - settings, - validationErrors, - validationWarnings, - hasPendingChanges: !_.isEmpty(pendingChanges), - hasSettings: !_.isEmpty(settings), - pendingChanges - }; -} - -export default selectSettings; diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts new file mode 100644 index 000000000..b7b6ab8c7 --- /dev/null +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -0,0 +1,167 @@ +import { cloneDeep, isEmpty } from 'lodash'; +import { Error } from 'App/State/AppSectionState'; +import Field from 'typings/Field'; +import { + Failure, + Pending, + PendingField, + PendingSection, + ValidationError, + ValidationFailure, + ValidationWarning, +} from 'typings/pending'; + +interface ValidationFailures { + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +function getValidationFailures(saveError?: Error): ValidationFailures { + if (!saveError || saveError.status !== 400) { + return { + errors: [], + warnings: [], + }; + } + + return cloneDeep(saveError.responseJSON as ValidationFailure[]).reduce( + (acc: ValidationFailures, failure: ValidationFailure) => { + if (failure.isWarning) { + acc.warnings.push(failure as ValidationWarning); + } else { + acc.errors.push(failure as ValidationError); + } + + return acc; + }, + { + errors: [], + warnings: [], + } + ); +} + +function getFailures(failures: ValidationFailure[], key: string) { + const result = []; + + for (let i = failures.length - 1; i >= 0; i--) { + if (failures[i].propertyName.toLowerCase() === key.toLowerCase()) { + result.unshift(mapFailure(failures[i])); + + failures.splice(i, 1); + } + } + + return result; +} + +function mapFailure(failure: ValidationFailure): Failure { + return { + errorMessage: failure.errorMessage, + infoLink: failure.infoLink, + detailedDescription: failure.detailedDescription, + + // TODO: Remove these renamed properties + message: failure.errorMessage, + link: failure.infoLink, + detailedMessage: failure.detailedDescription, + }; +} + +interface ModelBaseSetting { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [id: string]: any; +} + +function selectSettings<T extends ModelBaseSetting>( + item: T, + pendingChanges: Partial<ModelBaseSetting>, + saveError?: Error +) { + const { errors, warnings } = getValidationFailures(saveError); + + // Merge all settings from the item along with pending + // changes to ensure any settings that were not included + // with the item are included. + const allSettings = Object.assign({}, item, pendingChanges); + + const settings = Object.keys(allSettings).reduce( + (acc: PendingSection<T>, key) => { + if (key === 'fields') { + return acc; + } + + // Return a flattened value + if (key === 'implementationName') { + // acc.implementationName = item[key]; + + return acc; + } + + const setting: Pending<T> = { + value: item[key], + errors: getFailures(errors, key), + warnings: getFailures(warnings, key), + }; + + if (pendingChanges.hasOwnProperty(key)) { + setting.previousValue = setting.value; + setting.value = pendingChanges[key]; + setting.pending = true; + } + + // @ts-expect-error - This is a valid key + acc[key] = setting; + return acc; + }, + {} as PendingSection<T> + ); + + if ('fields' in item) { + const fields = + (item.fields as Field[]).reduce((acc: PendingField<T>[], f) => { + const field: PendingField<T> = Object.assign( + { pending: false, errors: [], warnings: [] }, + f + ); + + if ('fields' in pendingChanges) { + const pendingChangesFields = pendingChanges.fields as Record< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + >; + + if (pendingChangesFields.hasOwnProperty(field.name)) { + field.previousValue = field.value; + field.value = pendingChangesFields[field.name]; + field.pending = true; + } + } + + field.errors = getFailures(errors, field.name); + field.warnings = getFailures(warnings, field.name); + + acc.push(field); + return acc; + }, []) ?? []; + + if (fields.length) { + settings.fields = fields; + } + } + + const validationErrors = errors; + const validationWarnings = warnings; + + return { + settings, + validationErrors, + validationWarnings, + hasPendingChanges: !isEmpty(pendingChanges), + hasSettings: !isEmpty(settings), + pendingChanges, + }; +} + +export default selectSettings; diff --git a/frontend/src/Utilities/Object/getErrorMessage.ts b/frontend/src/Utilities/Object/getErrorMessage.ts index d757ceec3..72474b853 100644 --- a/frontend/src/Utilities/Object/getErrorMessage.ts +++ b/frontend/src/Utilities/Object/getErrorMessage.ts @@ -1,19 +1,15 @@ -interface AjaxResponse { - responseJSON: - | { - message: string | undefined; - } - | undefined; -} +import { Error } from 'App/State/AppSectionState'; -function getErrorMessage(xhr: AjaxResponse, fallbackErrorMessage?: string) { - if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) { +function getErrorMessage(xhr: Error, fallbackErrorMessage?: string) { + if (!xhr || !xhr.responseJSON) { return fallbackErrorMessage; } - const message = xhr.responseJSON.message; + if ('message' in xhr.responseJSON && xhr.responseJSON.message) { + return xhr.responseJSON.message; + } - return message || fallbackErrorMessage; + return fallbackErrorMessage; } export default getErrorMessage; diff --git a/frontend/src/typings/Field.ts b/frontend/src/typings/Field.ts index 4ebb05278..24a0b35ac 100644 --- a/frontend/src/typings/Field.ts +++ b/frontend/src/typings/Field.ts @@ -12,7 +12,7 @@ interface Field { order: number; name: string; label: string; - value: boolean | number | string; + value: boolean | number | string | number[]; type: string; advanced: boolean; privacy: string; diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index b84a60ada..13c2123cc 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -1,4 +1,7 @@ +import Field from './Field'; + export interface ValidationFailure { + isWarning: boolean; propertyName: string; errorMessage: string; infoLink?: string; @@ -14,12 +17,46 @@ export interface ValidationWarning extends ValidationFailure { isWarning: true; } -export interface Pending<T> { - value: T; - errors: ValidationError[]; - warnings: ValidationWarning[]; +export interface Failure { + errorMessage: ValidationFailure['errorMessage']; + infoLink: ValidationFailure['infoLink']; + detailedDescription: ValidationFailure['detailedDescription']; + + // TODO: Remove these renamed properties + + message: ValidationFailure['errorMessage']; + link: ValidationFailure['infoLink']; + detailedMessage: ValidationFailure['detailedDescription']; } -export type PendingSection<T> = { - [K in keyof T]: Pending<T[K]>; +export interface Pending<T> { + value: T; + errors: Failure[]; + warnings: Failure[]; + pending?: boolean; + previousValue?: T; +} + +export interface PendingField<T> + extends Field, + Omit<Pending<T>, 'previousValue' | 'value'> { + previousValue?: Field['value']; +} + +// export type PendingSection<T> = { +// [K in keyof T]: Pending<T[K]>; +// }; + +type Mapped<T> = { + [Prop in keyof T]: { + value: T[Prop]; + errors: Failure[]; + warnings: Failure[]; + pending?: boolean; + previousValue?: T[Prop]; + }; +}; + +export type PendingSection<T> = Mapped<T> & { + fields?: PendingField<T>[]; }; From a90866a73e6cff9a286c23e60c74672f4c0d317a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Nov 2024 16:48:05 -0800 Subject: [PATCH 661/762] Webpack web target --- frontend/build/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 85056b3cd..53f8ef431 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -26,6 +26,7 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', + target: 'web', stats: { children: false From 4491df3ae7530f2167beebc3548dd01fd2cc1a12 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Nov 2024 20:20:36 -0800 Subject: [PATCH 662/762] Update React and add React Query --- frontend/src/App/App.tsx | 21 ++++---- frontend/src/Helpers/Hooks/useApiQuery.ts | 56 ++++++++++++++++++++++ frontend/src/bootstrap.tsx | 9 ++-- frontend/src/index.ts | 25 ++++++++++ frontend/typings/Globals.d.ts | 1 + package.json | 9 ++-- yarn.lock | 58 +++++++++++++---------- 7 files changed, 138 insertions(+), 41 deletions(-) create mode 100644 frontend/src/Helpers/Hooks/useApiQuery.ts diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.tsx index 0014c1d3f..b71199bb3 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; import React from 'react'; import DocumentTitle from 'react-document-title'; @@ -12,17 +13,21 @@ interface AppProps { history: ConnectedRouterProps['history']; } +const queryClient = new QueryClient(); + function App({ store, history }: AppProps) { return ( <DocumentTitle title={window.Sonarr.instanceName}> - <Provider store={store}> - <ConnectedRouter history={history}> - <ApplyTheme /> - <PageConnector> - <AppRoutes /> - </PageConnector> - </ConnectedRouter> - </Provider> + <QueryClientProvider client={queryClient}> + <Provider store={store}> + <ConnectedRouter history={history}> + <ApplyTheme /> + <PageConnector> + <AppRoutes /> + </PageConnector> + </ConnectedRouter> + </Provider> + </QueryClientProvider> </DocumentTitle> ); } diff --git a/frontend/src/Helpers/Hooks/useApiQuery.ts b/frontend/src/Helpers/Hooks/useApiQuery.ts new file mode 100644 index 000000000..69c5c8672 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useApiQuery.ts @@ -0,0 +1,56 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +interface QueryOptions { + url: string; + headers?: HeadersInit; +} + +const absUrlRegex = /^(https?:)?\/\//i; +const apiRoot = window.Sonarr.apiRoot; + +function isAbsolute(url: string) { + return absUrlRegex.test(url); +} + +function getUrl(url: string) { + return apiRoot + url; +} + +function useApiQuery<T>(options: QueryOptions) { + const { url, headers } = options; + + const final = useMemo(() => { + if (isAbsolute(url)) { + return { + url, + headers, + }; + } + + return { + url: getUrl(url), + headers: { + ...headers, + 'X-Api-Key': window.Sonarr.apiKey, + }, + }; + }, [url, headers]); + + return useQuery({ + queryKey: [final.url], + queryFn: async () => { + const result = await fetch(final.url, { + headers: final.headers, + }); + + if (!result.ok) { + throw new Error('Failed to fetch'); + } + + return result.json() as T; + }, + }); +} + +export default useApiQuery; diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx index 6a6d7fc67..9ecf27e0e 100644 --- a/frontend/src/bootstrap.tsx +++ b/frontend/src/bootstrap.tsx @@ -1,6 +1,6 @@ import { createBrowserHistory } from 'history'; import React from 'react'; -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import createAppStore from 'Store/createAppStore'; import App from './App/App'; @@ -9,9 +9,8 @@ import 'Diag/ConsoleApi'; export async function bootstrap() { const history = createBrowserHistory(); const store = createAppStore(history); + const container = document.getElementById('root'); - render( - <App store={store} history={history} />, - document.getElementById('root') - ); + const root = createRoot(container!); // createRoot(container!) if you use TypeScript + root.render(<App store={store} history={history} />); } diff --git a/frontend/src/index.ts b/frontend/src/index.ts index bbb3b5932..325ea4d7f 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -14,6 +14,31 @@ window.Sonarr = await response.json(); __webpack_public_path__ = `${window.Sonarr.urlBase}/`; /* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ +const error = console.error; + +// Monkey patch console.error to filter out some warnings from React +// TODO: Remove this after the great TypeScript migration + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function logError(...parameters: any[]) { + const filter = parameters.find((parameter) => { + return ( + parameter.includes( + 'Support for defaultProps will be removed from function components in a future major release' + ) || + parameter.includes( + 'findDOMNode is deprecated and will be removed in the next major release' + ) + ); + }); + + if (!filter) { + error(...parameters); + } +} + +console.error = logError; + const { bootstrap } = await import('./bootstrap'); await bootstrap(); diff --git a/frontend/typings/Globals.d.ts b/frontend/typings/Globals.d.ts index 47c7a49da..d4f61354d 100644 --- a/frontend/typings/Globals.d.ts +++ b/frontend/typings/Globals.d.ts @@ -3,6 +3,7 @@ declare module '*.module.css'; interface Window { Sonarr: { apiKey: string; + apiRoot: string; instanceName: string; theme: string; urlBase: string; diff --git a/package.json b/package.json index 83034c9b9..fe5079530 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", "@sentry/browser": "7.119.1", + "@tanstack/react-query": "5.61.0", "@types/node": "20.16.11", - "@types/react": "18.2.79", - "@types/react-dom": "18.2.25", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", "classnames": "2.5.1", "connected-react-router": "6.9.3", "copy-to-clipboard": "3.3.3", @@ -48,7 +49,7 @@ "normalize.css": "8.0.1", "prop-types": "15.8.1", "qs": "6.13.0", - "react": "17.0.2", + "react": "18.3.1", "react-addons-shallow-compare": "15.6.3", "react-async-script": "1.2.0", "react-autosuggest": "10.1.0", @@ -58,7 +59,7 @@ "react-dnd-multi-backend": "6.0.2", "react-dnd-touch-backend": "14.1.1", "react-document-title": "2.0.3", - "react-dom": "17.0.2", + "react-dom": "18.3.1", "react-focus-lock": "2.9.4", "react-google-recaptcha": "2.1.0", "react-lazyload": "3.2.0", diff --git a/yarn.lock b/yarn.lock index 43427e636..d36aba5d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1223,6 +1223,18 @@ dependencies: "@sentry/types" "7.119.1" +"@tanstack/query-core@5.60.6": + version "5.60.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.60.6.tgz#0dd33fe231b0d18bf66d0c615b29899738300658" + integrity sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ== + +"@tanstack/react-query@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.61.0.tgz#73473feb37aa28ceb410e297ee060e18f06f88e0" + integrity sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ== + dependencies: + "@tanstack/query-core" "5.60.6" + "@types/archiver@^5.3.1": version "5.3.4" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" @@ -1335,10 +1347,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@18.2.25": - version "18.2.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" - integrity sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA== +"@types/react-dom@18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== dependencies: "@types/react" "*" @@ -1405,10 +1417,10 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@18.2.79": - version "18.2.79" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" - integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== +"@types/react@18.3.12": + version "18.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -5427,14 +5439,13 @@ react-document-title@2.0.3: prop-types "^15.5.6" react-side-effect "^1.0.2" -react-dom@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.2" react-focus-lock@2.9.4: version "2.9.4" @@ -5606,13 +5617,12 @@ react-window@1.8.10: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" read-pkg-up@^7.0.1: version "7.0.1" @@ -5979,13 +5989,12 @@ sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@>1.0.0, schema-utils@^4.0.0: version "4.2.0" @@ -6207,6 +6216,7 @@ string-template@~0.2.1: integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== From 417af2b91542e709e4b99aa5ca55b0501ba426ad Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Nov 2024 20:21:24 -0800 Subject: [PATCH 663/762] New: Ability to change root folder when editing series Closes #5544 --- .../src/Components/Form/FormInputButton.tsx | 5 +- .../src/Components/Form/FormInputGroup.tsx | 1 + frontend/src/Components/Form/PathInput.css | 4 + .../src/Components/Form/PathInput.css.d.ts | 1 + frontend/src/Components/Form/PathInput.tsx | 12 ++- .../Form/Select/RootFolderSelectInput.tsx | 9 +- frontend/src/Helpers/Props/icons.ts | 2 + .../Series/Edit/EditSeriesModalConnector.js | 40 ++++++++ .../Series/Edit/EditSeriesModalContent.tsx | 47 ++++++++++ .../Edit/RootFolder/RootFolderModal.tsx | 26 ++++++ .../RootFolder/RootFolderModalContent.tsx | 93 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + .../RootFolders/RootFolderService.cs | 4 +- .../Series/SeriesFolderController.cs | 31 +++++++ 14 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 frontend/src/Series/Edit/EditSeriesModalConnector.js create mode 100644 frontend/src/Series/Edit/RootFolder/RootFolderModal.tsx create mode 100644 frontend/src/Series/Edit/RootFolder/RootFolderModalContent.tsx create mode 100644 src/Sonarr.Api.V3/Series/SeriesFolderController.cs diff --git a/frontend/src/Components/Form/FormInputButton.tsx b/frontend/src/Components/Form/FormInputButton.tsx index f61779122..e5149ab14 100644 --- a/frontend/src/Components/Form/FormInputButton.tsx +++ b/frontend/src/Components/Form/FormInputButton.tsx @@ -14,13 +14,14 @@ function FormInputButton({ className = styles.button, canSpin = false, isLastButton = true, + kind = kinds.PRIMARY, ...otherProps }: FormInputButtonProps) { if (canSpin) { return ( <SpinnerButton className={classNames(className, !isLastButton && styles.middleButton)} - kind={kinds.PRIMARY} + kind={kind} {...otherProps} /> ); @@ -29,7 +30,7 @@ function FormInputButton({ return ( <Button className={classNames(className, !isLastButton && styles.middleButton)} - kind={kinds.PRIMARY} + kind={kind} {...otherProps} /> ); diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 15e16a2e8..0881e571a 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -145,6 +145,7 @@ interface FormInputGroupProps<T> { autoFocus?: boolean; includeNoChange?: boolean; includeNoChangeDisabled?: boolean; + valueOptions?: object; selectedValueOptions?: object; indexerFlags?: number; pending?: boolean; diff --git a/frontend/src/Components/Form/PathInput.css b/frontend/src/Components/Form/PathInput.css index 3b32b16f0..327a85ef8 100644 --- a/frontend/src/Components/Form/PathInput.css +++ b/frontend/src/Components/Form/PathInput.css @@ -16,3 +16,7 @@ height: 35px; } + +.fileBrowserMiddleButton { + composes: middleButton from '~./FormInputButton.css'; +} diff --git a/frontend/src/Components/Form/PathInput.css.d.ts b/frontend/src/Components/Form/PathInput.css.d.ts index d44c3dd56..82be3d1ff 100644 --- a/frontend/src/Components/Form/PathInput.css.d.ts +++ b/frontend/src/Components/Form/PathInput.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'fileBrowserButton': string; + 'fileBrowserMiddleButton': string; 'hasFileBrowser': string; 'inputWrapper': string; 'pathMatch': string; diff --git a/frontend/src/Components/Form/PathInput.tsx b/frontend/src/Components/Form/PathInput.tsx index f353f1be4..0caf66905 100644 --- a/frontend/src/Components/Form/PathInput.tsx +++ b/frontend/src/Components/Form/PathInput.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import React, { KeyboardEvent, SyntheticEvent, @@ -29,6 +30,7 @@ interface PathInputProps { value?: string; placeholder?: string; includeFiles: boolean; + hasButton?: boolean; hasFileBrowser?: boolean; onChange: (change: InputChanged<string>) => void; } @@ -96,6 +98,7 @@ export function PathInputInternal(props: PathInputInternalProps) { value: inputValue = '', paths, includeFiles, + hasButton, hasFileBrowser = true, onChange, onFetchPaths, @@ -229,9 +232,12 @@ export function PathInputInternal(props: PathInputInternalProps) { /> {hasFileBrowser ? ( - <div> + <> <FormInputButton - className={styles.fileBrowserButton} + className={classNames( + styles.fileBrowserButton, + hasButton && styles.fileBrowserMiddleButton + )} onPress={handleFileBrowserOpenPress} > <Icon name={icons.FOLDER_OPEN} /> @@ -245,7 +251,7 @@ export function PathInputInternal(props: PathInputInternalProps) { onChange={onChange} onModalClose={handleFileBrowserModalClose} /> - </div> + </> ) : null} </div> ); diff --git a/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx index 4704a3cd4..68b3d3da2 100644 --- a/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx +++ b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx @@ -3,7 +3,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; import usePrevious from 'Helpers/Hooks/usePrevious'; -import { addRootFolder } from 'Store/Actions/rootFolderActions'; +import { + addRootFolder, + fetchRootFolders, +} from 'Store/Actions/rootFolderActions'; import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; @@ -189,6 +192,10 @@ function RootFolderSelectInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + dispatch(fetchRootFolders()); + }, [dispatch]); + return ( <> <EnhancedSelectInput diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index e9a361066..ba6859e58 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -65,6 +65,7 @@ import { faFilter as fasFilter, faFlag as fasFlag, faFolderOpen as fasFolderOpen, + faFolderTree as farFolderTree, faForward as fasForward, faHeart as fasHeart, faHistory as fasHistory, @@ -201,6 +202,7 @@ export const REMOVE = fasTimes; export const RESTART = fasRedoAlt; export const RESTORE = fasHistory; export const REORDER = fasBars; +export const ROOT_FOLDER = farFolderTree; export const RSS = fasRss; export const SAVE = fasSave; export const SCENE_MAPPING = fasSitemap; diff --git a/frontend/src/Series/Edit/EditSeriesModalConnector.js b/frontend/src/Series/Edit/EditSeriesModalConnector.js new file mode 100644 index 000000000..ed1e5c6a6 --- /dev/null +++ b/frontend/src/Series/Edit/EditSeriesModalConnector.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditSeriesModal from './EditSeriesModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditSeriesModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'series' }); + this.props.onModalClose(); + }; + + // + // Render + + render() { + return ( + <EditSeriesModal + {...this.props} + onModalClose={this.onModalClose} + /> + ); + } +} + +EditSeriesModalConnector.propTypes = { + ...EditSeriesModal.propTypes, + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(EditSeriesModalConnector); diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Edit/EditSeriesModalContent.tsx index f1a7ffca4..2362000a9 100644 --- a/frontend/src/Series/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Edit/EditSeriesModalContent.tsx @@ -4,6 +4,7 @@ import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorN import AppState from 'App/State/AppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import Icon from 'Components/Icon'; @@ -21,6 +22,8 @@ import { saveSeries, setSeriesValue } from 'Store/Actions/seriesActions'; import selectSettings from 'Store/Selectors/selectSettings'; import { InputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; +import RootFolderModal from './RootFolder/RootFolderModal'; +import { RootFolderUpdated } from './RootFolder/RootFolderModalContent'; import styles from './EditSeriesModalContent.css'; export interface EditSeriesModalContentProps { @@ -43,11 +46,17 @@ function EditSeriesModalContent({ seriesType, path, tags, + rootFolderPath: initialRootFolderPath, } = useSeries(seriesId)!; + const { isSaving, saveError, pendingChanges } = useSelector( (state: AppState) => state.series ); + const [isRootFolderModalOpen, setIsRootFolderModalOpen] = useState(false); + + const [rootFolderPath, setRootFolderPath] = useState(initialRootFolderPath); + const isPathChanging = pendingChanges.path && path !== pendingChanges.path; const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); @@ -86,6 +95,26 @@ function EditSeriesModalContent({ [dispatch] ); + const handleRootFolderPress = useCallback(() => { + setIsRootFolderModalOpen(true); + }, []); + + const handleRootFolderModalClose = useCallback(() => { + setIsRootFolderModalOpen(false); + }, []); + + const handleRootFolderChange = useCallback( + ({ + path: newPath, + rootFolderPath: newRootFolderPath, + }: RootFolderUpdated) => { + setIsRootFolderModalOpen(false); + setRootFolderPath(newRootFolderPath); + handleInputChange({ name: 'path', value: newPath }); + }, + [handleInputChange] + ); + const handleCancelPress = useCallback(() => { setIsConfirmMoveModalOpen(false); }, []); @@ -196,6 +225,16 @@ function EditSeriesModalContent({ type={inputTypes.PATH} name="path" {...settings.path} + buttons={[ + <FormInputButton + key="fileBrowser" + kind={kinds.DEFAULT} + title="Root Folder" + onPress={handleRootFolderPress} + > + <Icon name={icons.ROOT_FOLDER} /> + </FormInputButton>, + ]} onChange={handleInputChange} /> </FormGroup> @@ -233,6 +272,14 @@ function EditSeriesModalContent({ </SpinnerErrorButton> </ModalFooter> + <RootFolderModal + isOpen={isRootFolderModalOpen} + seriesId={seriesId} + rootFolderPath={rootFolderPath} + onSavePress={handleRootFolderChange} + onModalClose={handleRootFolderModalClose} + /> + <MoveSeriesModal originalPath={path} destinationPath={pendingChanges.path} diff --git a/frontend/src/Series/Edit/RootFolder/RootFolderModal.tsx b/frontend/src/Series/Edit/RootFolder/RootFolderModal.tsx new file mode 100644 index 000000000..3fbf8ccc1 --- /dev/null +++ b/frontend/src/Series/Edit/RootFolder/RootFolderModal.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import RootFolderModalContent, { + RootFolderModalContentProps, +} from './RootFolderModalContent'; + +interface RootFolderModalProps extends RootFolderModalContentProps { + isOpen: boolean; +} + +function RootFolderModal(props: RootFolderModalProps) { + const { isOpen, rootFolderPath, seriesId, onSavePress, onModalClose } = props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <RootFolderModalContent + seriesId={seriesId} + rootFolderPath={rootFolderPath} + onSavePress={onSavePress} + onModalClose={onModalClose} + /> + </Modal> + ); +} + +export default RootFolderModal; diff --git a/frontend/src/Series/Edit/RootFolder/RootFolderModalContent.tsx b/frontend/src/Series/Edit/RootFolder/RootFolderModalContent.tsx new file mode 100644 index 000000000..d53d5e306 --- /dev/null +++ b/frontend/src/Series/Edit/RootFolder/RootFolderModalContent.tsx @@ -0,0 +1,93 @@ +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +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 useApiQuery from 'Helpers/Hooks/useApiQuery'; +import { inputTypes } from 'Helpers/Props'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; + +export interface RootFolderUpdated { + path: string; + rootFolderPath: string; +} + +export interface RootFolderModalContentProps { + seriesId: number; + rootFolderPath: string; + onSavePress(change: RootFolderUpdated): void; + onModalClose(): void; +} + +interface SeriesFolder { + folder: string; +} + +function RootFolderModalContent(props: RootFolderModalContentProps) { + const { seriesId, onSavePress, onModalClose } = props; + const { isWindows } = useSelector(createSystemStatusSelector()); + + const [rootFolderPath, setRootFolderPath] = useState(props.rootFolderPath); + + const { isLoading, data } = useApiQuery<SeriesFolder>({ + url: `/series/${seriesId}/folder`, + }); + + const onInputChange = useCallback(({ value }: InputChanged<string>) => { + setRootFolderPath(value); + }, []); + + const handleSavePress = useCallback(() => { + const separator = isWindows ? '\\' : '/'; + + onSavePress({ + path: `${rootFolderPath}${separator}${data?.folder}`, + rootFolderPath, + }); + }, [rootFolderPath, isWindows, data, onSavePress]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('UpdateSeriesPath')}</ModalHeader> + + <ModalBody> + <FormGroup> + <FormLabel>{translate('RootFolder')}</FormLabel> + + <FormInputGroup + type={inputTypes.ROOT_FOLDER_SELECT} + name="rootFolderPath" + value={rootFolderPath} + valueOptions={{ + seriesFolder: data?.folder, + isWindows, + }} + selectedValueOptions={{ + seriesFolder: data?.folder, + isWindows, + }} + helpText={translate('SeriesEditRootFolderHelpText')} + onChange={onInputChange} + /> + </FormGroup> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <Button disabled={isLoading || !data?.folder} onPress={handleSavePress}> + {translate('UpdatePath')} + </Button> + </ModalFooter> + </ModalContent> + ); +} + +export default RootFolderModalContent; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index d70350e67..53321e959 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -2085,6 +2085,8 @@ "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.", "UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.", "UpdaterLogFiles": "Updater Log Files", + "UpdatePath": "Update Path", + "UpdateSeriesPath": "Update Series Path", "Updates": "Updates", "UpgradeUntil": "Upgrade Until", "UpgradeUntilCustomFormatScore": "Upgrade Until Custom Format Score", diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 09bbf7134..277290cae 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -219,10 +219,10 @@ namespace NzbDrone.Core.RootFolders { var osPath = new OsPath(path); - return osPath.Directory.ToString().TrimEnd(osPath.IsUnixPath ? '/' : '\\'); + return osPath.Directory.ToString().GetCleanPath(); } - return possibleRootFolder.Path; + return possibleRootFolder.Path.GetCleanPath(); } } } diff --git a/src/Sonarr.Api.V3/Series/SeriesFolderController.cs b/src/Sonarr.Api.V3/Series/SeriesFolderController.cs new file mode 100644 index 000000000..06b8b141f --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesFolderController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Tv; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Series; + +[V3ApiController("series")] +public class SeriesFolderController : Controller +{ + private readonly ISeriesService _seriesService; + private readonly IBuildFileNames _fileNameBuilder; + + public SeriesFolderController(ISeriesService seriesService, IBuildFileNames fileNameBuilder) + { + _seriesService = seriesService; + _fileNameBuilder = fileNameBuilder; + } + + [HttpGet("{id}/folder")] + public object GetFolder([FromRoute] int id) + { + var series = _seriesService.GetSeries(id); + var folder = _fileNameBuilder.GetSeriesFolder(series); + + return new + { + folder + }; + } +} From 93c3f6d1d6b50a7ae06aca083aba5297d8d8b6e8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 24 Nov 2024 17:40:15 -0800 Subject: [PATCH 664/762] Fixed: Truncating long text in the middle when it shouldn't be truncated Closes #7413 --- frontend/build/webpack.config.js | 3 +- .../src/Components/Form/Tag/TagInputTag.tsx | 4 +- frontend/src/Components/MiddleTruncate.tsx | 63 +++++++++++++++++++ ...eProfileRow.css => ReleaseProfileItem.css} | 0 ...w.css.d.ts => ReleaseProfileItem.css.d.ts} | 0 ...eProfileRow.tsx => ReleaseProfileItem.tsx} | 12 ++-- .../Profiles/Release/ReleaseProfiles.css | 2 +- .../Profiles/Release/ReleaseProfiles.tsx | 4 +- frontend/typings/MiddleTruncate.d.ts | 16 ----- package.json | 1 - yarn.lock | 34 +--------- 11 files changed, 78 insertions(+), 61 deletions(-) create mode 100644 frontend/src/Components/MiddleTruncate.tsx rename frontend/src/Settings/Profiles/Release/{ReleaseProfileRow.css => ReleaseProfileItem.css} (100%) rename frontend/src/Settings/Profiles/Release/{ReleaseProfileRow.css.d.ts => ReleaseProfileItem.css.d.ts} (100%) rename frontend/src/Settings/Profiles/Release/{ReleaseProfileRow.tsx => ReleaseProfileItem.tsx} (91%) delete mode 100644 frontend/typings/MiddleTruncate.d.ts diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 53f8ef431..da97f7331 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -52,8 +52,7 @@ module.exports = (env) => { 'node_modules' ], alias: { - jquery: 'jquery/dist/jquery.min', - 'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate' + jquery: 'jquery/dist/jquery.min' }, fallback: { buffer: false, diff --git a/frontend/src/Components/Form/Tag/TagInputTag.tsx b/frontend/src/Components/Form/Tag/TagInputTag.tsx index 484bf45e0..7b549767c 100644 --- a/frontend/src/Components/Form/Tag/TagInputTag.tsx +++ b/frontend/src/Components/Form/Tag/TagInputTag.tsx @@ -1,8 +1,8 @@ import React, { useCallback } from 'react'; -import MiddleTruncate from 'react-middle-truncate'; import Label, { LabelProps } from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; +import MiddleTruncate from 'Components/MiddleTruncate'; import { icons } from 'Helpers/Props'; import { TagBase } from './TagInput'; import styles from './TagInputTag.css'; @@ -58,7 +58,7 @@ function TagInputTag<T extends TagBase>({ tabIndex={-1} onPress={handleDelete} > - <MiddleTruncate text={String(tag.name)} start={10} end={10} /> + <MiddleTruncate text={String(tag.name)} /> </Link> {canEdit ? ( diff --git a/frontend/src/Components/MiddleTruncate.tsx b/frontend/src/Components/MiddleTruncate.tsx new file mode 100644 index 000000000..f635b0bc9 --- /dev/null +++ b/frontend/src/Components/MiddleTruncate.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useMeasure from 'Helpers/Hooks/useMeasure'; + +interface MiddleTruncateProps { + text: string; +} + +function getTruncatedText(text: string, length: number) { + return `${text.slice(0, length)}...${text.slice(text.length - length)}`; +} + +function MiddleTruncate({ text }: MiddleTruncateProps) { + const [containerRef, { width: containerWidth }] = useMeasure(); + const [textRef, { width: textWidth }] = useMeasure(); + const [truncatedText, setTruncatedText] = useState(text); + const truncatedTextRef = useRef(text); + + useEffect(() => { + setTruncatedText(text); + }, [text]); + + useEffect(() => { + if (!containerWidth || !textWidth) { + return; + } + + if (textWidth <= containerWidth) { + return; + } + + const characterLength = textWidth / text.length; + const charactersToRemove = + Math.ceil(text.length - containerWidth / characterLength) + 3; + let length = Math.ceil(text.length / 2 - charactersToRemove / 2); + + let updatedText = getTruncatedText(text, length); + + // Make sure if the text is still too long, we keep reducing the length + // each time we re-run this. + while ( + updatedText.length >= truncatedTextRef.current.length && + length > 10 + ) { + length--; + updatedText = getTruncatedText(text, length); + } + + // Store the value in the ref so we can compare it in the next render, + // without triggering this effect every time we change the text. + truncatedTextRef.current = updatedText; + setTruncatedText(updatedText); + }, [text, truncatedTextRef, containerWidth, textWidth]); + + return ( + <div ref={containerRef} style={{ whiteSpace: 'nowrap' }}> + <div ref={textRef} style={{ display: 'inline-block' }}> + {truncatedText} + </div> + </div> + ); +} + +export default MiddleTruncate; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.css similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css rename to frontend/src/Settings/Profiles/Release/ReleaseProfileItem.css diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.css.d.ts similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts rename to frontend/src/Settings/Profiles/Release/ReleaseProfileItem.css.d.ts diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx similarity index 91% rename from frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx rename to frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx index d4cc963c2..58a7bd13f 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx @@ -1,9 +1,9 @@ import React, { useCallback } from 'react'; -import MiddleTruncate from 'react-middle-truncate'; import { useDispatch } from 'react-redux'; import { Tag } from 'App/State/TagsAppState'; import Card from 'Components/Card'; import Label from 'Components/Label'; +import MiddleTruncate from 'Components/MiddleTruncate'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; @@ -13,14 +13,14 @@ import Indexer from 'typings/Indexer'; import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; -import styles from './ReleaseProfileRow.css'; +import styles from './ReleaseProfileItem.css'; interface ReleaseProfileProps extends ReleaseProfile { tagList: Tag[]; indexerList: Indexer[]; } -function ReleaseProfileRow(props: ReleaseProfileProps) { +function ReleaseProfileItem(props: ReleaseProfileProps) { const { id, name, @@ -70,7 +70,7 @@ function ReleaseProfileRow(props: ReleaseProfileProps) { return ( <Label key={item} className={styles.label} kind={kinds.SUCCESS}> - <MiddleTruncate text={item} start={10} end={10} /> + <MiddleTruncate text={item} /> </Label> ); })} @@ -84,7 +84,7 @@ function ReleaseProfileRow(props: ReleaseProfileProps) { return ( <Label key={item} className={styles.label} kind={kinds.DANGER}> - <MiddleTruncate text={item} start={10} end={10} /> + <MiddleTruncate text={item} /> </Label> ); })} @@ -128,4 +128,4 @@ function ReleaseProfileRow(props: ReleaseProfileProps) { ); } -export default ReleaseProfileRow; +export default ReleaseProfileItem; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css index 43f17b9dc..c8ec63e78 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css @@ -4,7 +4,7 @@ } .addReleaseProfile { - composes: releaseProfile from '~./ReleaseProfileRow.css'; + composes: releaseProfile from '~./ReleaseProfileItem.css'; background-color: var(--cardAlternateBackgroundColor); color: var(--gray); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx index 98300b1af..c0a34a46c 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -14,7 +14,7 @@ import createClientSideCollectionSelector from 'Store/Selectors/createClientSide import createTagsSelector from 'Store/Selectors/createTagsSelector'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; -import ReleaseProfileRow from './ReleaseProfileRow'; +import ReleaseProfileItem from './ReleaseProfileItem'; import styles from './ReleaseProfiles.css'; function ReleaseProfiles() { @@ -59,7 +59,7 @@ function ReleaseProfiles() { {items.map((item) => { return ( - <ReleaseProfileRow + <ReleaseProfileItem key={item.id} tagList={tagList} indexerList={indexerList} diff --git a/frontend/typings/MiddleTruncate.d.ts b/frontend/typings/MiddleTruncate.d.ts deleted file mode 100644 index 598c28156..000000000 --- a/frontend/typings/MiddleTruncate.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -declare module 'react-middle-truncate' { - import { ComponentPropsWithoutRef } from 'react'; - - interface MiddleTruncateProps extends ComponentPropsWithoutRef<'div'> { - text: string; - ellipsis?: string; - start?: number | RegExp | string; - end?: number | RegExp | string; - smartCopy?: 'all' | 'partial'; - onResizeDebounceMs?: number; - } - - export default function MiddleTruncate( - props: MiddleTruncateProps - ): JSX.Element; -} diff --git a/package.json b/package.json index fe5079530..e619cfeca 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "react-google-recaptcha": "2.1.0", "react-lazyload": "3.2.0", "react-measure": "1.4.7", - "react-middle-truncate": "1.0.3", "react-popper": "1.3.7", "react-redux": "7.2.4", "react-router": "5.2.0", diff --git a/yarn.lock b/yarn.lock index d36aba5d1..4e1c21419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,7 +2271,7 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -classnames@2.5.1, classnames@^2.2.6: +classnames@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -4120,11 +4120,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isnumeric@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" - integrity sha512-uSJoAwnN1eCKDFKi8hL3UCYJSkQv+NwhKzhevUPIn/QZ8ILO21f+wQnlZHU0eh1rsLO1gI4w/HQdeOSTKwlqMg== - isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" @@ -4464,7 +4459,7 @@ lodash.upperfirst@4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4787,7 +4782,7 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== -normalize.css@8.0.1, normalize.css@^8.0.0: +normalize.css@8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== @@ -5491,16 +5486,6 @@ react-measure@1.4.7: prop-types "^15.5.4" resize-observer-polyfill "^1.4.1" -react-middle-truncate@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/react-middle-truncate/-/react-middle-truncate-1.0.3.tgz#42d198ad9738bc2d8f7b8e77e11e02107b856fe1" - integrity sha512-rBYJjSYgAvNayDk+yZz8QhQqbGLjsSZV2CuGJ4g18o6BUGlMgZ4fIOGKuIEIZj17zCXzSw7mCGAcZ4lw0y8Lgw== - dependencies: - classnames "^2.2.6" - lodash "^4.17.15" - normalize.css "^8.0.0" - units-css "^0.4.0" - react-popper@1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" @@ -6771,14 +6756,6 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -units-css@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" - integrity sha512-WijzYC+chwzg2D6HmNGUSzPAgFRJfuxVyG9oiY28Ei5E+g6fHoPkhXUr5GV+5hE/RTHZNd9SuX2KLioYHdttoA== - dependencies: - isnumeric "^0.2.0" - viewport-dimensions "^0.2.0" - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -6864,11 +6841,6 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -viewport-dimensions@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" - integrity sha512-94JqlKxEP4m7WO+N3rm4tFRGXZmXXwSPQCoV+EPxDnn8YAGiLU3T+Ha1imLreAjXsHl0K+ELnIqv64i1XZHLFQ== - warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" From 40f4ef27b22113c1dae0d0cbdee8205132bed68a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 24 Nov 2024 20:51:33 -0800 Subject: [PATCH 665/762] Support Postgres with non-standard version string --- .../Datastore/DatabaseVersionParserFixture.cs | 38 +++++++++++++++++++ src/NzbDrone.Core/Datastore/Database.cs | 4 +- .../Datastore/DatabaseVersionParser.cs | 16 ++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs new file mode 100644 index 000000000..05bf04fea --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Test.Datastore; + +[TestFixture] +public class DatabaseVersionParserFixture +{ + [TestCase("3.44.2", 3, 44, 2)] + public void should_parse_sqlite_database_version(string serverVersion, int majorVersion, int minorVersion, int buildVersion) + { + var version = DatabaseVersionParser.ParseServerVersion(serverVersion); + + version.Should().NotBeNull(); + version.Major.Should().Be(majorVersion); + version.Minor.Should().Be(minorVersion); + version.Build.Should().Be(buildVersion); + } + + [TestCase("14.8 (Debian 14.8-1.pgdg110+1)", 14, 8, null)] + [TestCase("16.3 (Debian 16.3-1.pgdg110+1)", 16, 3, null)] + [TestCase("16.3 - Percona Distribution", 16, 3, null)] + [TestCase("17.0 - Percona Server", 17, 0, null)] + public void should_parse_postgres_database_version(string serverVersion, int majorVersion, int minorVersion, int? buildVersion) + { + var version = DatabaseVersionParser.ParseServerVersion(serverVersion); + + version.Should().NotBeNull(); + version.Major.Should().Be(majorVersion); + version.Minor.Should().Be(minorVersion); + + if (buildVersion.HasValue) + { + version.Build.Should().Be(buildVersion.Value); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index 887039bcb..741a22f0b 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -2,7 +2,6 @@ using System; using System.Data; using System.Data.Common; using System.Data.SQLite; -using System.Text.RegularExpressions; using Dapper; using NLog; using NzbDrone.Common.Instrumentation; @@ -52,9 +51,8 @@ namespace NzbDrone.Core.Datastore { using var db = _datamapperFactory(); var dbConnection = db as DbConnection; - var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", ""); - return new Version(version); + return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion); } } diff --git a/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs b/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs new file mode 100644 index 000000000..ffc77cf18 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs @@ -0,0 +1,16 @@ +using System; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Datastore; + +public static class DatabaseVersionParser +{ + private static readonly Regex VersionRegex = new (@"^[^ ]+", RegexOptions.Compiled); + + public static Version ParseServerVersion(string serverVersion) + { + var match = VersionRegex.Match(serverVersion); + + return match.Success ? new Version(match.Value) : null; + } +} From f9606518eef78117f1e06a8bcc34af57ab0d2454 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 27 Nov 2024 07:40:51 -0800 Subject: [PATCH 666/762] Fixed: Error loading queue Closes #7422 --- .../ApiTests/QueueFixture.cs | 73 +++++++++++++++++++ .../Client/QueueClient.cs | 13 ++++ .../IntegrationTestBase.cs | 2 + src/Sonarr.Api.V3/Queue/QueueResource.cs | 23 ++++-- 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 src/NzbDrone.Integration.Test/ApiTests/QueueFixture.cs create mode 100644 src/NzbDrone.Integration.Test/Client/QueueClient.cs diff --git a/src/NzbDrone.Integration.Test/ApiTests/QueueFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/QueueFixture.cs new file mode 100644 index 000000000..8dfa62872 --- /dev/null +++ b/src/NzbDrone.Integration.Test/ApiTests/QueueFixture.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Linq; +using System.Threading; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Integration.Test.Client; +using Sonarr.Api.V3.Queue; +using Sonarr.Http; + +namespace NzbDrone.Integration.Test.ApiTests +{ + [TestFixture] + public class QueueFixture : IntegrationTest + { + private PagingResource<QueueResource> GetFirstPage() + { + var request = Queue.BuildRequest(); + request.AddParameter("includeUnknownSeriesItems", true); + + return Queue.Get<PagingResource<QueueResource>>(request); + } + + private void RefreshQueue() + { + var command = Commands.Post(new SimpleCommandResource { Name = "RefreshMonitoredDownloads" }); + + for (var i = 0; i < 30; i++) + { + var updatedCommand = Commands.Get(command.Id); + + if (updatedCommand.Status == CommandStatus.Completed) + { + return; + } + + Thread.Sleep(1000); + i++; + } + } + + [Test] + [Order(0)] + public void ensure_queue_is_empty_when_download_client_is_configured() + { + EnsureNoDownloadClient(); + EnsureDownloadClient(); + + var queue = GetFirstPage(); + + queue.TotalRecords.Should().Be(0); + queue.Records.Should().BeEmpty(); + } + + [Test] + [Order(1)] + public void ensure_queue_is_not_empty() + { + EnsureNoDownloadClient(); + + var client = EnsureDownloadClient(); + var directory = client.Fields.First(v => v.Name == "watchFolder").Value as string; + + File.WriteAllText(Path.Combine(directory, "Series.Title.S01E01.mkv"), "Test Download"); + RefreshQueue(); + + var queue = GetFirstPage(); + + queue.TotalRecords.Should().Be(1); + queue.Records.Should().NotBeEmpty(); + } + } +} diff --git a/src/NzbDrone.Integration.Test/Client/QueueClient.cs b/src/NzbDrone.Integration.Test/Client/QueueClient.cs new file mode 100644 index 000000000..f35869b52 --- /dev/null +++ b/src/NzbDrone.Integration.Test/Client/QueueClient.cs @@ -0,0 +1,13 @@ +using RestSharp; +using Sonarr.Api.V3.Queue; + +namespace NzbDrone.Integration.Test.Client +{ + public class QueueClient : ClientBase<QueueResource> + { + public QueueClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) + { + } + } +} diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 3e8a8a2b0..c9786bdd3 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -57,6 +57,7 @@ namespace NzbDrone.Integration.Test public ClientBase<TagResource> Tags; public ClientBase<EpisodeResource> WantedMissing; public ClientBase<EpisodeResource> WantedCutoffUnmet; + public QueueClient Queue; private List<SignalRMessage> _signalRReceived; @@ -121,6 +122,7 @@ namespace NzbDrone.Integration.Test Tags = new ClientBase<TagResource>(RestClient, ApiKey); WantedMissing = new ClientBase<EpisodeResource>(RestClient, ApiKey, "wanted/missing"); WantedCutoffUnmet = new ClientBase<EpisodeResource>(RestClient, ApiKey, "wanted/cutoff"); + Queue = new QueueClient(RestClient, ApiKey); } [OneTimeTearDown] diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index 06e614aeb..f209a3ccc 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -26,8 +26,11 @@ namespace Sonarr.Api.V3.Queue public int CustomFormatScore { get; set; } public decimal Size { get; set; } public string Title { get; set; } - public decimal SizeLeft { get; set; } - public TimeSpan? TimeLeft { get; set; } + + // Collides with existing properties due to case-insensitive deserialization + // public decimal SizeLeft { get; set; } + // public TimeSpan? TimeLeft { get; set; } + public DateTime? EstimatedCompletionTime { get; set; } public DateTime? Added { get; set; } public QueueStatus Status { get; set; } @@ -43,9 +46,10 @@ namespace Sonarr.Api.V3.Queue public string OutputPath { get; set; } public bool EpisodeHasFile { get; set; } - [Obsolete] + [Obsolete("Will be replaced by SizeLeft")] public decimal Sizeleft { get; set; } - [Obsolete] + + [Obsolete("Will be replaced by TimeLeft")] public TimeSpan? Timeleft { get; set; } } @@ -75,8 +79,11 @@ namespace Sonarr.Api.V3.Queue CustomFormatScore = customFormatScore, Size = model.Size, Title = model.Title, - SizeLeft = model.SizeLeft, - TimeLeft = model.TimeLeft, + + // Collides with existing properties due to case-insensitive deserialization + // SizeLeft = model.SizeLeft, + // TimeLeft = model.TimeLeft, + EstimatedCompletionTime = model.EstimatedCompletionTime, Added = model.Added, Status = model.Status, @@ -92,10 +99,10 @@ namespace Sonarr.Api.V3.Queue OutputPath = model.OutputPath, EpisodeHasFile = model.Episode?.HasFile ?? false, - #pragma warning disable CS0612 + #pragma warning disable CS0618 Sizeleft = model.SizeLeft, Timeleft = model.TimeLeft, - #pragma warning restore CS0612 + #pragma warning restore CS0618 }; } From 62bcf397ddc158b079e9345ff197c8c770a79ba4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 27 Nov 2024 17:00:09 -0800 Subject: [PATCH 667/762] Fixed: Adding/Editing not replacing Implementation Name --- frontend/src/Store/Selectors/selectSettings.ts | 2 +- frontend/src/typings/pending.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts index b7b6ab8c7..2fb229e75 100644 --- a/frontend/src/Store/Selectors/selectSettings.ts +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -93,7 +93,7 @@ function selectSettings<T extends ModelBaseSetting>( // Return a flattened value if (key === 'implementationName') { - // acc.implementationName = item[key]; + acc.implementationName = item[key]; return acc; } diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index 13c2123cc..af0dd95e1 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -58,5 +58,6 @@ type Mapped<T> = { }; export type PendingSection<T> = Mapped<T> & { + implementationName?: string; fields?: PendingField<T>[]; }; From bd656ae7f66fc9224ef2a57857152ee5d54d54f8 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:55:50 +0200 Subject: [PATCH 668/762] Fixed: Avoid default category on existing Transmission configurations Co-authored-by: Mark McDowall <mark@mcdowall.ca> --- .../Clients/Transmission/TransmissionSettings.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 17fc43bd3..a7bfbcab4 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; @@ -27,6 +28,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission { private static readonly TransmissionSettingsValidator Validator = new (); + // This constructor is used when creating a new instance, such as the user adding a new Transmission client. public TransmissionSettings() { Host = "localhost"; @@ -35,6 +37,18 @@ namespace NzbDrone.Core.Download.Clients.Transmission TvCategory = "tv-sonarr"; } + // TODO: Remove this in v5 + // This constructor is used when deserializing from JSON, it will set the + // category to the deserialized value, defaulting to null. + [JsonConstructor] + public TransmissionSettings(string tvCategory = null) + { + Host = "localhost"; + Port = 9091; + UrlBase = "/transmission/"; + TvCategory = tvCategory; + } + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] public string Host { get; set; } From 65d07fa99e670953b80ddaf3e12b06d109b8bab7 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Thu, 28 Nov 2024 01:31:54 +0000 Subject: [PATCH 669/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 43 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e66ef48f3..95af4db2a 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -6970,6 +6970,29 @@ } } }, + "/api/v3/series/{id}/folder": { + "get": { + "tags": [ + "SeriesFolder" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v3/series/import": { "post": { "tags": [ @@ -10848,15 +10871,6 @@ "type": "string", "nullable": true }, - "sizeleft": { - "type": "number", - "format": "double" - }, - "timeleft": { - "type": "string", - "format": "date-span", - "nullable": true - }, "estimatedCompletionTime": { "type": "string", "format": "date-time", @@ -10911,6 +10925,17 @@ }, "episodeHasFile": { "type": "boolean" + }, + "sizeleft": { + "type": "number", + "format": "double", + "deprecated": true + }, + "timeleft": { + "type": "string", + "format": "date-span", + "nullable": true, + "deprecated": true } }, "additionalProperties": false From 00c16cd06b0a2da0cca0916961b08adc163752bf Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 1 Dec 2024 02:45:23 +0000 Subject: [PATCH 670/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Albrt9527 <2563009889@qq.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: mryx007 <mryx@mail.de> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 224 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/es.json | 4 +- .../Localization/Core/pt_BR.json | 7 +- .../Localization/Core/zh_CN.json | 10 +- .../Localization/Core/zh_HANS.json | 1 + .../Localization/Core/zh_Hans.json | 1 + 6 files changed, 240 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Core/Localization/Core/zh_HANS.json create mode 100644 src/NzbDrone.Core/Localization/Core/zh_Hans.json diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 89f5179ab..8a516afc3 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -141,8 +141,8 @@ "AutoRedownloadFailedFromInteractiveSearch": "Erneuter Download aus Interaktiver Suche fehlgeschlagen", "AudioLanguages": "Audio Sprachen", "AuthenticationMethodHelpTextWarning": "Bitte wähle eine gültige Authentifizierungsmethode aus", - "AuthenticationRequiredPasswordHelpTextWarning": "Gib ein neues Passwort ein", - "AuthenticationRequiredUsernameHelpTextWarning": "Gib einen neuen Benutzernamen ein", + "AuthenticationRequiredPasswordHelpTextWarning": "Neues Passwort eingeben", + "AuthenticationRequiredUsernameHelpTextWarning": "Neuen Benutzernamen eingeben", "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 aus Datien erkennen. Dazu ist es erforderlich, dass {appName} Teile der Datei liest, was zu hoher Festplatten- oder Netzwerkaktivität während der Scans 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.", @@ -873,5 +873,223 @@ "DownloadClientValidationCategoryMissingDetail": "Die von Ihnen eingegebene Kategorie existiert nicht in {clientName}. Erstellen Sie sie zuerst in {clientName}.", "DownloadClientValidationGroupMissingDetail": "Die von Ihnen eingegebene Gruppe existiert nicht in {clientName}. Erstellen Sie sie zuerst in {clientName}.", "IgnoreDownloads": "Downloads ignorieren", - "IgnoreDownloadsHint": "Hindert {appName}, diese Downloads weiter zu verarbeiten" + "IgnoreDownloadsHint": "Hindert {appName}, diese Downloads weiter zu verarbeiten", + "Genres": "Genres", + "Grab": "Holen", + "FileBrowser": "Dateibrowser", + "FormatAgeMinutes": "Minuten", + "HistorySeason": "Verlauf für diese Staffel anzeigen", + "Enabled": "Aktiviert", + "IconForSpecialsHelpText": "Symbol für besondere Folgen anzeigen (Staffel 0)", + "Ignored": "Ignoriert", + "Import": "Import", + "Global": "Weltweit", + "IconForCutoffUnmetHelpText": "Symbol für Dateien anzeigen, wenn die Grenze nicht erreicht wurde", + "Ended": "Beendet", + "EpisodeAirDate": "Erscheinungsdatum der Folge", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} konnte nicht feststellen, für welche Serie und Episode dieses Release bestimmt ist. {appName} kann dieses Release möglicherweise nicht automatisch importieren. Möchtest du '{title}' abrufen?", + "Group": "Gruppe", + "EditImportListExclusion": "Importliste-Ausnahmen bearbeiten", + "EnableInteractiveSearchHelpText": "Wird verwendet, wenn die interaktive Suche verwendet wird", + "HttpHttps": "HTTP(S)", + "Downloading": "wird runtergeladen", + "Edit": "Bearbeiten", + "ICalFeed": "iCal Feed", + "ICalIncludeUnmonitoredEpisodesHelpText": "Nicht überwachte Folgen in den iCal-Feed aufnehmen", + "ICalShowAsAllDayEventsHelpText": "Ereignisse werden als ganztägige Ereignisse in deinem Kalender angezeigt", + "FailedToLoadCustomFiltersFromApi": "Benutzerdefinierte Filter konnten nicht von der API geladen werden", + "HasUnmonitoredSeason": "Hat nicht überwachte Staffel", + "DownloadFailedEpisodeTooltip": "Download der Episode fehlgeschlagen", + "CustomFormatsSpecificationExceptLanguage": "Ausgenommen Sprache", + "EnableProfile": "Profil aktivieren", + "FailedToLoadUiSettingsFromApi": "UI-Einstellungen konnten nicht von der API geladen werden", + "File": "Datei", + "FileBrowserPlaceholderText": "Beginne mit der Eingabe oder wähle unten einen Pfad aus", + "FileManagement": "Dateiverwaltung", + "Filter": "Filter", + "FilterIs": "ist", + "FilterIsAfter": "ist nach", + "FilterIsBefore": "ist vor", + "FullSeason": "Komplette Staffel", + "General": "Allgemein", + "Completed": "Fertiggestellt", + "CutoffNotMet": "Grenzwert nicht erreicht", + "DownloadClientUnavailable": "Download Client nicht verfügbar", + "DownloadClientsLoadError": "Download Clients können nicht geladen werden", + "Enable": "Aktivieren", + "EnableAutomaticSearchHelpTextWarning": "Wird verwendet, wenn die interaktive Suche verwendet wird", + "EnableInteractiveSearchHelpTextWarning": "Die Suche wird von diesem Indexer nicht unterstützt", + "Episode": "Folge", + "EpisodeDownloaded": "Folge heruntergeladen", + "EpisodeMissingFromDisk": "Folge fehlt auf der Festplatte", + "EpisodeTitle": "Folgentitel", + "ErrorLoadingContent": "Es ist ein Fehler beim Laden dieses Inhalts aufgetreten", + "ErrorLoadingContents": "Fehler beim Laden von Inhalten", + "ExistingTag": "Vorhandener Tag", + "FreeSpace": "Freier Platz", + "HasMissingSeason": "Hat eine fehlende Staffel", + "Host": "Host", + "ICalTagsSeriesHelpText": "Der Feed enthält nur Serien mit mindestens einem passenden Tag", + "IgnoredAddresses": "Ignorierte Adressen", + "Images": "Bilder", + "ImdbId": "IMDb ID", + "ImportErrors": "Import Fehler", + "Downloaded": "Heruntergeladen", + "EpisodeFileMissingTooltip": "Folge fehlt", + "FormatAgeHour": "Stunde", + "CustomColonReplacementFormatHelpText": "Zeichen, die als Ersatz für Doppelpunkte zu verwenden sind", + "EnableRss": "RSS aktivieren", + "Folders": "Ordner", + "HourShorthand": "h", + "DownloadClientValidationVerifySsl": "Überprüfe die SSL-Einstellungen", + "DownloadClientValidationVerifySslDetail": "Bitte überprüfe deine SSL-Konfiguration sowohl auf {clientName} als auch auf {appName}", + "DownloadIgnored": "Download Ignoriert", + "DownloadPropersAndRepacks": "Props und Repacks", + "EditAutoTag": "Automatische Tags bearbeiten", + "EditConnectionImplementation": "Verbindung bearbeiten - {implementationName}", + "EditGroups": "Gruppen Bearbeiten", + "EditQualityProfile": "Qualitätsprofil bearbeiten", + "EnableSsl": "SSL aktivieren", + "EpisodeCount": "Anzahl der Folgen", + "AutoTaggingSpecificationMaximumYear": "Höchstjahr", + "AutoTaggingSpecificationMinimumYear": "Höchstjahr", + "AutoTaggingSpecificationSeriesType": "Serientyp", + "EpisodeFileDeleted": "Folge gelöscht", + "EpisodeImported": "Folge wurde importiert", + "Episodes": "Folgen", + "ExpandAll": "Alle erweitern", + "FilterEqual": "gleich", + "FinaleTooltip": "Serien oder Staffelfinale", + "FirstDayOfWeek": "Erster Tag der Woche", + "Forecast": "Vorhersage", + "FormatAgeDay": "Tag", + "FormatAgeDays": "Tage", + "Grabbed": "Geholt", + "Existing": "Vorhanden", + "ExistingSeries": "Vorhandene Serien", + "Failed": "Fehlgeschlagen", + "FailedToFetchUpdates": "Updates konnten nicht abgerufen werden", + "FailedToLoadSeriesFromApi": "Serie konnte nicht von API geladen werden", + "FailedToLoadSystemStatusFromApi": "Der Systemstatus konnte nicht von der API geladen werden", + "FileNameTokens": "Dateinamen Token", + "FilterDoesNotStartWith": "beginnt nicht mit", + "Files": "Dateien", + "FilterIsNot": "ist nicht", + "FilterLessThanOrEqual": "kleiner als oder gleich", + "FilterNotEqual": "nicht gleich", + "GeneralSettingsLoadError": "Allgemeine Einstellungen können nicht geladen werden", + "GeneralSettingsSummary": "Port, SSL, Benutzername/Kennwort, Proxy, Analyse und Updates", + "GrabId": "ID holen", + "HistoryModalHeaderSeason": "Verlauf {season}", + "HomePage": "Hauptseite", + "Hostname": "Hostname", + "IconForSpecials": "Icon für Specials", + "ImportExistingSeries": "Bereits existierende Serien importieren", + "ImportExtraFiles": "Zusätzliche Dateien importieren", + "ICalLink": "iCal Link", + "From": "Von", + "DownloadClientSabnzbdValidationEnableJobFolders": "Auftragsordner aktivieren", + "DownloadIgnoredEpisodeTooltip": "Episode Download Ignoriert", + "EditDelayProfile": "Delay Profil bearbeiten", + "EnableAutomaticAdd": "Automatisches Hinzufügen aktivieren", + "EpisodeFileDeletedTooltip": "Folge gelöscht", + "EpisodeFileRenamed": "Folge umbenannt", + "ErrorRestoringBackup": "Fehler beim Wiederherstellen der Sicherung", + "Extend": "Erweitern", + "External": "Extern", + "FileNames": "Dateinamen", + "FilterDoesNotContain": "enthält nicht", + "FilterEndsWith": "endet mit", + "FilterLessThan": "weniger als", + "FormatAgeMinute": "Minute", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "GrabRelease": "Release holen", + "Health": "Gesundheit", + "DownloadClientValidationUnableToConnect": "Verbindung zu {clientName} kann nicht hergestellt werden", + "EnableCompletedDownloadHandlingHelpText": "Automatischer Import abgeschlossener Downloads vom Download Client", + "EnableMetadataHelpText": "Aktiviere die Erstellung von Metadaten-Dateien für diesen Metadaten-Typ", + "FilterContains": "enthält", + "FilterGreaterThan": "größer als", + "GrabSelected": "Auswahl abrufen", + "HardlinkCopyFiles": "Hardlink/Dateien kopieren", + "ICalShowAsAllDayEvents": "Als ganztägige Ereignisse anzeigen", + "IRC": "IRC", + "ImportCustomFormat": "Import Custom Format", + "DownloadClientQbittorrentSettingsContentLayout": "Inhaltslayout", + "DownloadClientValidationUnableToConnectDetail": "Bitte überprüfe den Hostnamen und den Port.", + "Duplicate": "Duplizieren", + "EpisodesLoadError": "Folgen können nicht geladen werden", + "Events": "Ereignisse", + "False": "Falsch", + "FeatureRequests": "Feature Anfragen", + "HiddenClickToShow": "Versteckt, zum Anzeigen anklicken", + "DownloadFailed": "Download gescheitert", + "EditSelectedSeries": "Ausgewählte Serien bearbeiten", + "EpisodeInfo": "Folgeninfo", + "EpisodeIsNotMonitored": "Folge wird nicht überwacht", + "EpisodeTitleRequired": "Folgentitel erforderlich", + "FilterEpisodesPlaceholder": "Folgen nach Titel oder Nummer filtern", + "FilterInLast": "im letzten", + "Folder": "Ordner", + "HideEpisodes": "Folgen ausblenden", + "EditRemotePathMapping": "Remote Pfadzuordnung bearbeiten", + "EditRestriction": "Beschränkung bearbeiten", + "FailedToLoadTagsFromApi": "Tags konnten nicht von der API geladen werden", + "FilterDoesNotEndWith": "endet nicht mit", + "HideAdvanced": "Erweiterte Einstellungen ausblenden", + "History": "Verlauf", + "HistoryLoadError": "Verlauf konnte nicht geladen werden", + "DownloadClientValidationTestNzbs": "Die Liste der NZBs konnte nicht abgerufen werden: {exceptionMessage}", + "DownloadClientValidationTestTorrents": "Die Liste der Torrents konnte nicht abgerufen werden: {exceptionMessage}", + "DownloadStationStatusExtracting": "Extrahieren: {progress}%", + "Duration": "Dauer", + "FailedToFetchSettings": "Einstellungen können nicht abgerufen werden", + "EditDownloadClientImplementation": "Download Client bearbeiten - {implementationName}", + "Error": "Fehler", + "Example": "Beispiel", + "FailedToLoadQualityProfilesFromApi": "Qualitätsprofile konnten nicht von der API geladen werden", + "FailedToLoadTranslationsFromApi": "Übersetzungen konnten nicht von der API geladen werden", + "Filename": "Dateiname", + "FilterGreaterThanOrEqual": "größer als oder gleich", + "FilterInNext": "im nächsten", + "FilterNotInNext": "nicht im nächsten", + "FilterSeriesPlaceholder": "Serien filtern", + "Fixed": "Behoben", + "FormatAgeHours": "Stunden", + "Formats": "Formate", + "Exception": "Ausnahme", + "EpisodeFileRenamedTooltip": "Folge umbenannt", + "EditConditionImplementation": "Bedingung bearbeiten - {implementationName}", + "EditReleaseProfile": "Release Profil bearbeiten", + "FavoriteFolderAdd": "Favoritenordner hinzufügen", + "FavoriteFolderRemove": "Favoritenordner entfernen", + "FavoriteFolders": "Favoritenordner", + "FilterNotInLast": "nicht im letzten", + "FilterStartsWith": "beginnt mit", + "Filters": "Filter", + "IconForFinales": "Symbol für finale Episoden", + "Delay": "Verzögerung", + "DownloadWarning": "Download Warnung: {warningMessage}", + "EditSeriesModalHeader": "Bearbeiten - {title}", + "FailedToLoadSonarr": "Fehler beim Laden von {appName}", + "Fallback": "Fallback", + "FormatDateTime": "{formattedDate} {formattedTime}", + "Forums": "Forums", + "DeleteSelected": "Ausgewählte löschen", + "DownloadClientValidationUnknownException": "Unbekannte Ausnahme: {exception}", + "DownloadClients": "Download Clients", + "DownloadPropersAndRepacksHelpText": "Ob automatisch auf Proper/Repacks aktualisiert werden soll oder nicht", + "DownloadPropersAndRepacksHelpTextWarning": "Verwende benutzerdefinierte Formate für automatische Upgrades auf Propers/Repacks", + "EditCustomFormat": "Benutzerdefiniertes Format bearbeiten", + "EditSelectedCustomFormats": "Ausgewählte benutzerdefinierte Formate bearbeiten", + "EnableAutomaticSearchHelpText": "Wird verwendet, wenn die automatische Suche über die Benutzeroberfläche oder durch {appName} durchgeführt wird.", + "EnableSslHelpText": "Erfordert einen Neustart als Administrator, um wirksam zu werden", + "EndedOnly": "Nur beendete", + "EndedSeriesDescription": "Es werden keine weiteren Episoden oder Staffeln erwartet", + "EpisodeIsDownloading": "Folge wird heruntergeladen", + "ErrorLoadingPage": "Es ist ein Fehler beim Laden dieser Seite aufgetreten", + "ExtraFileExtensionsHelpTextsExamples": "Beispiele: '.sub, .nfo' oder 'sub,nfo'", + "FolderNameTokens": "Ordnernamen Token", + "GeneralSettings": "Allgemeine Einstellungen", + "ICalFeedHelpText": "Kopiere diese URL in deine(n) Client(s) oder klicke zum Abonnieren, wenn dein Browser webcal unterstützt" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 60e0b6f25..d1135cb29 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2135,5 +2135,7 @@ "Completed": "Completado", "CutoffNotMet": "Límite no alcanzado", "Menu": "Menú", - "Premiere": "Estreno" + "Premiere": "Estreno", + "UpdateSeriesPath": "Actualizar Ruta de Series", + "UpdatePath": "Actualizar Ruta" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index efd8cf626..7221f82d3 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -248,7 +248,7 @@ "LastWriteTime": "Hora da última gravação", "Location": "Localização", "LogFilesLocation": "Os arquivos de log estão localizados em: {location}", - "Logs": "Logs", + "Logs": "Registros", "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commit do Github para saber mais", "Manual": "Manual", "Message": "Mensagem", @@ -2135,5 +2135,8 @@ "CutoffNotMet": "Corte Não Alcançado", "Premiere": "Estreia", "Completed": "Completado", - "Menu": "Menu" + "Menu": "Menu", + "NotificationsSettingsWebhookHeaders": "Cabeçalhos", + "UpdatePath": "Caminho da Atualização", + "UpdateSeriesPath": "Atualizar Caminho da Série" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 450776524..52777ae4f 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1955,5 +1955,13 @@ "InstallMajorVersionUpdate": "安装更新", "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?", "Fallback": "备选", - "FailedToFetchSettings": "设置同步失败" + "FailedToFetchSettings": "设置同步失败", + "Warning": "警告", + "OnFileUpgrade": "文件升级时", + "OnFileImport": "关于文件导入", + "ManageFormats": "管理格式", + "FavoriteFolderAdd": "添加收藏文件夹", + "FavoriteFolderRemove": "删除收藏夹", + "FavoriteFolders": "收藏夹", + "NotificationsSettingsWebhookHeaders": "标头" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_HANS.json b/src/NzbDrone.Core/Localization/Core/zh_HANS.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/src/NzbDrone.Core/Localization/Core/zh_HANS.json @@ -0,0 +1 @@ +{} diff --git a/src/NzbDrone.Core/Localization/Core/zh_Hans.json b/src/NzbDrone.Core/Localization/Core/zh_Hans.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/src/NzbDrone.Core/Localization/Core/zh_Hans.json @@ -0,0 +1 @@ +{} From efd48710e43583554261fd8cb09caf95efdb95c7 Mon Sep 17 00:00:00 2001 From: Robin Dadswell <robin@robindadswell.tech> Date: Sun, 1 Dec 2024 15:04:18 +0000 Subject: [PATCH 671/762] Deleted translation using Weblate (zh_HANS (generated) (zh_HANS)) --- src/NzbDrone.Core/Localization/Core/zh_HANS.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/NzbDrone.Core/Localization/Core/zh_HANS.json diff --git a/src/NzbDrone.Core/Localization/Core/zh_HANS.json b/src/NzbDrone.Core/Localization/Core/zh_HANS.json deleted file mode 100644 index 0967ef424..000000000 --- a/src/NzbDrone.Core/Localization/Core/zh_HANS.json +++ /dev/null @@ -1 +0,0 @@ -{} From 160151c6e000c6620ba2e04ca6245317c5c9ba16 Mon Sep 17 00:00:00 2001 From: hhjuhl <84127693+hhjuhl@users.noreply.github.com> Date: Mon, 2 Dec 2024 01:15:33 +0100 Subject: [PATCH 672/762] Use 'text-wrap: balance' for text wrapping on overview and details Co-authored-by: Mark McDowall <mark@mcdowall.ca> --- frontend/src/Series/Details/SeriesDetails.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index cdda349f0..21ff2722d 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -59,6 +59,7 @@ } .title { + text-wrap: balance; font-weight: 300; font-size: 50px; line-height: 50px; @@ -143,6 +144,7 @@ flex: 1 0 0; margin-top: 8px; min-height: 0; + text-wrap: balance; font-size: $intermediateFontSize; } From 8c67a3bdee65aa35bdd82ac8614be875236d88de Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 11 Nov 2024 10:03:18 -0800 Subject: [PATCH 673/762] Add reason enum to decision engine rejections --- .../DownloadDecisionMakerFixture.cs | 50 ++++++------- .../ImportFixture.cs | 9 +-- .../DownloadApprovedFixture.cs | 12 +-- .../PendingReleaseServiceTests/AddFixture.cs | 2 +- .../RemoveGrabbedFixture.cs | 2 +- .../RemoveRejectedFixture.cs | 2 +- .../ImportApprovedEpisodesFixture.cs | 7 +- .../ImportDecisionMakerFixture.cs | 13 ++-- src/NzbDrone.Core/DecisionEngine/Decision.cs | 32 -------- .../DecisionEngine/DownloadDecision.cs | 4 +- .../DecisionEngine/DownloadDecisionMaker.cs | 27 +++---- .../DecisionEngine/DownloadRejection.cs | 9 +++ .../DecisionEngine/DownloadRejectionReason.cs | 75 +++++++++++++++++++ .../DecisionEngine/DownloadSpecDecision.cs | 34 +++++++++ src/NzbDrone.Core/DecisionEngine/Rejection.cs | 10 ++- .../AcceptableSizeSpecification.cs | 16 ++-- .../AlreadyImportedSpecification.cs | 12 +-- .../AnimeVersionUpgradeSpecification.cs | 16 ++-- .../BlockedIndexerSpecification.cs | 8 +- .../Specifications/BlocklistSpecification.cs | 8 +- ...stomFormatAllowedByProfileSpecification.cs | 8 +- .../Specifications/FreeSpaceSpecification.cs | 14 ++-- .../Specifications/FullSeasonSpecification.cs | 8 +- ...> IDownloadDecisionEngineSpecification.cs} | 4 +- .../MaximumSizeSpecification.cs | 12 +-- .../Specifications/MinimumAgeSpecification.cs | 12 +-- .../MultiSeasonSpecification.cs | 8 +- .../Specifications/NotSampleSpecification.cs | 8 +- .../Specifications/ProtocolSpecification.cs | 10 +-- .../QualityAllowedByProfileSpecification.cs | 8 +- .../Specifications/QueueSpecification.cs | 24 +++--- .../Specifications/RawDiskSpecification.cs | 16 ++-- .../ReleaseRestrictionsSpecification.cs | 10 +-- .../Specifications/RepackSpecification.cs | 19 ++--- .../Specifications/RetentionSpecification.cs | 10 +-- .../RssSync/DelaySpecification.cs | 20 ++--- .../DeletedEpisodeFileSpecification.cs | 12 +-- .../RssSync/HistorySpecification.cs | 24 +++--- .../RssSync/IndexerTagSpecification.cs | 12 +-- .../RssSync/MonitoredEpisodeSpecification.cs | 14 ++-- .../RssSync/ProperSpecification.cs | 14 ++-- .../SameEpisodesGrabSpecification.cs | 8 +- .../SceneMappingSpecification.cs | 14 ++-- .../Search/EpisodeRequestedSpecification.cs | 14 ++-- .../Search/SeasonMatchSpecification.cs | 12 +-- .../Search/SeriesSpecification.cs | 10 +-- .../SingleEpisodeSearchMatchSpecification.cs | 24 +++--- .../SeasonPackOnlySpecification.cs | 10 +-- .../SplitEpisodeSpecification.cs | 8 +- .../TorrentSeedingSpecification.cs | 12 +-- .../UpgradeAllowedSpecification.cs | 8 +- .../UpgradeDiskSpecification.cs | 20 ++--- .../DownloadedEpisodesImportService.cs | 19 +++-- .../IImportDecisionEngineSpecification.cs | 5 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../EpisodeImport/ImportDecision.cs | 5 +- .../EpisodeImport/ImportDecisionMaker.cs | 17 ++--- .../EpisodeImport/ImportRejection.cs | 11 +++ .../EpisodeImport/ImportRejectionReason.cs | 38 ++++++++++ .../EpisodeImport/ImportSpecDecision.cs | 34 +++++++++ .../EpisodeImport/Manual/ManualImportItem.cs | 3 +- .../Manual/ManualImportService.cs | 11 ++- .../AbsoluteEpisodeNumberSpecification.cs | 11 ++- .../AlreadyImportedSpecification.cs | 10 +-- .../EpisodeTitleSpecification.cs | 17 ++--- .../Specifications/FreeSpaceSpecification.cs | 13 ++-- .../Specifications/FullSeasonSpecification.cs | 9 +-- .../HasAudioTrackSpecification.cs | 9 +-- .../MatchesFolderSpecification.cs | 17 ++--- .../MatchesGrabSpecification.cs | 13 ++-- .../Specifications/NotSampleSpecification.cs | 11 ++- .../NotUnpackingSpecification.cs | 11 ++- .../SameEpisodesImportSpecification.cs | 6 +- .../SplitEpisodeSpecification.cs | 9 +-- .../UnverifiedSceneNumberingSpecification.cs | 9 +-- .../Specifications/UpgradeSpecification.cs | 12 +-- src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 2 +- .../ManualImport/ManualImportController.cs | 2 +- .../ManualImportReprocessResource.cs | 3 +- .../ManualImport/ManualImportResource.cs | 28 ++++++- 80 files changed, 634 insertions(+), 458 deletions(-) delete mode 100644 src/NzbDrone.Core/DecisionEngine/Decision.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/DownloadRejection.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/DownloadSpecDecision.cs rename src/NzbDrone.Core/DecisionEngine/Specifications/{IDecisionEngineSpecification.cs => IDownloadDecisionEngineSpecification.cs} (59%) create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejection.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportSpecDecision.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index f195a2c99..f71a6e2d5 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -22,38 +22,38 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private List<ReleaseInfo> _reports; private RemoteEpisode _remoteEpisode; - private Mock<IDecisionEngineSpecification> _pass1; - private Mock<IDecisionEngineSpecification> _pass2; - private Mock<IDecisionEngineSpecification> _pass3; + private Mock<IDownloadDecisionEngineSpecification> _pass1; + private Mock<IDownloadDecisionEngineSpecification> _pass2; + private Mock<IDownloadDecisionEngineSpecification> _pass3; - private Mock<IDecisionEngineSpecification> _fail1; - private Mock<IDecisionEngineSpecification> _fail2; - private Mock<IDecisionEngineSpecification> _fail3; + private Mock<IDownloadDecisionEngineSpecification> _fail1; + private Mock<IDownloadDecisionEngineSpecification> _fail2; + private Mock<IDownloadDecisionEngineSpecification> _fail3; - private Mock<IDecisionEngineSpecification> _failDelayed1; + private Mock<IDownloadDecisionEngineSpecification> _failDelayed1; [SetUp] public void Setup() { - _pass1 = new Mock<IDecisionEngineSpecification>(); - _pass2 = new Mock<IDecisionEngineSpecification>(); - _pass3 = new Mock<IDecisionEngineSpecification>(); + _pass1 = new Mock<IDownloadDecisionEngineSpecification>(); + _pass2 = new Mock<IDownloadDecisionEngineSpecification>(); + _pass3 = new Mock<IDownloadDecisionEngineSpecification>(); - _fail1 = new Mock<IDecisionEngineSpecification>(); - _fail2 = new Mock<IDecisionEngineSpecification>(); - _fail3 = new Mock<IDecisionEngineSpecification>(); + _fail1 = new Mock<IDownloadDecisionEngineSpecification>(); + _fail2 = new Mock<IDownloadDecisionEngineSpecification>(); + _fail3 = new Mock<IDownloadDecisionEngineSpecification>(); - _failDelayed1 = new Mock<IDecisionEngineSpecification>(); + _failDelayed1 = new Mock<IDownloadDecisionEngineSpecification>(); - _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept); - _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept); - _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept); + _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Accept); + _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Accept); + _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Accept); - _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail1")); - _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail2")); - _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail3")); + _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail1")); + _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail2")); + _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail3")); - _failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("failDelayed1")); + _failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumAgeDelay, "failDelayed1")); _failDelayed1.SetupGet(c => c.Priority).Returns(SpecificationPriority.Disk); _reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "The.Office.S03E115.DVDRip.XviD-OSiTV" } }; @@ -68,9 +68,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Returns(_remoteEpisode); } - private void GivenSpecifications(params Mock<IDecisionEngineSpecification>[] mocks) + private void GivenSpecifications(params Mock<IDownloadDecisionEngineSpecification>[] mocks) { - Mocker.SetConstant<IEnumerable<IDecisionEngineSpecification>>(mocks.Select(c => c.Object)); + Mocker.SetConstant<IEnumerable<IDownloadDecisionEngineSpecification>>(mocks.Select(c => c.Object)); } [Test] @@ -273,7 +273,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Episodes = episodes.Where(v => v.SceneEpisodeNumber == p.EpisodeNumbers.First()).ToList() }); - Mocker.SetConstant<IEnumerable<IDecisionEngineSpecification>>(new List<IDecisionEngineSpecification> + Mocker.SetConstant<IEnumerable<IDownloadDecisionEngineSpecification>>(new List<IDownloadDecisionEngineSpecification> { Mocker.Resolve<NzbDrone.Core.DecisionEngine.Specifications.Search.EpisodeRequestedSpecification>() }); @@ -345,7 +345,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var result = Subject.GetRssDecision(_reports); result.Should().HaveCount(1); - result.First().Rejections.First().Reason.Should().Contain("12345"); + result.First().Rejections.First().Message.Should().Contain("12345"); } } } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs index 56d64eb81..0544d7246 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs @@ -4,7 +4,6 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; @@ -122,11 +121,11 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { new ImportResult( new ImportDecision( - new LocalEpisode { Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 } }, new Rejection("Rejected!")), "Test Failure"), + new LocalEpisode { Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 } }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"), new ImportResult( new ImportDecision( - new LocalEpisode { Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 } }, new Rejection("Rejected!")), "Test Failure") + new LocalEpisode { Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 } }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure") }); Subject.Import(_trackedDownload); @@ -146,11 +145,11 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { new ImportResult( new ImportDecision( - new LocalEpisode { Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 } }, new Rejection("Rejected!")), "Test Failure"), + new LocalEpisode { Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 } }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"), new ImportResult( new ImportDecision( - new LocalEpisode { Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 } }, new Rejection("Rejected!")), "Test Failure") + new LocalEpisode { Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 } }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure") }); _trackedDownload.RemoteEpisode.Episodes.Clear(); diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index b158fda2e..7ac1affbc 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -186,8 +186,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_return_an_empty_list_when_none_are_approved() { var decisions = new List<DownloadDecision>(); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); + decisions.Add(new DownloadDecision(null, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!"))); + decisions.Add(new DownloadDecision(null, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!"))); Subject.GetQualifiedReports(decisions).Should().BeEmpty(); } @@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var remoteEpisode = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p)); var decisions = new List<DownloadDecision>(); - decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); + decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary))); await Subject.ProcessDecisions(decisions); Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteEpisode>(), null), Times.Never()); @@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var decisions = new List<DownloadDecision>(); decisions.Add(new DownloadDecision(remoteEpisode)); - decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); + decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary))); await Subject.ProcessDecisions(decisions); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never()); @@ -226,8 +226,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var remoteEpisode = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p)); var decisions = new List<DownloadDecision>(); - decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); - decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); + decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary))); + decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary))); await Subject.ProcessDecisions(decisions); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index bbf1a6bf8..8adcaf3cd 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _remoteEpisode.ParsedEpisodeInfo = _parsedEpisodeInfo; _remoteEpisode.Release = _release; - _temporarilyRejected = new DownloadDecision(_remoteEpisode, new Rejection("Temp Rejected", RejectionType.Temporary)); + _temporarilyRejected = new DownloadDecision(_remoteEpisode, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary)); _heldReleases = new List<PendingRelease>(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index b1007418c..d7e806eb0 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _remoteEpisode.ParsedEpisodeInfo = _parsedEpisodeInfo; _remoteEpisode.Release = _release; - _temporarilyRejected = new DownloadDecision(_remoteEpisode, new Rejection("Temp Rejected", RejectionType.Temporary)); + _temporarilyRejected = new DownloadDecision(_remoteEpisode, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary)); _heldReleases = new List<PendingRelease>(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 07d3dcb0c..6a914208a 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _remoteEpisode.ParsedEpisodeInfo = _parsedEpisodeInfo; _remoteEpisode.Release = _release; - _temporarilyRejected = new DownloadDecision(_remoteEpisode, new Rejection("Temp Rejected", RejectionType.Temporary)); + _temporarilyRejected = new DownloadDecision(_remoteEpisode, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary)); Mocker.GetMock<IPendingReleaseRepository>() .Setup(s => s.All()) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index 0561e765a..7ae77797b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -7,7 +7,6 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; @@ -47,9 +46,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport var episodes = Builder<Episode>.CreateListOfSize(5) .Build(); - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!"))); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!"))); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!"))); _rejectedDecisions.ForEach(r => r.LocalEpisode.FileEpisodeInfo = new ParsedEpisodeInfo()); foreach (var episode in episodes) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 7fddbd1fe..51d181abe 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -4,7 +4,6 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; @@ -46,13 +45,13 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _fail2 = new Mock<IImportDecisionEngineSpecification>(); _fail3 = new Mock<IImportDecisionEngineSpecification>(); - _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); - _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); - _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept()); + _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept()); + _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept()); + _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept()); - _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail1")); - _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail2")); - _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3")); + _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail1")); + _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail2")); + _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail3")); _series = Builder<Series>.CreateNew() .With(e => e.Path = @"C:\Test\Series".AsOsAgnostic()) diff --git a/src/NzbDrone.Core/DecisionEngine/Decision.cs b/src/NzbDrone.Core/DecisionEngine/Decision.cs deleted file mode 100644 index 160e2599d..000000000 --- a/src/NzbDrone.Core/DecisionEngine/Decision.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace NzbDrone.Core.DecisionEngine -{ - public class Decision - { - public bool Accepted { get; private set; } - public string Reason { get; private set; } - - private static readonly Decision AcceptDecision = new Decision { Accepted = true }; - private Decision() - { - } - - public static Decision Accept() - { - return AcceptDecision; - } - - public static Decision Reject(string reason, params object[] args) - { - return Reject(string.Format(reason, args)); - } - - public static Decision Reject(string reason) - { - return new Decision - { - Accepted = false, - Reason = reason - }; - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs index 01aa647cf..096a8e6c7 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.DecisionEngine public class DownloadDecision { public RemoteEpisode RemoteEpisode { get; private set; } - public IEnumerable<Rejection> Rejections { get; private set; } + public IEnumerable<DownloadRejection> Rejections { get; private set; } public bool Approved => !Rejections.Any(); @@ -27,7 +27,7 @@ namespace NzbDrone.Core.DecisionEngine } } - public DownloadDecision(RemoteEpisode episode, params Rejection[] rejections) + public DownloadDecision(RemoteEpisode episode, params DownloadRejection[] rejections) { RemoteEpisode = episode; Rejections = rejections.ToList(); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index e5ee8ac03..57ef05786 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -23,14 +23,14 @@ namespace NzbDrone.Core.DecisionEngine public class DownloadDecisionMaker : IMakeDownloadDecision { - private readonly IEnumerable<IDecisionEngineSpecification> _specifications; + private readonly IEnumerable<IDownloadDecisionEngineSpecification> _specifications; private readonly IParsingService _parsingService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly IRemoteEpisodeAggregationService _aggregationService; private readonly ISceneMappingService _sceneMappingService; private readonly Logger _logger; - public DownloadDecisionMaker(IEnumerable<IDecisionEngineSpecification> specifications, + public DownloadDecisionMaker(IEnumerable<IDownloadDecisionEngineSpecification> specifications, IParsingService parsingService, ICustomFormatCalculationService formatService, IRemoteEpisodeAggregationService aggregationService, @@ -95,19 +95,20 @@ namespace NzbDrone.Core.DecisionEngine if (remoteEpisode.Series == null) { - var reason = "Unknown Series"; var matchingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle, parsedEpisodeInfo.SeasonNumber); if (matchingTvdbId.HasValue) { - reason = $"{parsedEpisodeInfo.SeriesTitle} matches an alias for series with TVDB ID: {matchingTvdbId}"; + decision = new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.MatchesAnotherSeries, $"{parsedEpisodeInfo.SeriesTitle} matches an alias for series with TVDB ID: {matchingTvdbId}")); + } + else + { + decision = new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.UnknownSeries, "Unknown Series")); } - - decision = new DownloadDecision(remoteEpisode, new Rejection(reason)); } else if (remoteEpisode.Episodes.Empty()) { - decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to identify correct episode(s) using release name and scene mappings")); + decision = new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.UnknownEpisode, "Unable to identify correct episode(s) using release name and scene mappings")); } else { @@ -141,7 +142,7 @@ namespace NzbDrone.Core.DecisionEngine Languages = parsedEpisodeInfo.Languages }; - decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to parse release")); + decision = new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.UnableToParse, "Unable to parse release")); } } } @@ -150,7 +151,7 @@ namespace NzbDrone.Core.DecisionEngine _logger.Error(e, "Couldn't process release."); var remoteEpisode = new RemoteEpisode { Release = report }; - decision = new DownloadDecision(remoteEpisode, new Rejection("Unexpected error processing release")); + decision = new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Error, "Unexpected error processing release")); } reportNumber++; @@ -193,7 +194,7 @@ namespace NzbDrone.Core.DecisionEngine private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { - var reasons = Array.Empty<Rejection>(); + var reasons = Array.Empty<DownloadRejection>(); foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key)) { @@ -210,7 +211,7 @@ namespace NzbDrone.Core.DecisionEngine return new DownloadDecision(remoteEpisode, reasons.ToArray()); } - private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) + private DownloadRejection EvaluateSpec(IDownloadDecisionEngineSpecification spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) { try { @@ -218,7 +219,7 @@ namespace NzbDrone.Core.DecisionEngine if (!result.Accepted) { - return new Rejection(result.Reason, spec.Type); + return new DownloadRejection(result.Reason, result.Message, spec.Type); } } catch (Exception e) @@ -226,7 +227,7 @@ namespace NzbDrone.Core.DecisionEngine e.Data.Add("report", remoteEpisode.Release.ToJson()); e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); _logger.Error(e, "Couldn't evaluate decision on {0}", remoteEpisode.Release.Title); - return new Rejection($"{spec.GetType().Name}: {e.Message}"); + return new DownloadRejection(DownloadRejectionReason.DecisionError, $"{spec.GetType().Name}: {e.Message}"); } return null; diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadRejection.cs b/src/NzbDrone.Core/DecisionEngine/DownloadRejection.cs new file mode 100644 index 000000000..fbaeccdf1 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/DownloadRejection.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.DecisionEngine; + +public class DownloadRejection : Rejection<DownloadRejectionReason> +{ + public DownloadRejection(DownloadRejectionReason reason, string message, RejectionType type = RejectionType.Permanent) + : base(reason, message, type) + { + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs new file mode 100644 index 000000000..72e83b93f --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs @@ -0,0 +1,75 @@ +namespace NzbDrone.Core.DecisionEngine; + +public enum DownloadRejectionReason +{ + Unknown, + UnknownSeries, + UnknownEpisode, + MatchesAnotherSeries, + UnableToParse, + Error, + DecisionError, + MinimumAgeDelay, + SeriesNotMonitored, + EpisodeNotMonitored, + HistoryRecentCutoffMet, + HistoryCdhDisabledCutoffMet, + HistoryHigherPreference, + HistoryHigherRevision, + HistoryCutoffMet, + HistoryCustomFormatCutoffMet, + HistoryCustomFormatScore, + HistoryCustomFormatScoreIncrement, + NoMatchingTag, + PropersDisabled, + ProperForOldFile, + WrongEpisode, + WrongSeason, + WrongSeries, + FullSeason, + UnknownRuntime, + BelowMinimumSize, + AboveMaximumSize, + AlreadyImportedSameHash, + AlreadyImportedSameName, + UnknownReleaseGroup, + ReleaseGroupDoesNotMatch, + IndexerDisabled, + Blocklisted, + CustomFormatMinimumScore, + MinimumFreeSpace, + FullSeasonNotAired, + MaximumSizeExceeded, + MinimumAge, + MaximumAge, + MultiSeason, + Sample, + ProtocolDisabled, + QualityNotWanted, + QualityUpgradesDisabled, + QueueHigherPreference, + QueueHigherRevision, + QueueCutoffMet, + QueueCustomFormatCutoffMet, + QueueCustomFormatScore, + QueueCustomFormatScoreIncrement, + QueueNoUpgrades, + QueuePropersDisabled, + Raw, + MustContainMissing, + MustNotContainPresent, + RepackDisabled, + RepackUnknownReleaseGroup, + RepackReleaseGroupDoesNotMatch, + ExistingFileHasMoreEpisodes, + AmbiguousNumbering, + NotSeasonPack, + SplitEpisode, + MinimumSeeders, + DiskHigherPreference, + DiskHigherRevision, + DiskCutoffMet, + DiskCustomFormatCutoffMet, + DiskCustomFormatScore, + DiskCustomFormatScoreIncrement, +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadSpecDecision.cs b/src/NzbDrone.Core/DecisionEngine/DownloadSpecDecision.cs new file mode 100644 index 000000000..53653293d --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/DownloadSpecDecision.cs @@ -0,0 +1,34 @@ +namespace NzbDrone.Core.DecisionEngine +{ + public class DownloadSpecDecision + { + public bool Accepted { get; private set; } + public DownloadRejectionReason Reason { get; set; } + public string Message { get; private set; } + + private static readonly DownloadSpecDecision AcceptDownloadSpecDecision = new () { Accepted = true }; + private DownloadSpecDecision() + { + } + + public static DownloadSpecDecision Accept() + { + return AcceptDownloadSpecDecision; + } + + public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message, params object[] args) + { + return Reject(reason, string.Format(message, args)); + } + + public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message) + { + return new DownloadSpecDecision + { + Accepted = false, + Reason = reason, + Message = message + }; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Rejection.cs b/src/NzbDrone.Core/DecisionEngine/Rejection.cs index 723968f9b..1f7057ea2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Rejection.cs +++ b/src/NzbDrone.Core/DecisionEngine/Rejection.cs @@ -1,19 +1,21 @@ namespace NzbDrone.Core.DecisionEngine { - public class Rejection + public class Rejection<TRejectionReason> { - public string Reason { get; set; } + public TRejectionReason Reason { get; set; } + public string Message { get; set; } public RejectionType Type { get; set; } - public Rejection(string reason, RejectionType type = RejectionType.Permanent) + public Rejection(TRejectionReason reason, string message, RejectionType type = RejectionType.Permanent) { Reason = reason; + Message = message; Type = type; } public override string ToString() { - return string.Format("[{0}] {1}", Type, Reason); + return string.Format("[{0}] {1}", Type, Message); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 04688ce14..2fea42d5d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class AcceptableSizeSpecification : IDecisionEngineSpecification + public class AcceptableSizeSpecification : IDownloadDecisionEngineSpecification { private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IEpisodeService _episodeService; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Beginning size check for: {0}", subject); @@ -33,13 +33,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (subject.ParsedEpisodeInfo.Special) { _logger.Debug("Special release found, skipping size check."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (subject.Release.Size == 0) { _logger.Debug("Release has unknown size, skipping size check"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var seriesRuntime = subject.Series.Runtime; @@ -75,7 +75,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (runtime == 0) { _logger.Debug("Runtime of all episodes is 0, unable to validate size until it is available, rejecting"); - return Decision.Reject("Runtime of all episodes is 0, unable to validate size until it is available"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.UnknownRuntime, "Runtime of all episodes is 0, unable to validate size until it is available"); } var qualityDefinition = _qualityDefinitionService.Get(quality); @@ -93,7 +93,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min"; _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage); - return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); + return DownloadSpecDecision.Reject(DownloadRejectionReason.BelowMinimumSize, "{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); } } @@ -114,12 +114,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min"; _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting", subject, subject.Release.Size, maxSize, runtimeMessage); - return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage); + return DownloadSpecDecision.Reject(DownloadRejectionReason.AboveMaximumSize, "{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage); } } _logger.Debug("Item: {0}, meets size constraints", subject); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AlreadyImportedSpecification.cs index 00bad3ba3..6a4b7cd96 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AlreadyImportedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AlreadyImportedSpecification.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class AlreadyImportedSpecification : IDecisionEngineSpecification + public class AlreadyImportedSpecification : IDownloadDecisionEngineSpecification { private readonly IHistoryService _historyService; private readonly IConfigService _configService; @@ -27,14 +27,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var cdhEnabled = _configService.EnableCompletedDownloadHandling; if (!cdhEnabled) { _logger.Debug("Skipping already imported check because CDH is disabled"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } _logger.Debug("Performing already imported check on report"); @@ -80,7 +80,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (torrentInfo?.InfoHash != null && torrentInfo.InfoHash.ToUpper() == lastGrabbed.DownloadId) { _logger.Debug("Has same torrent hash as a grabbed and imported release"); - return Decision.Reject("Has same torrent hash as a grabbed and imported release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.AlreadyImportedSameHash, "Has same torrent hash as a grabbed and imported release"); } } @@ -90,11 +90,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (release.Title.Equals(lastGrabbed.SourceTitle, StringComparison.InvariantCultureIgnoreCase)) { _logger.Debug("Has same release name as a grabbed and imported release"); - return Decision.Reject("Has same release name as a grabbed and imported release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.AlreadyImportedSameName, "Has same release name as a grabbed and imported release"); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs index d08f8de20..61e7a5535 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class AnimeVersionUpgradeSpecification : IDecisionEngineSpecification + public class AnimeVersionUpgradeSpecification : IDownloadDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; @@ -25,13 +25,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var releaseGroup = subject.ParsedEpisodeInfo.ReleaseGroup; if (subject.Series.SeriesType != SeriesTypes.Anime) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; @@ -39,7 +39,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer) { _logger.Debug("Version upgrades are not preferred, skipping check"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) @@ -54,24 +54,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (file.ReleaseGroup.IsNullOrWhiteSpace()) { _logger.Debug("Unable to compare release group, existing file's release group is unknown"); - return Decision.Reject("Existing release group is unknown"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.UnknownReleaseGroup, "Existing release group is unknown"); } if (releaseGroup.IsNullOrWhiteSpace()) { _logger.Debug("Unable to compare release group, release's release group is unknown"); - return Decision.Reject("Release group is unknown"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.UnknownReleaseGroup, "Release group is unknown"); } if (file.ReleaseGroup != releaseGroup) { _logger.Debug("Existing Release group is: {0} - release's release group is: {1}", file.ReleaseGroup, releaseGroup); - return Decision.Reject("{0} does not match existing release group {1}", releaseGroup, file.ReleaseGroup); + return DownloadSpecDecision.Reject(DownloadRejectionReason.ReleaseGroupDoesNotMatch, "{0} does not match existing release group {1}", releaseGroup, file.ReleaseGroup); } } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs index 9f7f0bc20..db5e59d6e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class BlockedIndexerSpecification : IDecisionEngineSpecification + public class BlockedIndexerSpecification : IDownloadDecisionEngineSpecification { private readonly IIndexerStatusService _indexerStatusService; private readonly Logger _logger; @@ -27,15 +27,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Temporary; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var status = _blockedIndexerCache.Find(subject.Release.IndexerId.ToString()); if (status != null) { - return Decision.Reject($"Indexer {subject.Release.Indexer} is blocked till {status.DisabledTill} due to failures, cannot grab release."); + return DownloadSpecDecision.Reject(DownloadRejectionReason.IndexerDisabled, $"Indexer {subject.Release.Indexer} is blocked till {status.DisabledTill} due to failures, cannot grab release."); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } private IDictionary<string, IndexerStatus> FetchBlockedIndexer() diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlocklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlocklistSpecification.cs index 422c4a7dd..61069b5b5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlocklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlocklistSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class BlocklistSpecification : IDecisionEngineSpecification + public class BlocklistSpecification : IDownloadDecisionEngineSpecification { private readonly IBlocklistService _blocklistService; private readonly Logger _logger; @@ -19,15 +19,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (_blocklistService.Blocklisted(subject.Series.Id, subject.Release)) { _logger.Debug("{0} is blocklisted, rejecting.", subject.Release.Title); - return Decision.Reject("Release is blocklisted"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.Blocklisted, "Release is blocklisted"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs index a047abb3f..425c183d1 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs @@ -4,22 +4,22 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class CustomFormatAllowedbyProfileSpecification : IDecisionEngineSpecification + public class CustomFormatAllowedbyProfileSpecification : IDownloadDecisionEngineSpecification { public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var minScore = subject.Series.QualityProfile.Value.MinFormatScore; var score = subject.CustomFormatScore; if (score < minScore) { - return Decision.Reject("Custom Formats {0} have score {1} below Series profile minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.CustomFormatMinimumScore, "Custom Formats {0} have score {1} below Series profile minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs index 2d3d3b082..944bde33a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class FreeSpaceSpecification : IDecisionEngineSpecification + public class FreeSpaceSpecification : IDownloadDecisionEngineSpecification { private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; @@ -24,12 +24,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (_configService.SkipFreeSpaceCheckWhenImporting) { _logger.Debug("Skipping free space check"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var size = subject.Release.Size; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Debug("Unable to get available space for {0}. Skipping", path); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var minimumSpace = _configService.MinimumFreeSpaceWhenImporting.Megabytes(); @@ -60,7 +60,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var message = "Importing after download will exceed available disk space"; _logger.Debug(message); - return Decision.Reject(message); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumFreeSpace, message); } if (remainingSpace < minimumSpace) @@ -68,10 +68,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var message = $"Not enough free space ({minimumSpace.SizeSuffix()}) to import after download: {remainingSpace.SizeSuffix()}. (Settings: Media Management: Minimum Free Space)"; _logger.Debug(message); - return Decision.Reject(message); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumFreeSpace, message); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index 084ab0847..91f886ae2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -7,7 +7,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class FullSeasonSpecification : IDecisionEngineSpecification + public class FullSeasonSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -19,7 +19,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.ParsedEpisodeInfo.FullSeason) { @@ -28,11 +28,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (subject.Episodes.Any(e => !e.AirDateUtc.HasValue || e.AirDateUtc.Value.After(DateTime.UtcNow.AddHours(24)))) { _logger.Debug("Full season release {0} rejected. All episodes haven't aired yet.", subject.Release.Title); - return Decision.Reject("Full season release rejected. All episodes haven't aired yet."); + return DownloadSpecDecision.Reject(DownloadRejectionReason.FullSeasonNotAired, "Full season release rejected. All episodes haven't aired yet."); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/IDownloadDecisionEngineSpecification.cs similarity index 59% rename from src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs rename to src/NzbDrone.Core/DecisionEngine/Specifications/IDownloadDecisionEngineSpecification.cs index 2a1312191..9eecf0a93 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/IDownloadDecisionEngineSpecification.cs @@ -3,12 +3,12 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public interface IDecisionEngineSpecification + public interface IDownloadDecisionEngineSpecification { RejectionType Type { get; } SpecificationPriority Priority { get; } - Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); + DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MaximumSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MaximumSizeSpecification.cs index 553d06ab6..d9c0b420b 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MaximumSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MaximumSizeSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class MaximumSizeSpecification : IDecisionEngineSpecification + public class MaximumSizeSpecification : IDownloadDecisionEngineSpecification { private readonly IConfigService _configService; private readonly Logger _logger; @@ -20,7 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var size = subject.Release.Size; var maximumSize = _configService.MaximumSize.Megabytes(); @@ -28,13 +28,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (maximumSize == 0) { _logger.Debug("Maximum size is not set."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (size == 0) { _logger.Debug("Release has unknown size, skipping size check."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } _logger.Debug("Checking if release meets maximum size requirements. {0}", size.SizeSuffix()); @@ -44,10 +44,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var message = $"{size.SizeSuffix()} is too big, maximum size is {maximumSize.SizeSuffix()} (Settings->Indexers->Maximum Size)"; _logger.Debug(message); - return Decision.Reject(message); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MaximumSizeExceeded, message); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs index 854e4e5ed..61972c6b5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class MinimumAgeSpecification : IDecisionEngineSpecification + public class MinimumAgeSpecification : IDownloadDecisionEngineSpecification { private readonly IConfigService _configService; private readonly Logger _logger; @@ -20,12 +20,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Temporary; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) { _logger.Debug("Not checking minimum age requirement for non-usenet report"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var age = subject.Release.AgeMinutes; @@ -35,7 +35,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (minimumAge == 0) { _logger.Debug("Minimum age is not set."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } _logger.Debug("Checking if report meets minimum age requirements. {0}", ageRounded); @@ -43,12 +43,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (age < minimumAge) { _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge); - return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumAge, "Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge); } _logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", ageRounded, minimumAge); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs index 1c6479436..ba117324a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class MultiSeasonSpecification : IDecisionEngineSpecification + public class MultiSeasonSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -16,15 +16,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.ParsedEpisodeInfo.IsMultiSeason) { _logger.Debug("Multi-season release {0} rejected. Not supported", subject.Release.Title); - return Decision.Reject("Multi-season releases are not supported"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MultiSeason, "Multi-season releases are not supported"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index 7340df169..5a22882a5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class NotSampleSpecification : IDecisionEngineSpecification + public class NotSampleSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -17,15 +17,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release.Title.ToLower().Contains("sample") && subject.Release.Size < 70.Megabytes()) { _logger.Debug("Sample release, rejecting."); - return Decision.Reject("Sample"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.Sample, "Sample"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs index ba956ecbd..e950611ff 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Profiles.Delay; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class ProtocolSpecification : IDecisionEngineSpecification + public class ProtocolSpecification : IDownloadDecisionEngineSpecification { private readonly IDelayProfileService _delayProfileService; private readonly Logger _logger; @@ -21,23 +21,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var delayProfile = _delayProfileService.BestForTags(subject.Series.Tags); if (subject.Release.DownloadProtocol == DownloadProtocol.Usenet && !delayProfile.EnableUsenet) { _logger.Debug("[{0}] Usenet is not enabled for this series", subject.Release.Title); - return Decision.Reject("Usenet is not enabled for this series"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.ProtocolDisabled, "Usenet is not enabled for this series"); } if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent && !delayProfile.EnableTorrent) { _logger.Debug("[{0}] Torrent is not enabled for this series", subject.Release.Title); - return Decision.Reject("Torrent is not enabled for this series"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.ProtocolDisabled, "Torrent is not enabled for this series"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 4adab86a0..6e2057868 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class QualityAllowedByProfileSpecification : IDecisionEngineSpecification + public class QualityAllowedByProfileSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -16,7 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedEpisodeInfo.Quality); @@ -27,10 +27,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!qualityOrGroup.Allowed) { _logger.Debug("Quality {0} rejected by Series' quality profile", subject.ParsedEpisodeInfo.Quality); - return Decision.Reject("{0} is not wanted in profile", subject.ParsedEpisodeInfo.Quality.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QualityNotWanted, "{0} is not wanted in profile", subject.ParsedEpisodeInfo.Quality.Quality); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 97a1993e8..66160260e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -10,7 +10,7 @@ using NzbDrone.Core.Queue; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class QueueSpecification : IDecisionEngineSpecification + public class QueueSpecification : IDownloadDecisionEngineSpecification { private readonly IQueueService _queueService; private readonly UpgradableSpecification _upgradableSpecification; @@ -34,7 +34,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var queue = _queueService.GetQueue(); var matchingEpisode = queue.Where(q => q.RemoteEpisode?.Series != null && @@ -65,7 +65,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications queuedItemCustomFormats, subject.ParsedEpisodeInfo.Quality)) { - return Decision.Reject("Release in queue already meets cutoff: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCutoffMet, "Release in queue already meets cutoff: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); } _logger.Debug("Checking if release is higher quality than queued release. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); @@ -79,22 +79,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications switch (upgradeableRejectReason) { case UpgradeableRejectReason.BetterQuality: - return Decision.Reject("Release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueHigherPreference, "Release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); case UpgradeableRejectReason.BetterRevision: - return Decision.Reject("Release in queue is of equal or higher revision: {0}", remoteEpisode.ParsedEpisodeInfo.Quality.Revision); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueHigherRevision, "Release in queue is of equal or higher revision: {0}", remoteEpisode.ParsedEpisodeInfo.Quality.Revision); case UpgradeableRejectReason.QualityCutoff: - return Decision.Reject("Release in queue meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCutoffMet, "Release in queue meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); case UpgradeableRejectReason.CustomFormatCutoff: - return Decision.Reject("Release in queue meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCustomFormatCutoffMet, "Release in queue meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("Release in queue has an equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(queuedItemCustomFormats)); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCustomFormatScore, "Release in queue has an equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(queuedItemCustomFormats)); case UpgradeableRejectReason.MinCustomFormatScore: - return Decision.Reject("Release in queue has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCustomFormatScoreIncrement, "Release in queue has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); } _logger.Debug("Checking if profiles allow upgrading. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); @@ -105,7 +105,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications subject.ParsedEpisodeInfo.Quality, subject.CustomFormats)) { - return Decision.Reject("Another release is queued and the Quality profile does not allow upgrades"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueNoUpgrades, "Another release is queued and the Quality profile does not allow upgrades"); } if (_upgradableSpecification.IsRevisionUpgrade(remoteEpisode.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Quality)) @@ -113,12 +113,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (_configService.DownloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade) { _logger.Debug("Auto downloading of propers is disabled"); - return Decision.Reject("Proper downloading is disabled"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueuePropersDisabled, "Proper downloading is disabled"); } } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index ed4fb1545..681a7ce29 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -7,7 +7,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class RawDiskSpecification : IDecisionEngineSpecification + public class RawDiskSpecification : IDownloadDecisionEngineSpecification { private static readonly Regex[] DiscRegex = new[] { @@ -29,11 +29,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } foreach (var regex in DiscRegex) @@ -41,28 +41,28 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (regex.IsMatch(subject.Release.Title)) { _logger.Debug("Release contains raw Bluray/DVD, rejecting."); - return Decision.Reject("Raw Bluray/DVD release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.Raw, "Raw Bluray/DVD release"); } } if (subject.Release.Container.IsNullOrWhiteSpace()) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (_dvdContainerTypes.Contains(subject.Release.Container.ToLower())) { _logger.Debug("Release contains raw DVD, rejecting."); - return Decision.Reject("Raw DVD release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.Raw, "Raw DVD release"); } if (_blurayContainerTypes.Contains(subject.Release.Container.ToLower())) { _logger.Debug("Release contains raw Bluray, rejecting."); - return Decision.Reject("Raw Bluray release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.Raw, "Raw Bluray release"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 2fcad98ef..a125d4f01 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification + public class ReleaseRestrictionsSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; private readonly IReleaseProfileService _releaseProfileService; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if release meets restrictions: {0}", subject); @@ -43,7 +43,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var terms = string.Join(", ", requiredTerms); _logger.Debug("[{0}] does not contain one of the required terms: {1}", title, terms); - return Decision.Reject("Does not contain one of the required terms: {0}", terms); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MustContainMissing, "Does not contain one of the required terms: {0}", terms); } } @@ -56,12 +56,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var terms = string.Join(", ", foundTerms); _logger.Debug("[{0}] contains these ignored terms: {1}", title, terms); - return Decision.Reject("Contains these ignored terms: {0}", terms); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MustNotContainPresent, "Contains these ignored terms: {0}", terms); } } _logger.Debug("[{0}] No restrictions apply, allowing", subject); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } private List<string> ContainsAny(List<string> terms, string title) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs index b82c7e564..bda0cdeed 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class RepackSpecification : IDecisionEngineSpecification + public class RepackSpecification : IDownloadDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; @@ -25,11 +25,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (!subject.ParsedEpisodeInfo.Quality.Revision.IsRepack) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer) { _logger.Debug("Repacks are not preferred, skipping check"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) @@ -47,7 +47,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade) { _logger.Debug("Auto downloading of repacks is disabled"); - return Decision.Reject("Repack downloading is disabled"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.RepackDisabled, "Repack downloading is disabled"); } var releaseGroup = subject.ParsedEpisodeInfo.ReleaseGroup; @@ -55,12 +55,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (fileReleaseGroup.IsNullOrWhiteSpace()) { - return Decision.Reject("Unable to determine release group for the existing file"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.RepackUnknownReleaseGroup, "Unable to determine release group for the existing file"); } if (releaseGroup.IsNullOrWhiteSpace()) { - return Decision.Reject("Unable to determine release group for this release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.RepackUnknownReleaseGroup, "Unable to determine release group for this release"); } if (!fileReleaseGroup.Equals(releaseGroup, StringComparison.InvariantCultureIgnoreCase)) @@ -69,7 +69,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications "Release is a repack for a different release group. Release Group: {0}. File release group: {1}", releaseGroup, fileReleaseGroup); - return Decision.Reject( + return DownloadSpecDecision.Reject( + DownloadRejectionReason.RepackReleaseGroupDoesNotMatch, "Release is a repack for a different release group. Release Group: {0}. File release group: {1}", releaseGroup, fileReleaseGroup); @@ -77,7 +78,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index 99cf93f67..22fcf4253 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class RetentionSpecification : IDecisionEngineSpecification + public class RetentionSpecification : IDownloadDecisionEngineSpecification { private readonly IConfigService _configService; private readonly Logger _logger; @@ -19,12 +19,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) { _logger.Debug("Not checking retention requirement for non-usenet report"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var age = subject.Release.Age; @@ -34,10 +34,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (retention > 0 && age > retention) { _logger.Debug("Report age: {0} rejected by user's retention limit", age); - return Decision.Reject("Older than configured retention"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MaximumAge, "Older than configured retention"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 07dce7b5c..1a9339bb3 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class DelaySpecification : IDecisionEngineSpecification + public class DelaySpecification : IDownloadDecisionEngineSpecification { private readonly IPendingReleaseService _pendingReleaseService; private readonly IDelayProfileService _delayProfileService; @@ -26,12 +26,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Temporary; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null && searchCriteria.UserInvokedSearch) { _logger.Debug("Ignoring delay for user invoked search"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var qualityProfile = subject.Series.QualityProfile.Value; @@ -42,7 +42,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (delay == 0) { _logger.Debug("QualityProfile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var qualityComparer = new QualityModelComparer(qualityProfile); @@ -58,7 +58,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (qualityCompare == 0 && newQuality?.Revision.CompareTo(currentQuality.Revision) > 0) { _logger.Debug("New quality is a better revision for existing quality, skipping delay"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } @@ -72,7 +72,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (isBestInProfile && isPreferredProtocol) { _logger.Debug("Quality is highest in profile for preferred protocol, will not delay"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } @@ -85,7 +85,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (score >= minimum && isPreferredProtocol) { _logger.Debug("Custom format score ({0}) meets minimum ({1}) for preferred protocol, will not delay", score, minimum); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } @@ -95,16 +95,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (oldest != null && oldest.Release.AgeMinutes > delay) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (subject.Release.AgeMinutes < delay) { _logger.Debug("Waiting for better quality release, There is a {0} minute delay on {1}", delay, subject.Release.DownloadProtocol); - return Decision.Reject("Waiting for better quality release"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumAgeDelay, "Waiting for better quality release"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs index 7a2d3f5d6..2f8208fb4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs @@ -10,7 +10,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class DeletedEpisodeFileSpecification : IDecisionEngineSpecification + public class DeletedEpisodeFileSpecification : IDownloadDecisionEngineSpecification { private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; @@ -26,17 +26,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Disk; public RejectionType Type => RejectionType.Temporary; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (!_configService.AutoUnmonitorPreviouslyDownloadedEpisodes) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (searchCriteria != null) { _logger.Debug("Skipping deleted episodefile check during search"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var missingEpisodeFiles = subject.Episodes @@ -54,10 +54,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } _logger.Debug("Files for this episode exist in the database but not on disk, will be unmonitored on next diskscan. skipping."); - return Decision.Reject("Series is not monitored"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.SeriesNotMonitored, "Series is not monitored"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } private bool IsEpisodeFileMissing(Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 34b2ace4b..f063c5e28 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class HistorySpecification : IDecisionEngineSpecification + public class HistorySpecification : IDownloadDecisionEngineSpecification { private readonly IHistoryService _historyService; private readonly UpgradableSpecification _upgradableSpecification; @@ -33,12 +33,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) { _logger.Debug("Skipping history check during search"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var cdhEnabled = _configService.EnableCompletedDownloadHandling; @@ -81,10 +81,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { if (recent) { - return Decision.Reject("Recent grab event in history already meets cutoff: {0}", mostRecent.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryRecentCutoffMet, "Recent grab event in history already meets cutoff: {0}", mostRecent.Quality); } - return Decision.Reject("CDH is disabled and grab event in history already meets cutoff: {0}", mostRecent.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCdhDisabledCutoffMet, "CDH is disabled and grab event in history already meets cutoff: {0}", mostRecent.Quality); } var rejectionSubject = recent ? "Recent" : "CDH is disabled and"; @@ -95,27 +95,27 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync continue; case UpgradeableRejectReason.BetterQuality: - return Decision.Reject("{0} grab event in history is of equal or higher preference: {1}", rejectionSubject, mostRecent.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryHigherPreference, "{0} grab event in history is of equal or higher preference: {1}", rejectionSubject, mostRecent.Quality); case UpgradeableRejectReason.BetterRevision: - return Decision.Reject("{0} grab event in history is of equal or higher revision: {1}", rejectionSubject, mostRecent.Quality.Revision); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryHigherRevision, "{0} grab event in history is of equal or higher revision: {1}", rejectionSubject, mostRecent.Quality.Revision); case UpgradeableRejectReason.QualityCutoff: - return Decision.Reject("{0} grab event in history meets quality cutoff: {1}", rejectionSubject, qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCutoffMet, "{0} grab event in history meets quality cutoff: {1}", rejectionSubject, qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); case UpgradeableRejectReason.CustomFormatCutoff: - return Decision.Reject("{0} grab event in history meets Custom Format cutoff: {1}", rejectionSubject, qualityProfile.CutoffFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCustomFormatCutoffMet, "{0} grab event in history meets Custom Format cutoff: {1}", rejectionSubject, qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("{0} grab event in history has an equal or higher Custom Format score: {1}", rejectionSubject, qualityProfile.CalculateCustomFormatScore(customFormats)); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCustomFormatScore, "{0} grab event in history has an equal or higher Custom Format score: {1}", rejectionSubject, qualityProfile.CalculateCustomFormatScore(customFormats)); case UpgradeableRejectReason.MinCustomFormatScore: - return Decision.Reject("{0} grab event in history has Custom Format score within Custom Format score increment: {1}", rejectionSubject, qualityProfile.MinUpgradeFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCustomFormatScoreIncrement, "{0} grab event in history has Custom Format score within Custom Format score increment: {1}", rejectionSubject, qualityProfile.MinUpgradeFormatScore); } } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs index 2d861bad8..0bdac306f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class IndexerTagSpecification : IDecisionEngineSpecification + public class IndexerTagSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; private readonly IIndexerFactory _indexerFactory; @@ -22,11 +22,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release == null || subject.Series?.Tags == null || subject.Release.IndexerId == 0) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } IndexerDefinition indexer; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync catch (ModelNotFoundException) { _logger.Debug("Indexer with id {0} does not exist, skipping indexer tags check", subject.Release.IndexerId); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } // If indexer has tags, check that at least one of them is present on the series @@ -47,10 +47,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { _logger.Debug("Indexer {0} has tags. None of these are present on series {1}. Rejecting", subject.Release.Indexer, subject.Series); - return Decision.Reject("Series tags do not match any of the indexer tags"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.NoMatchingTag, "Series tags do not match any of the indexer tags"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs index a0524bea2..cbd36198d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class MonitoredEpisodeSpecification : IDecisionEngineSpecification + public class MonitoredEpisodeSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -17,33 +17,33 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) { if (!searchCriteria.MonitoredEpisodesOnly) { _logger.Debug("Skipping monitored check during search"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } if (!subject.Series.Monitored) { _logger.Debug("{0} is present in the DB but not tracked. Rejecting", subject.Series); - return Decision.Reject("Series is not monitored"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.SeriesNotMonitored, "Series is not monitored"); } var monitoredCount = subject.Episodes.Count(episode => episode.Monitored); if (monitoredCount == subject.Episodes.Count) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (subject.Episodes.Count == 1) { _logger.Debug("Episode is not monitored. Rejecting", monitoredCount, subject.Episodes.Count); - return Decision.Reject("Episode is not monitored"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.EpisodeNotMonitored, "Episode is not monitored"); } if (monitoredCount == 0) @@ -55,7 +55,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger.Debug("Only {0}/{1} episodes in the release are monitored. Rejecting", monitoredCount, subject.Episodes.Count); } - return Decision.Reject("One or more episodes is not monitored"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.EpisodeNotMonitored, "One or more episodes is not monitored"); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index d2e843c90..ea7fc99ab 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class ProperSpecification : IDecisionEngineSpecification + public class ProperSpecification : IDownloadDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; @@ -24,11 +24,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; @@ -36,7 +36,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer) { _logger.Debug("Propers are not preferred, skipping check"); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) @@ -46,18 +46,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade) { _logger.Debug("Auto downloading of propers is disabled"); - return Decision.Reject("Proper downloading is disabled"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.PropersDisabled, "Proper downloading is disabled"); } if (file.DateAdded < DateTime.Today.AddDays(-7)) { _logger.Debug("Proper for old file, rejecting: {0}", subject); - return Decision.Reject("Proper for old file"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.ProperForOldFile, "Proper for old file"); } } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs index 0ef769d01..7bddcd175 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class SameEpisodesGrabSpecification : IDecisionEngineSpecification + public class SameEpisodesGrabSpecification : IDownloadDecisionEngineSpecification { private readonly SameEpisodesSpecification _sameEpisodesSpecification; private readonly Logger _logger; @@ -18,15 +18,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (_sameEpisodesSpecification.IsSatisfiedBy(subject.Episodes)) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } _logger.Debug("Episode file on disk contains more episodes than this release contains"); - return Decision.Reject("Episode file on disk contains more episodes than this release contains"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.ExistingFileHasMoreEpisodes, "Episode file on disk contains more episodes than this release contains"); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SceneMappingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SceneMappingSpecification.cs index 04bb2c712..271103b32 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SceneMappingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SceneMappingSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class SceneMappingSpecification : IDecisionEngineSpecification + public class SceneMappingSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -17,18 +17,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Temporary; // Temporary till there's a mapping - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (remoteEpisode.SceneMapping == null) { _logger.Debug("No applicable scene mapping, skipping."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (remoteEpisode.SceneMapping.SceneOrigin.IsNullOrWhiteSpace()) { _logger.Debug("No explicit scene origin in scene mapping."); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var split = remoteEpisode.SceneMapping.SceneOrigin.Split(':'); @@ -50,11 +50,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (remoteEpisode.SceneMapping.Comment.IsNotNullOrWhiteSpace()) { - return Decision.Reject("{0} has ambiguous numbering"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.AmbiguousNumbering, "{0} has ambiguous numbering"); } else { - return Decision.Reject("Ambiguous numbering"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.AmbiguousNumbering, "Ambiguous numbering"); } } @@ -65,7 +65,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("SceneMapping origin is explicitly unknown, unsure what numbering scheme it uses but '{0}' will be assumed. Provide full release title to Sonarr/TheXEM team.", type); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs index 65677471d..4d94ccb72 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.Search { - public class EpisodeRequestedSpecification : IDecisionEngineSpecification + public class EpisodeRequestedSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -17,11 +17,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var criteriaEpisodes = searchCriteria.Episodes.Select(v => v.Id).ToList(); @@ -37,20 +37,20 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (episodes.Count > 1) { - return Decision.Reject($"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}-{episodes.Last().EpisodeNumber}"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongEpisode, $"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}-{episodes.Last().EpisodeNumber}"); } else { - return Decision.Reject($"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongEpisode, $"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}"); } } else { - return Decision.Reject("Episode wasn't requested"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongEpisode, "Episode wasn't requested"); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index 1acfd44f9..f89c41988 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.Search { - public class SeasonMatchSpecification : IDecisionEngineSpecification + public class SeasonMatchSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; private readonly ISceneMappingService _sceneMappingService; @@ -19,26 +19,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var singleEpisodeSpec = searchCriteria as SeasonSearchCriteria; if (singleEpisodeSpec == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongSeason, "Wrong season"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs index 07afbaada..e1796eb41 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.Search { - public class SeriesSpecification : IDecisionEngineSpecification + public class SeriesSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -16,11 +16,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } _logger.Debug("Checking if series matches searched series"); @@ -28,10 +28,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (remoteEpisode.Series.Id != searchCriteria.Series.Id) { _logger.Debug("Series {0} does not match {1}", remoteEpisode.Series, searchCriteria.Series); - return Decision.Reject("Wrong series"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongSeries, "Wrong series"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs index 1aa995ef7..bd76be698 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.Search { - public class SingleEpisodeSearchMatchSpecification : IDecisionEngineSpecification + public class SingleEpisodeSearchMatchSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; private readonly ISceneMappingService _sceneMappingService; @@ -20,11 +20,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var singleEpisodeSpec = searchCriteria as SingleEpisodeSearchCriteria; @@ -39,41 +39,41 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return IsSatisfiedBy(remoteEpisode, animeEpisodeSpec); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } - private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec) + private DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec) { if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongSeason, "Wrong season"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Any()) { _logger.Debug("Full season result during single episode search, skipping."); - return Decision.Reject("Full season pack"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.FullSeason, "Full season pack"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Contains(singleEpisodeSpec.EpisodeNumber)) { _logger.Debug("Episode number does not match searched episode number, skipping."); - return Decision.Reject("Wrong episode"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.WrongEpisode, "Wrong episode"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } - private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, AnimeEpisodeSearchCriteria animeEpisodeSpec) + private DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, AnimeEpisodeSearchCriteria animeEpisodeSpec) { if (remoteEpisode.ParsedEpisodeInfo.FullSeason && !animeEpisodeSpec.IsSeasonSearch) { _logger.Debug("Full season result during single episode search, skipping."); - return Decision.Reject("Full season pack"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.FullSeason, "Full season pack"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SeasonPackOnlySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SeasonPackOnlySpecification.cs index 2934be358..12939332e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SeasonPackOnlySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SeasonPackOnlySpecification.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class SeasonPackOnlySpecification : IDecisionEngineSpecification + public class SeasonPackOnlySpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -20,11 +20,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria == null || searchCriteria.Episodes.Count == 1) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } if (subject.Release.SeasonSearchMaximumSingleEpisodeAge > 0) @@ -37,12 +37,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (subset.Count > 0 && subset.Max(e => e.AirDateUtc).Value.Before(DateTime.UtcNow - TimeSpan.FromDays(subject.Release.SeasonSearchMaximumSingleEpisodeAge))) { _logger.Debug("Release {0}: last episode in this season aired more than {1} days ago, season pack required.", subject.Release.Title, subject.Release.SeasonSearchMaximumSingleEpisodeAge); - return Decision.Reject("Last episode in this season aired more than {0} days ago, season pack required.", subject.Release.SeasonSearchMaximumSingleEpisodeAge); + return DownloadSpecDecision.Reject(DownloadRejectionReason.NotSeasonPack, "Last episode in this season aired more than {0} days ago, season pack required.", subject.Release.SeasonSearchMaximumSingleEpisodeAge); } } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs index fef3be741..c9bff29a3 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class SplitEpisodeSpecification : IDecisionEngineSpecification + public class SplitEpisodeSpecification : IDownloadDecisionEngineSpecification { private readonly Logger _logger; @@ -16,15 +16,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.ParsedEpisodeInfo.IsSplitEpisode) { _logger.Debug("Split episode release {0} rejected. Not supported", subject.Release.Title); - return Decision.Reject("Split episode releases are not supported"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.SplitEpisode, "Split episode releases are not supported"); } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs index be0de3fdb..ee6df440b 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class TorrentSeedingSpecification : IDecisionEngineSpecification + public class TorrentSeedingSpecification : IDownloadDecisionEngineSpecification { private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; @@ -20,13 +20,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { var torrentInfo = remoteEpisode.Release as TorrentInfo; if (torrentInfo == null || torrentInfo.IndexerId == 0) { - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } IndexerDefinition indexer; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications catch (ModelNotFoundException) { _logger.Debug("Indexer with id {0} does not exist, skipping seeders check", torrentInfo.IndexerId); - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; @@ -49,11 +49,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (torrentInfo.Seeders.HasValue && torrentInfo.Seeders.Value < minimumSeeders) { _logger.Debug("Not enough seeders: {0}. Minimum seeders: {1}", torrentInfo.Seeders, minimumSeeders); - return Decision.Reject("Not enough seeders: {0}. Minimum seeders: {1}", torrentInfo.Seeders, minimumSeeders); + return DownloadSpecDecision.Reject(DownloadRejectionReason.MinimumSeeders, "Not enough seeders: {0}. Minimum seeders: {1}", torrentInfo.Seeders, minimumSeeders); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs index f83e137cc..9de0cead4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class UpgradeAllowedSpecification : IDecisionEngineSpecification + public class UpgradeAllowedSpecification : IDownloadDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; private readonly ICustomFormatCalculationService _formatService; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var qualityProfile = subject.Series.QualityProfile.Value; @@ -48,11 +48,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Debug("Upgrading is not allowed by the quality profile"); - return Decision.Reject("Existing file and the Quality profile does not allow upgrades"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.QualityUpgradesDisabled, "Existing file and the Quality profile does not allow upgrades"); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index c7afa3351..0185d4d87 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { - public class UpgradeDiskSpecification : IDecisionEngineSpecification + public class UpgradeDiskSpecification : IDownloadDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; private readonly ICustomFormatCalculationService _formatService; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; - public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var qualityProfile = subject.Series.QualityProfile.Value; @@ -48,7 +48,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var cutoff = qualityProfile.UpgradeAllowed ? qualityProfile.Cutoff : qualityProfile.FirststAllowedQuality().Id; var qualityCutoff = qualityProfile.Items[qualityProfile.GetIndex(cutoff).Index]; - return Decision.Reject("Existing file meets cutoff: {0}", qualityCutoff); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file meets cutoff: {0}", qualityCutoff); } var customFormats = _formatService.ParseCustomFormat(file); @@ -65,26 +65,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications continue; case UpgradeableRejectReason.BetterQuality: - return Decision.Reject("Existing file on disk is of equal or higher preference: {0}", file.Quality); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherPreference, "Existing file on disk is of equal or higher preference: {0}", file.Quality); case UpgradeableRejectReason.BetterRevision: - return Decision.Reject("Existing file on disk is of equal or higher revision: {0}", file.Quality.Revision); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherRevision, "Existing file on disk is of equal or higher revision: {0}", file.Quality.Revision); case UpgradeableRejectReason.QualityCutoff: - return Decision.Reject("Existing file on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]); case UpgradeableRejectReason.CustomFormatCutoff: - return Decision.Reject("Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatCutoffMet, "Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore); case UpgradeableRejectReason.CustomFormatScore: - return Decision.Reject("Existing file on disk has a equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScore, "Existing file on disk has a equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); case UpgradeableRejectReason.MinCustomFormatScore: - return Decision.Reject("Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScoreIncrement, "Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); } } - return Decision.Accept(); + return DownloadSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index f7afddebc..98d55064b 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -6,7 +6,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Parser; @@ -178,7 +177,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Warn("Unable to process folder that is mapped to an existing series"); return new List<ImportResult> { - RejectionResult("Import path is mapped to a series folder") + RejectionResult(ImportRejectionReason.SeriesFolder, "Import path is mapped to a series folder") }; } @@ -255,7 +254,7 @@ namespace NzbDrone.Core.MediaFiles return new List<ImportResult> { - new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new Rejection("Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'") + new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new ImportRejection(ImportRejectionReason.InvalidFilePath, "Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'") }; } @@ -268,7 +267,7 @@ namespace NzbDrone.Core.MediaFiles return new List<ImportResult> { new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, - new Rejection($"Invalid video file, unsupported extension: '{extension}'")), + new ImportRejection(ImportRejectionReason.UnsupportedExtension, $"Invalid video file, unsupported extension: '{extension}'")), $"Invalid video file, unsupported extension: '{extension}'") }; } @@ -300,19 +299,19 @@ namespace NzbDrone.Core.MediaFiles private ImportResult FileIsLockedResult(string videoFile) { _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); - return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); + return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new ImportRejection(ImportRejectionReason.FileLocked, "Locked file, try again later")), "Locked file, try again later"); } private ImportResult UnknownSeriesResult(string message, string videoFile = null) { var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; - return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message); + return new ImportResult(new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.UnknownSeries, "Unknown Series")), message); } - private ImportResult RejectionResult(string message) + private ImportResult RejectionResult(ImportRejectionReason reason, string message) { - return new ImportResult(new ImportDecision(null, new Rejection(message)), message); + return new ImportResult(new ImportDecision(null, new ImportRejection(reason, message)), message); } private ImportResult CheckEmptyResultForIssue(string folder) @@ -321,12 +320,12 @@ namespace NzbDrone.Core.MediaFiles if (files.Any(file => FileExtensions.ExecutableExtensions.Contains(Path.GetExtension(file)))) { - return RejectionResult("Caution: Found executable file"); + return RejectionResult(ImportRejectionReason.ExecutableFile, "Caution: Found executable file"); } if (files.Any(file => FileExtensions.ArchiveExtensions.Contains(Path.GetExtension(file)))) { - return RejectionResult("Found archive file, might need to be extracted"); + return RejectionResult(ImportRejectionReason.ArchiveFile, "Found archive file, might need to be extracted"); } return null; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs index 9778664cb..68378ebc5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs @@ -1,11 +1,10 @@ -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; +using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IImportDecisionEngineSpecification { - Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); + ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index f5419dbf6..90f15a348 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -224,7 +224,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport // Adding all the rejected decisions importResults.AddRange(decisions.Where(c => !c.Approved) - .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); + .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Message).ToArray()))); return importResults; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs index 5e4e2ede2..8a0ad0034 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -9,11 +8,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportDecision { public LocalEpisode LocalEpisode { get; private set; } - public IEnumerable<Rejection> Rejections { get; private set; } + public IEnumerable<ImportRejection> Rejections { get; private set; } public bool Approved => Rejections.Empty(); - public ImportDecision(LocalEpisode localEpisode, params Rejection[] rejections) + public ImportDecision(LocalEpisode localEpisode, params ImportRejection[] rejections) { LocalEpisode = localEpisode; Rejections = rejections.ToList(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 590b0f5d7..762a5a4f3 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -5,7 +5,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; @@ -136,15 +135,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (IsPartialSeason(localEpisode)) { - decision = new ImportDecision(localEpisode, new Rejection("Partial season packs are not supported")); + decision = new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.PartialSeason, "Partial season packs are not supported")); } else if (IsSeasonExtra(localEpisode)) { - decision = new ImportDecision(localEpisode, new Rejection("Extras are not supported")); + decision = new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.SeasonExtra, "Extras are not supported")); } else { - decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + decision = new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.InvalidSeasonOrEpisode, "Invalid season or episode")); } } else @@ -167,13 +166,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } catch (AugmentingFailedException) { - decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); + decision = new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.UnableToParse, "Unable to parse file")); } catch (Exception ex) { _logger.Error(ex, "Couldn't import file. {0}", localEpisode.Path); - decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); + decision = new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.Error, "Unexpected error processing file")); } if (decision == null) @@ -192,7 +191,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return decision; } - private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + private ImportRejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { try { @@ -200,7 +199,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (!result.Accepted) { - return new Rejection(result.Reason); + return new ImportRejection(result.Reason, result.Message); } } catch (Exception e) @@ -208,7 +207,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport // e.Data.Add("report", remoteEpisode.Report.ToJson()); // e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); _logger.Error(e, "Couldn't evaluate decision on {0}", localEpisode.Path); - return new Rejection($"{spec.GetType().Name}: {e.Message}"); + return new ImportRejection(ImportRejectionReason.DecisionError, $"{spec.GetType().Name}: {e.Message}"); } return null; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejection.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejection.cs new file mode 100644 index 000000000..0970873a3 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejection.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.DecisionEngine; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport; + +public class ImportRejection : Rejection<ImportRejectionReason> +{ + public ImportRejection(ImportRejectionReason reason, string message, RejectionType type = RejectionType.Permanent) + : base(reason, message, type) + { + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs new file mode 100644 index 000000000..f13f5d2a1 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs @@ -0,0 +1,38 @@ +namespace NzbDrone.Core.MediaFiles.EpisodeImport; + +public enum ImportRejectionReason +{ + Unknown, + FileLocked, + UnknownSeries, + ExecutableFile, + ArchiveFile, + SeriesFolder, + InvalidFilePath, + UnsupportedExtension, + PartialSeason, + SeasonExtra, + InvalidSeasonOrEpisode, + UnableToParse, + Error, + DecisionError, + NoEpisodes, + MissingAbsoluteEpisodeNumber, + EpisodeAlreadyImported, + TitleMissing, + TitleTba, + MinimumFreeSpace, + FullSeason, + NoAudio, + EpisodeUnexpected, + EpisodeNotFoundInRelease, + Sample, + SampleIndeterminate, + Unpacking, + ExistingFileHasMoreEpisodes, + SplitEpisode, + UnverifiedSceneMapping, + NotQualityUpgrade, + NotRevisionUpgrade, + NotCustomFormatUpgrade +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportSpecDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportSpecDecision.cs new file mode 100644 index 000000000..d4f5c5c2f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportSpecDecision.cs @@ -0,0 +1,34 @@ +namespace NzbDrone.Core.MediaFiles.EpisodeImport +{ + public class ImportSpecDecision + { + public bool Accepted { get; private set; } + public ImportRejectionReason Reason { get; set; } + public string Message { get; private set; } + + private static readonly ImportSpecDecision AcceptDecision = new () { Accepted = true }; + private ImportSpecDecision() + { + } + + public static ImportSpecDecision Accept() + { + return AcceptDecision; + } + + public static ImportSpecDecision Reject(ImportRejectionReason reason, string message, params object[] args) + { + return Reject(reason, string.Format(message, args)); + } + + public static ImportSpecDecision Reject(ImportRejectionReason reason, string message) + { + return new ImportSpecDecision + { + Accepted = false, + Reason = reason, + Message = message + }; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index 9f690474b..4e4052b6c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -27,7 +26,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } public ReleaseType ReleaseType { get; set; } - public IEnumerable<Rejection> Rejections { get; set; } + public IEnumerable<ImportRejection> Rejections { get; set; } public ManualImportItem() { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index b3112bc24..aee6b97f4 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -7,7 +7,6 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Languages; @@ -104,7 +103,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual Quality = new QualityModel(Quality.Unknown), Languages = new List<Language> { Language.Unknown }, Size = _diskProvider.GetFileSize(file), - Rejections = Enumerable.Empty<Rejection>() + Rejections = Enumerable.Empty<ImportRejection>() })); } @@ -226,7 +225,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual ReleaseType = releaseType }; - return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); + return MapItem(new ImportDecision(localEpisode, new ImportRejection(ImportRejectionReason.NoEpisodes, "Episodes not selected")), rootFolder, downloadId, null); } return ProcessFile(rootFolder, rootFolder, path, downloadId, series); @@ -338,7 +337,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.Size = _diskProvider.GetFileSize(file); return MapItem(new ImportDecision(localEpisode, - new Rejection("Unknown Series")), + new ImportRejection(ImportRejectionReason.UnknownSeries, "Unknown Series")), rootFolder, downloadId, null); @@ -367,7 +366,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual RelativePath = rootFolder.GetRelativePath(file), Name = Path.GetFileNameWithoutExtension(file), Size = _diskProvider.GetFileSize(file), - Rejections = new List<Rejection>() + Rejections = new List<ImportRejection>() }; } @@ -471,7 +470,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.IndexerFlags = (int)episodeFile.IndexerFlags; item.ReleaseType = episodeFile.ReleaseType; item.Size = _diskProvider.GetFileSize(item.Path); - item.Rejections = Enumerable.Empty<Rejection>(); + item.Rejections = Enumerable.Empty<ImportRejection>(); item.EpisodeFileId = episodeFile.Id; item.CustomFormats = _formatCalculator.ParseCustomFormat(episodeFile, series); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AbsoluteEpisodeNumberSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AbsoluteEpisodeNumberSpecification.cs index 9e361ccf9..134871fa5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AbsoluteEpisodeNumberSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AbsoluteEpisodeNumberSpecification.cs @@ -1,7 +1,6 @@ using System; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -20,18 +19,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.Series.SeriesType != SeriesTypes.Anime) { _logger.Debug("Series type is not Anime, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (!_buildFileNames.RequiresAbsoluteEpisodeNumber()) { _logger.Debug("File name format does not require absolute episode number, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } foreach (var episode in localEpisode.Episodes) @@ -49,11 +48,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { _logger.Debug("Episode does not have an absolute episode number and recently aired"); - return Decision.Reject("Episode does not have an absolute episode number and recently aired"); + return ImportSpecDecision.Reject(ImportRejectionReason.MissingAbsoluteEpisodeNumber, "Episode does not have an absolute episode number and recently aired"); } } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs index e7a62650b..30afe273b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs @@ -22,12 +22,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public SpecificationPriority Priority => SpecificationPriority.Database; - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (downloadClientItem == null) { _logger.Debug("No download client information is available, skipping"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } foreach (var episode in localEpisode.Episodes) @@ -64,17 +64,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (lastImported.Date.After(lastGrabbed.Date)) { _logger.Debug("Episode file previously imported at {0}", lastImported.Date); - return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime()); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeAlreadyImported, "Episode file already imported at {0}", lastImported.Date.ToLocalTime()); } } else { _logger.Debug("Episode file previously imported at {0}", lastImported.Date); - return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime()); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeAlreadyImported, "Episode file already imported at {0}", lastImported.Date.ToLocalTime()); } } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs index 79a94e2f5..0860caf2e 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs @@ -3,7 +3,6 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -29,12 +28,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { _logger.Debug("{0} is in series folder, skipping check", localEpisode.Path); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var episodeTitleRequired = _configService.EpisodeTitleRequired; @@ -42,13 +41,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (episodeTitleRequired == EpisodeTitleRequiredType.Never) { _logger.Debug("Episode titles are never required, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (!_buildFileNames.RequiresEpisodeTitle(localEpisode.Series, localEpisode.Episodes)) { _logger.Debug("File name format does not require episode title, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var episodes = localEpisode.Episodes; @@ -64,7 +63,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications e.AirDateUtc.Value == firstEpisode.AirDateUtc.Value) < 4) { _logger.Debug("Episode title only required for bulk season releases"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } foreach (var episode in episodes) @@ -82,18 +81,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { _logger.Debug("Episode does not have a title and recently aired"); - return Decision.Reject("Episode does not have a title and recently aired"); + return ImportSpecDecision.Reject(ImportRejectionReason.TitleMissing, "Episode does not have a title and recently aired"); } if (title.Equals("TBA")) { _logger.Debug("Episode has a TBA title and recently aired"); - return Decision.Reject("Episode has a TBA title and recently aired"); + return ImportSpecDecision.Reject(ImportRejectionReason.TitleTba, "Episode has a TBA title and recently aired"); } } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 29f74ca6b..1db0df0c6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -4,7 +4,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -23,12 +22,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (_configService.SkipFreeSpaceCheckWhenImporting) { _logger.Debug("Skipping free space check when importing"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } try @@ -36,7 +35,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (localEpisode.ExistingFile) { _logger.Debug("Skipping free space check for existing episode"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var path = Directory.GetParent(localEpisode.Series.Path); @@ -45,13 +44,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (!freeSpace.HasValue) { _logger.Debug("Free space check returned an invalid result for: {0}", path); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (freeSpace < localEpisode.Size + _configService.MinimumFreeSpaceWhenImporting.Megabytes()) { _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localEpisode, localEpisode.Size); - return Decision.Reject("Not enough free space"); + return ImportSpecDecision.Reject(ImportRejectionReason.MinimumFreeSpace, "Not enough free space"); } } catch (DirectoryNotFoundException ex) @@ -63,7 +62,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Error(ex, "Unable to check free disk space while importing. {0}", localEpisode.Path); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 4d13eda6f..47116827b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -1,5 +1,4 @@ using NLog; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -14,20 +13,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.FileEpisodeInfo == null) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (localEpisode.FileEpisodeInfo.FullSeason) { _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 ImportSpecDecision.Reject(ImportRejectionReason.FullSeason, "Single episode file contains all episodes in seasons. Review file name or manually import"); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs index 4a66eeea1..3e7680496 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/HasAudioTrackSpecification.cs @@ -1,5 +1,4 @@ using NLog; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -14,22 +13,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.MediaInfo == null) { _logger.Debug("Failed to get media info from the file, make sure ffprobe is available, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (localEpisode.MediaInfo.AudioStreamCount == 0) { _logger.Debug("No audio tracks found in file"); - return Decision.Reject("No audio tracks detected"); + return ImportSpecDecision.Reject(ImportRejectionReason.NoAudio, "No audio tracks detected"); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index 230190cd5..22d3143d0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -21,11 +20,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _parsingService = parsingService; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var fileInfo = localEpisode.FileEpisodeInfo; @@ -44,13 +43,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (folderInfo == null) { _logger.Debug("No folder ParsedEpisodeInfo, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (fileInfo == null) { _logger.Debug("No file ParsedEpisodeInfo, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var folderEpisodes = _parsingService.GetEpisodes(folderInfo, localEpisode.Series, true); @@ -59,7 +58,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (folderEpisodes.Empty()) { _logger.Debug("No episode numbers in folder ParsedEpisodeInfo, skipping check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var unexpected = fileEpisodes.Where(e => folderEpisodes.All(o => o.Id != e.Id)).ToList(); @@ -70,13 +69,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (unexpected.Count == 1) { - return Decision.Reject("Episode {0} was unexpected considering the {1} folder name", FormatEpisode(unexpected), folderInfo.ReleaseTitle); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeUnexpected, "Episode {0} was unexpected considering the {1} folder name", FormatEpisode(unexpected), folderInfo.ReleaseTitle); } - return Decision.Reject("Episodes {0} were unexpected considering the {1} folder name", FormatEpisode(unexpected), folderInfo.ReleaseTitle); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeUnexpected, "Episodes {0} were unexpected considering the {1} folder name", FormatEpisode(unexpected), folderInfo.ReleaseTitle); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } private string FormatEpisode(List<Episode> episodes) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs index a37762745..52c163bad 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -18,18 +17,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var releaseInfo = localEpisode.Release; if (releaseInfo == null || releaseInfo.EpisodeIds.Empty()) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } var unexpected = localEpisode.Episodes.Where(e => releaseInfo.EpisodeIds.All(o => o != e.Id)).ToList(); @@ -40,13 +39,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (unexpected.Count == 1) { - return Decision.Reject("Episode {0} was not found in the grabbed release: {1}", FormatEpisode(unexpected), releaseInfo.Title); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeNotFoundInRelease, "Episode {0} was not found in the grabbed release: {1}", FormatEpisode(unexpected), releaseInfo.Title); } - return Decision.Reject("Episodes {0} were not found in the grabbed release: {1}", FormatEpisode(unexpected), releaseInfo.Title); + return ImportSpecDecision.Reject(ImportRejectionReason.EpisodeNotFoundInRelease, "Episodes {0} were not found in the grabbed release: {1}", FormatEpisode(unexpected), releaseInfo.Title); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } private string FormatEpisode(List<Episode> episodes) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index 5d748f0f1..d042a7844 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -1,5 +1,4 @@ using NLog; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -18,12 +17,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { _logger.Debug("Existing file, skipping sample check"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } try @@ -32,11 +31,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (sample == DetectSampleResult.Sample) { - return Decision.Reject("Sample"); + return ImportSpecDecision.Reject(ImportRejectionReason.Sample, "Sample"); } else if (sample == DetectSampleResult.Indeterminate) { - return Decision.Reject("Unable to determine if file is a sample"); + return ImportSpecDecision.Reject(ImportRejectionReason.SampleIndeterminate, "Unable to determine if file is a sample"); } } catch (InvalidSeasonException e) @@ -44,7 +43,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Warn(e, "Invalid season detected during sample check"); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index f8097737d..0db81d576 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -4,7 +4,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -23,12 +22,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { _logger.Debug("{0} is in series folder, skipping unpacking check", localEpisode.Path); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) @@ -41,13 +40,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (OsInfo.IsNotWindows) { _logger.Debug("{0} is still being unpacked", localEpisode.Path); - return Decision.Reject("File is still being unpacked"); + return ImportSpecDecision.Reject(ImportRejectionReason.Unpacking, "File is still being unpacked"); } if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) { _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); - return Decision.Reject("File is still being unpacked"); + return ImportSpecDecision.Reject(ImportRejectionReason.Unpacking, "File is still being unpacked"); } } @@ -55,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications } } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs index b645f500c..93178226d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs @@ -19,15 +19,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public RejectionType Type => RejectionType.Permanent; - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (_sameEpisodesSpecification.IsSatisfiedBy(localEpisode.Episodes)) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } _logger.Debug("Episode file on disk contains more episodes than this file contains"); - return Decision.Reject("Episode file on disk contains more episodes than this file contains"); + return ImportSpecDecision.Reject(ImportRejectionReason.ExistingFileHasMoreEpisodes, "Episode file on disk contains more episodes than this file contains"); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs index 8a84b0b86..14ced57e2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs @@ -1,5 +1,4 @@ using NLog; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -14,20 +13,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.FileEpisodeInfo == null) { - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (localEpisode.FileEpisodeInfo.IsSplitEpisode) { _logger.Debug("Single episode split into multiple files"); - return Decision.Reject("Single episode split into multiple files"); + return ImportSpecDecision.Reject(ImportRejectionReason.SplitEpisode, "Single episode split into multiple files"); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs index ede9cee58..a22df4411 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs @@ -1,6 +1,5 @@ using System.Linq; using NLog; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications @@ -14,21 +13,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { if (localEpisode.ExistingFile) { _logger.Debug("Skipping scene numbering check for existing episode"); - return Decision.Accept(); + return ImportSpecDecision.Accept(); } if (localEpisode.Episodes.Any(v => v.UnverifiedSceneNumbering)) { _logger.Debug("This file uses unverified scene numbers, will not auto-import until numbering is confirmed on TheXEM. Skipping {0}", localEpisode.Path); - return Decision.Reject("This show has individual episode mappings on TheXEM but the mapping for this episode has not been confirmed yet by their administrators. TheXEM needs manual input."); + return ImportSpecDecision.Reject(ImportRejectionReason.UnverifiedSceneMapping, "This show has individual episode mappings on TheXEM but the mapping for this episode has not been confirmed yet by their administrators. TheXEM needs manual input."); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 0a6d5a6be..ae824ffb9 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -3,7 +3,6 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -25,7 +24,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; var qualityProfile = localEpisode.Series.QualityProfile.Value; @@ -46,7 +45,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (qualityCompare < 0) { _logger.Debug("This file isn't a quality upgrade for all episodes. Existing quality: {0}. New Quality {1}. Skipping {2}", episodeFile.Quality.Quality, localEpisode.Quality.Quality, localEpisode.Path); - return Decision.Reject("Not an upgrade for existing episode file(s). Existing quality: {0}. New Quality {1}.", episodeFile.Quality.Quality, localEpisode.Quality.Quality); + return ImportSpecDecision.Reject(ImportRejectionReason.NotQualityUpgrade, "Not an upgrade for existing episode file(s). Existing quality: {0}. New Quality {1}.", episodeFile.Quality.Quality, localEpisode.Quality.Quality); } // Same quality, propers/repacks are preferred and it is not a revision update. Reject revision downgrade. @@ -56,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications localEpisode.Quality.Revision.CompareTo(episodeFile.Quality.Revision) < 0) { _logger.Debug("This file isn't a quality revision upgrade for all episodes. Skipping {0}", localEpisode.Path); - return Decision.Reject("Not a quality revision upgrade for existing episode file(s)"); + return ImportSpecDecision.Reject(ImportRejectionReason.NotRevisionUpgrade, "Not a quality revision upgrade for existing episode file(s)"); } var currentFormats = _formatService.ParseCustomFormat(episodeFile); @@ -72,7 +71,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications currentFormats != null ? currentFormats.ConcatToString() : "", currentFormatScore); - return Decision.Reject("Not a Custom Format upgrade for existing episode file(s). New: [{0}] ({1}) do not improve on Existing: [{2}] ({3})", + return ImportSpecDecision.Reject(ImportRejectionReason.NotCustomFormatUpgrade, + "Not a Custom Format upgrade for existing episode file(s). New: [{0}] ({1}) do not improve on Existing: [{2}] ({3})", newFormats != null ? newFormats.ConcatToString() : "", newFormatScore, currentFormats != null ? currentFormats.ConcatToString() : "", @@ -86,7 +86,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications currentFormatScore); } - return Decision.Accept(); + return ImportSpecDecision.Accept(); } } } diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index 2b4ddb899..d9b12f633 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -138,7 +138,7 @@ namespace Sonarr.Api.V3.Indexers TvdbId = releaseInfo.TvdbId, TvRageId = releaseInfo.TvRageId, ImdbId = releaseInfo.ImdbId, - Rejections = model.Rejections.Select(r => r.Reason).ToList(), + Rejections = model.Rejections.Select(r => r.Message).ToList(), PublishDate = releaseInfo.PublishDate, CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index 46ab91a95..7c727fd4c 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -45,7 +45,7 @@ namespace Sonarr.Api.V3.ManualImport item.Episodes = processedItem.Episodes.ToResource(); item.ReleaseType = processedItem.ReleaseType; item.IndexerFlags = processedItem.IndexerFlags; - item.Rejections = processedItem.Rejections; + item.Rejections = processedItem.Rejections.Select(r => r.ToResource()); item.CustomFormats = processedItem.CustomFormats.ToResource(false); item.CustomFormatScore = processedItem.CustomFormatScore; diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 4eb2bbe4b..182fefe22 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -24,6 +23,6 @@ namespace Sonarr.Api.V3.ManualImport public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } public ReleaseType ReleaseType { get; set; } - public IEnumerable<Rejection> Rejections { get; set; } + public IEnumerable<ImportRejectionResource> Rejections { get; set; } } } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index a65e6bdf3..b65da7987 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -3,6 +3,7 @@ using System.Linq; using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -33,7 +34,7 @@ namespace Sonarr.Api.V3.ManualImport public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } public ReleaseType ReleaseType { get; set; } - public IEnumerable<Rejection> Rejections { get; set; } + public IEnumerable<ImportRejectionResource> Rejections { get; set; } } public static class ManualImportResourceMapper @@ -70,7 +71,7 @@ namespace Sonarr.Api.V3.ManualImport DownloadId = model.DownloadId, IndexerFlags = model.IndexerFlags, ReleaseType = model.ReleaseType, - Rejections = model.Rejections + Rejections = model.Rejections.Select(r => r.ToResource()) }; } @@ -79,4 +80,27 @@ namespace Sonarr.Api.V3.ManualImport return models.Select(ToResource).ToList(); } } + + public class ImportRejectionResource + { + public string Reason { get; set; } + public RejectionType Type { get; set; } + } + + public static class ImportRejectionResourceMapper + { + public static ImportRejectionResource ToResource(this ImportRejection rejection) + { + if (rejection == null) + { + return null; + } + + return new ImportRejectionResource + { + Reason = rejection.Message, + Type = rejection.Type + }; + } + } } From 776143cc813ec1b5fa31fbf8667c3ab174b71f5c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Nov 2024 20:13:00 -0800 Subject: [PATCH 674/762] New: Option to treat downloads with non-media extensions as failed Closes #7369 --- .../IndexerTests/SeedConfigProviderFixture.cs | 17 ++--- .../IndexerTests/TestIndexerSettings.cs | 1 + .../Download/CompletedDownloadService.cs | 7 +- .../Download/DownloadProcessingService.cs | 12 ++-- .../Download/RejectedImportService.cs | 50 ++++++++++++++ .../TrackedDownloads/TrackedDownload.cs | 6 ++ .../TrackedDownloadService.cs | 5 ++ .../BroadcastheNet/BroadcastheNetSettings.cs | 4 ++ .../Indexers/CachedIndexerSettingsProvider.cs | 69 +++++++++++++++++++ src/NzbDrone.Core/Indexers/FailDownloads.cs | 12 ++++ .../Indexers/Fanzub/FanzubSettings.cs | 4 ++ .../Indexers/FileList/FileListSettings.cs | 4 ++ .../Indexers/HDBits/HDBitsSettings.cs | 4 ++ .../Indexers/IIndexerSettings.cs | 2 + .../Indexers/IPTorrents/IPTorrentsSettings.cs | 4 ++ .../Indexers/Newznab/NewznabSettings.cs | 4 ++ .../Indexers/Nyaa/NyaaSettings.cs | 4 ++ .../Indexers/SeedConfigProvider.cs | 37 ++-------- .../TorrentRss/TorrentRssIndexerSettings.cs | 4 ++ .../Torrentleech/TorrentleechSettings.cs | 4 ++ .../Indexers/Torznab/TorznabSettings.cs | 6 +- src/NzbDrone.Core/Localization/Core/en.json | 2 + .../DownloadedEpisodesImportService.cs | 5 ++ .../EpisodeImport/ImportRejectionReason.cs | 1 + .../MediaFiles/FileExtensions.cs | 12 +++- 25 files changed, 229 insertions(+), 51 deletions(-) create mode 100644 src/NzbDrone.Core/Download/RejectedImportService.cs create mode 100644 src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs create mode 100644 src/NzbDrone.Core/Indexers/FailDownloads.cs diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs index 9a29e5193..f01d50d55 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using FluentAssertions; using Moq; using NUnit.Framework; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Torznab; using NzbDrone.Core.Parser.Model; @@ -16,9 +16,9 @@ namespace NzbDrone.Core.Test.IndexerTests [Test] public void should_not_return_config_for_non_existent_indexer() { - Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(It.IsAny<int>())) - .Throws(new ModelNotFoundException(typeof(IndexerDefinition), 0)); + Mocker.GetMock<ICachedIndexerSettingsProvider>() + .Setup(v => v.GetSettings(It.IsAny<int>())) + .Returns<CachedIndexerSettings>(null); var result = Subject.GetSeedConfiguration(new RemoteEpisode { @@ -38,11 +38,12 @@ namespace NzbDrone.Core.Test.IndexerTests var settings = new TorznabSettings(); settings.SeedCriteria.SeasonPackSeedTime = 10; - Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(It.IsAny<int>())) - .Returns(new IndexerDefinition + Mocker.GetMock<ICachedIndexerSettingsProvider>() + .Setup(v => v.GetSettings(It.IsAny<int>())) + .Returns(new CachedIndexerSettings { - Settings = settings + FailDownloads = new HashSet<FailDownloads> { FailDownloads.Executables }, + SeedCriteriaSettings = settings.SeedCriteria }); var result = Subject.GetSeedConfiguration(new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs index 948867108..706e87c16 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs @@ -15,5 +15,6 @@ namespace NzbDrone.Core.Test.IndexerTests public string BaseUrl { get; set; } public IEnumerable<int> MultiLanguages { get; set; } + public IEnumerable<int> FailDownloads { get; set; } } } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index a71c6b9cf..ffb7b60be 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.Download private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported; private readonly IEpisodeService _episodeService; private readonly IMediaFileService _mediaFileService; + private readonly IRejectedImportService _rejectedImportService; private readonly Logger _logger; public CompletedDownloadService(IEventAggregator eventAggregator, @@ -46,6 +47,7 @@ namespace NzbDrone.Core.Download ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported, IEpisodeService episodeService, IMediaFileService mediaFileService, + IRejectedImportService rejectedImportService, Logger logger) { _eventAggregator = eventAggregator; @@ -57,6 +59,7 @@ namespace NzbDrone.Core.Download _trackedDownloadAlreadyImported = trackedDownloadAlreadyImported; _episodeService = episodeService; _mediaFileService = mediaFileService; + _rejectedImportService = rejectedImportService; _logger = logger; } @@ -165,10 +168,8 @@ namespace NzbDrone.Core.Download { var firstResult = importResults.First(); - if (firstResult.Result == ImportResultType.Rejected && firstResult.ImportDecision.LocalEpisode == null) + if (_rejectedImportService.Process(trackedDownload, firstResult)) { - trackedDownload.Warn(new TrackedDownloadStatusMessage(firstResult.Errors.First(), new List<string>())); - return; } } diff --git a/src/NzbDrone.Core/Download/DownloadProcessingService.cs b/src/NzbDrone.Core/Download/DownloadProcessingService.cs index 271d1b54a..cbc57c48b 100644 --- a/src/NzbDrone.Core/Download/DownloadProcessingService.cs +++ b/src/NzbDrone.Core/Download/DownloadProcessingService.cs @@ -55,14 +55,18 @@ namespace NzbDrone.Core.Download { try { + // Process completed items followed by failed, this allows failed imports to have + // their state changed and be processed immediately instead of the next execution. + + if (enableCompletedDownloadHandling && trackedDownload.State == TrackedDownloadState.ImportPending) + { + _completedDownloadService.Import(trackedDownload); + } + if (trackedDownload.State == TrackedDownloadState.FailedPending) { _failedDownloadService.ProcessFailed(trackedDownload); } - else if (enableCompletedDownloadHandling && trackedDownload.State == TrackedDownloadState.ImportPending) - { - _completedDownloadService.Import(trackedDownload); - } } catch (Exception e) { diff --git a/src/NzbDrone.Core/Download/RejectedImportService.cs b/src/NzbDrone.Core/Download/RejectedImportService.cs new file mode 100644 index 000000000..2cbb8f523 --- /dev/null +++ b/src/NzbDrone.Core/Download/RejectedImportService.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.MediaFiles.EpisodeImport; + +namespace NzbDrone.Core.Download; + +public interface IRejectedImportService +{ + bool Process(TrackedDownload trackedDownload, ImportResult importResult); +} + +public class RejectedImportService : IRejectedImportService +{ + private readonly ICachedIndexerSettingsProvider _cachedIndexerSettingsProvider; + + public RejectedImportService(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider) + { + _cachedIndexerSettingsProvider = cachedIndexerSettingsProvider; + } + + public bool Process(TrackedDownload trackedDownload, ImportResult importResult) + { + if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode != null) + { + return false; + } + + var indexerSettings = _cachedIndexerSettingsProvider.GetSettings(trackedDownload.RemoteEpisode.Release.IndexerId); + var rejectionReason = importResult.ImportDecision.Rejections.FirstOrDefault()?.Reason; + + if (rejectionReason == ImportRejectionReason.DangerousFile && + indexerSettings.FailDownloads.Contains(FailDownloads.PotentiallyDangerous)) + { + trackedDownload.Fail(); + } + else if (rejectionReason == ImportRejectionReason.ExecutableFile && + indexerSettings.FailDownloads.Contains(FailDownloads.Executables)) + { + trackedDownload.Fail(); + } + else + { + trackedDownload.Warn(new TrackedDownloadStatusMessage(importResult.Errors.First(), new List<string>())); + } + + return true; + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 0a982e7ff..3f0543b03 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -35,6 +35,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads Status = TrackedDownloadStatus.Warning; StatusMessages = statusMessages; } + + public void Fail() + { + Status = TrackedDownloadStatus.Error; + State = TrackedDownloadState.FailedPending; + } } public enum TrackedDownloadState diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index bf5eda8f5..cc54dc1cb 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -166,6 +166,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads { trackedDownload.RemoteEpisode.Release.IndexerFlags = flags; } + + if (downloadHistory != null) + { + trackedDownload.RemoteEpisode.Release.IndexerId = downloadHistory.IndexerId; + } } } diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index af0169502..6c5d2c473 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet BaseUrl = "https://api.broadcasthe.net/"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -48,6 +49,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs b/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs new file mode 100644 index 000000000..f5cb3064c --- /dev/null +++ b/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.Indexers; + +public interface ICachedIndexerSettingsProvider +{ + CachedIndexerSettings GetSettings(int indexerId); +} + +public class CachedIndexerSettingsProvider : ICachedIndexerSettingsProvider, IHandle<ProviderUpdatedEvent<IIndexer>> +{ + private readonly IIndexerFactory _indexerFactory; + private readonly ICached<CachedIndexerSettings> _cache; + + public CachedIndexerSettingsProvider(IIndexerFactory indexerFactory, ICacheManager cacheManager) + { + _indexerFactory = indexerFactory; + _cache = cacheManager.GetRollingCache<CachedIndexerSettings>(GetType(), "settingsByIndexer", TimeSpan.FromHours(1)); + } + + public CachedIndexerSettings GetSettings(int indexerId) + { + if (indexerId == 0) + { + return null; + } + + return _cache.Get(indexerId.ToString(), () => FetchIndexerSettings(indexerId)); + } + + private CachedIndexerSettings FetchIndexerSettings(int indexerId) + { + var indexer = _indexerFactory.Get(indexerId); + var indexerSettings = indexer.Settings as IIndexerSettings; + + if (indexerSettings == null) + { + return null; + } + + var settings = new CachedIndexerSettings + { + FailDownloads = indexerSettings.FailDownloads.Select(f => (FailDownloads)f).ToHashSet() + }; + + if (indexer.Settings is ITorrentIndexerSettings torrentIndexerSettings) + { + settings.SeedCriteriaSettings = torrentIndexerSettings.SeedCriteria; + } + + return settings; + } + + public void Handle(ProviderUpdatedEvent<IIndexer> message) + { + _cache.Clear(); + } +} + +public class CachedIndexerSettings +{ + public HashSet<FailDownloads> FailDownloads { get; set; } + public SeedCriteriaSettings SeedCriteriaSettings { get; set; } +} diff --git a/src/NzbDrone.Core/Indexers/FailDownloads.cs b/src/NzbDrone.Core/Indexers/FailDownloads.cs new file mode 100644 index 000000000..bccb8eeb3 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/FailDownloads.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Indexers; + +public enum FailDownloads +{ + [FieldOption(Label = "Executables")] + Executables = 0, + + [FieldOption(Label = "Potentially Dangerous")] + PotentiallyDangerous = 1 +} diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index fe46ab0dd..2b6368a42 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Core.Indexers.Fanzub { BaseUrl = "http://fanzub.com/rss/"; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl", HelpText = "IndexerSettingsRssUrlHelpText")] @@ -36,6 +37,9 @@ namespace NzbDrone.Core.Indexers.Fanzub [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 9cff94744..576acbe72 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -38,6 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList AnimeCategories = Array.Empty<int>(); MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)] @@ -67,6 +68,9 @@ namespace NzbDrone.Core.Indexers.FileList [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index 8bab1adde..1e2dc0d2d 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Indexers.HDBits Codecs = Array.Empty<int>(); Mediums = Array.Empty<int>(); MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -64,6 +65,9 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(10, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs index 5491b7c52..395066de5 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs @@ -8,5 +8,7 @@ namespace NzbDrone.Core.Indexers string BaseUrl { get; set; } IEnumerable<int> MultiLanguages { get; set; } + + IEnumerable<int> FailDownloads { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index f9db8deec..af6748186 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents { MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerIPTorrentsSettingsFeedUrl", HelpText = "IndexerIPTorrentsSettingsFeedUrlHelpText")] @@ -51,6 +52,9 @@ namespace NzbDrone.Core.Indexers.IPTorrents [FieldDefinition(4, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index 36240529d..07ab7e5cd 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -59,6 +59,7 @@ namespace NzbDrone.Core.Indexers.Newznab Categories = new[] { 5030, 5040 }; AnimeCategories = Enumerable.Empty<int>(); MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "URL")] @@ -86,6 +87,9 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(8, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + // Field 8 is used by TorznabSettings MinimumSeeders // If you need to add another field here, update TorznabSettings as well and this comment diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index d960a77cc..6f8290530 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Indexers.Nyaa AdditionalParameters = "&cats=1_0&filter=1"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -53,6 +54,9 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs index 64840994f..29c995037 100644 --- a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs +++ b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs @@ -1,10 +1,6 @@ using System; -using NzbDrone.Common.Cache; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.Clients; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.Indexers { @@ -14,15 +10,13 @@ namespace NzbDrone.Core.Indexers TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSeason); } - public class SeedConfigProvider : ISeedConfigProvider, IHandle<ProviderUpdatedEvent<IIndexer>> + public class SeedConfigProvider : ISeedConfigProvider { - private readonly IIndexerFactory _indexerFactory; - private readonly ICached<SeedCriteriaSettings> _cache; + private readonly ICachedIndexerSettingsProvider _cachedIndexerSettingsProvider; - public SeedConfigProvider(IIndexerFactory indexerFactory, ICacheManager cacheManager) + public SeedConfigProvider(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider) { - _indexerFactory = indexerFactory; - _cache = cacheManager.GetRollingCache<SeedCriteriaSettings>(GetType(), "criteriaByIndexer", TimeSpan.FromHours(1)); + _cachedIndexerSettingsProvider = cachedIndexerSettingsProvider; } public TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode remoteEpisode) @@ -47,7 +41,8 @@ namespace NzbDrone.Core.Indexers return null; } - var seedCriteria = _cache.Get(indexerId.ToString(), () => FetchSeedCriteria(indexerId)); + var settings = _cachedIndexerSettingsProvider.GetSettings(indexerId); + var seedCriteria = settings?.SeedCriteriaSettings; if (seedCriteria == null) { @@ -67,25 +62,5 @@ namespace NzbDrone.Core.Indexers return seedConfig; } - - private SeedCriteriaSettings FetchSeedCriteria(int indexerId) - { - try - { - var indexer = _indexerFactory.Get(indexerId); - var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; - - return torrentIndexerSettings?.SeedCriteria; - } - catch (ModelNotFoundException) - { - return null; - } - } - - public void Handle(ProviderUpdatedEvent<IIndexer> message) - { - _cache.Clear(); - } } } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index baefcb04b..43c324a18 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss AllowZeroSize = false; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl")] @@ -51,6 +52,9 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index 12415e24a..b2fbdb66f 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech BaseUrl = "http://rss.torrentleech.org"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MultiLanguages = Array.Empty<int>(); + FailDownloads = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -48,6 +49,9 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable<int> FailDownloads { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 1936529a7..35951a54c 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -52,13 +52,13 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(8, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] + [FieldDefinition(9, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(9)] + [FieldDefinition(10)] public SeedCriteriaSettings SeedCriteria { get; set; } = new (); - [FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + [FieldDefinition(11, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 53321e959..5be713c1f 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -998,6 +998,8 @@ "IndexerSettingsCategoriesHelpText": "Drop down list, leave blank to disable standard/daily shows", "IndexerSettingsCookie": "Cookie", "IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.", + "IndexerSettingsFailDownloads": "Fail Downloads", + "IndexerSettingsFailDownloadsHelpText": "While processing completed downloads {appName} will treat selected errors preventing importing as failed downloads.", "IndexerSettingsMinimumSeeders": "Minimum Seeders", "IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.", "IndexerSettingsMultiLanguageRelease": "Multi Languages", diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 98d55064b..093792cd0 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -318,6 +318,11 @@ namespace NzbDrone.Core.MediaFiles { var files = _diskProvider.GetFiles(folder, true); + if (files.Any(file => FileExtensions.DangerousExtensions.Contains(Path.GetExtension(file)))) + { + return RejectionResult(ImportRejectionReason.DangerousFile, "Caution: Found potentially dangerous file"); + } + if (files.Any(file => FileExtensions.ExecutableExtensions.Contains(Path.GetExtension(file)))) { return RejectionResult(ImportRejectionReason.ExecutableFile, "Caution: Found executable file"); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs index f13f5d2a1..b9a282b2d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs @@ -5,6 +5,7 @@ public enum ImportRejectionReason Unknown, FileLocked, UnknownSeries, + DangerousFile, ExecutableFile, ArchiveFile, SeriesFolder, diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index 77d787645..59a9cf3b1 100644 --- a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs @@ -18,19 +18,27 @@ namespace NzbDrone.Core.MediaFiles ".tb2", ".tbz2", ".tgz", - ".zip", + ".zip" + }; + + private static List<string> _dangerousExtensions = new List<string> + { + ".lnk", + ".ps1", + ".vbs", ".zipx" }; private static List<string> _executableExtensions = new List<string> { - ".exe", ".bat", ".cmd", + ".exe", ".sh" }; public static HashSet<string> ArchiveExtensions => new HashSet<string>(_archiveExtensions, StringComparer.OrdinalIgnoreCase); + public static HashSet<string> DangerousExtensions => new HashSet<string>(_dangerousExtensions, StringComparer.OrdinalIgnoreCase); public static HashSet<string> ExecutableExtensions => new HashSet<string>(_executableExtensions, StringComparer.OrdinalIgnoreCase); } } From e039dc45e267cf717e00c0a49ba637012f37e3d7 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Mon, 2 Dec 2024 01:16:36 +0100 Subject: [PATCH 675/762] New: Add Languages to Webhook Notifications Closes #7421 --- src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs | 4 ++++ src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs | 3 +++ src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs index c1a0d2364..aafb92414 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisodeFile.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Notifications.Webhook @@ -20,6 +22,7 @@ namespace NzbDrone.Core.Notifications.Webhook SceneName = episodeFile.SceneName; Size = episodeFile.Size; DateAdded = episodeFile.DateAdded; + Languages = episodeFile.Languages; if (episodeFile.MediaInfo != null) { @@ -36,6 +39,7 @@ namespace NzbDrone.Core.Notifications.Webhook public string SceneName { get; set; } public long Size { get; set; } public DateTime DateAdded { get; set; } + public List<Language> Languages { get; set; } public WebhookEpisodeFileMediaInfo MediaInfo { get; set; } public string SourcePath { get; set; } public string RecycleBinPath { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs index bd825a062..f9eaba4f2 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -21,6 +22,7 @@ namespace NzbDrone.Core.Notifications.Webhook Size = remoteEpisode.Release.Size; CustomFormats = remoteEpisode.CustomFormats?.Select(x => x.Name).ToList(); CustomFormatScore = remoteEpisode.CustomFormatScore; + Languages = remoteEpisode.Languages; } public string Quality { get; set; } @@ -31,5 +33,6 @@ namespace NzbDrone.Core.Notifications.Webhook public long Size { get; set; } public int CustomFormatScore { get; set; } public List<string> CustomFormats { get; set; } + public List<Language> Languages { get; set; } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs index de1da85ad..1a9b51d7e 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Languages; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Webhook @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Notifications.Webhook public List<string> Genres { get; set; } public List<WebhookImage> Images { get; set; } public List<string> Tags { get; set; } + public Language OriginalLanguage { get; set; } public WebhookSeries() { @@ -39,6 +41,7 @@ namespace NzbDrone.Core.Notifications.Webhook Genres = series.Genres; Images = series.Images.Select(i => new WebhookImage(i)).ToList(); Tags = tags; + OriginalLanguage = series.OriginalLanguage; } } } From 4c41a4f368046f73f82306bbd73bec992392938b Mon Sep 17 00:00:00 2001 From: soup <s0up4200@pm.me> Date: Mon, 2 Dec 2024 01:20:08 +0100 Subject: [PATCH 676/762] New: Add config file setting for CGNAT authentication bypass --- .../IPAddressExtensionsFixture.cs | 19 +++++++++++++++++++ .../Extensions/IpAddressExtensions.cs | 6 ++++++ src/NzbDrone.Common/Options/AuthOptions.cs | 1 + .../Configuration/ConfigFileProvider.cs | 3 +++ .../Configuration/ConfigService.cs | 6 ++++++ .../Config/HostConfigResource.cs | 1 + .../Authentication/UiAuthorizationHandler.cs | 9 ++++++--- 7 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs index f7ed71f94..39c71d33d 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs @@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests [TestCase("1.2.3.4")] [TestCase("172.55.0.1")] [TestCase("192.55.0.1")] + [TestCase("100.64.0.1")] + [TestCase("100.127.255.254")] public void should_return_false_for_public_ip_address(string ipAddress) { IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse(); } + + [TestCase("100.64.0.1")] + [TestCase("100.127.255.254")] + [TestCase("100.100.100.100")] + public void should_return_true_for_cgnat_ip_address(string ipAddress) + { + IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue(); + } + + [TestCase("1.2.3.4")] + [TestCase("192.168.5.1")] + [TestCase("100.63.255.255")] + [TestCase("100.128.0.0")] + public void should_return_false_for_non_cgnat_ip_address(string ipAddress) + { + IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs index 7feb431c4..d329df61f 100644 --- a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs @@ -52,5 +52,11 @@ namespace NzbDrone.Common.Extensions return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); } + + public static bool IsCgnatIpAddress(this IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127; + } } } diff --git a/src/NzbDrone.Common/Options/AuthOptions.cs b/src/NzbDrone.Common/Options/AuthOptions.cs index 2b63308d3..64330b68b 100644 --- a/src/NzbDrone.Common/Options/AuthOptions.cs +++ b/src/NzbDrone.Common/Options/AuthOptions.cs @@ -6,4 +6,5 @@ public class AuthOptions public bool? Enabled { get; set; } public string Method { get; set; } public string Required { get; set; } + public bool? TrustCgnatIpAddresses { get; set; } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index bb61499db..3a3b201ea 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -65,6 +65,7 @@ namespace NzbDrone.Core.Configuration string PostgresPassword { get; } string PostgresMainDb { get; } string PostgresLogDb { get; } + bool TrustCgnatIpAddresses { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -475,5 +476,7 @@ namespace NzbDrone.Core.Configuration { SetValue("ApiKey", GenerateApiKey()); } + + public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index f52480e19..d497e4f0c 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -390,6 +390,12 @@ namespace NzbDrone.Core.Configuration public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty); + public bool TrustCgnatIpAddresses + { + get { return GetValueBoolean("TrustCgnatIpAddresses", false); } + set { SetValue("TrustCgnatIpAddresses", value); } + } + private string GetValue(string key) { return GetValue(key, string.Empty); diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs index f03bdce33..af8e8424f 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -45,6 +45,7 @@ namespace Sonarr.Api.V3.Config public string BackupFolder { get; set; } public int BackupInterval { get; set; } public int BackupRetention { get; set; } + public bool TrustCgnatIpAddresses { get; set; } } public static class HostConfigResourceMapper diff --git a/src/Sonarr.Http/Authentication/UiAuthorizationHandler.cs b/src/Sonarr.Http/Authentication/UiAuthorizationHandler.cs index ead8d3885..a763aed75 100644 --- a/src/Sonarr.Http/Authentication/UiAuthorizationHandler.cs +++ b/src/Sonarr.Http/Authentication/UiAuthorizationHandler.cs @@ -27,10 +27,13 @@ namespace NzbDrone.Http.Authentication if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses) { if (context.Resource is HttpContext httpContext && - IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) && - ipAddress.IsLocalAddress()) + IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress)) { - context.Succeed(requirement); + if (ipAddress.IsLocalAddress() || + (_configService.TrustCgnatIpAddresses && ipAddress.IsCgnatIpAddress())) + { + context.Succeed(requirement); + } } } From 8cb58a63d8ec1b290bc57ad2cf1e90809ceebce9 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 1 Dec 2024 08:59:44 -0800 Subject: [PATCH 677/762] Fixed: Don't fail import if symlink target can't be resolved Closes #7431 --- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 07b6775cb..6fcfa94b0 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -190,16 +190,23 @@ namespace NzbDrone.Common.Disk var fi = new FileInfo(path); - // If the file is a symlink, resolve the target path and get the size of the target file. - if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint)) + try { - var targetPath = fi.ResolveLinkTarget(true)?.FullName; - - if (targetPath != null) + // If the file is a symlink, resolve the target path and get the size of the target file. + if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint)) { - fi = new FileInfo(targetPath); + var targetPath = fi.ResolveLinkTarget(true)?.FullName; + + if (targetPath != null) + { + fi = new FileInfo(targetPath); + } } } + catch (IOException ex) + { + Logger.Trace(ex, "Unable to resolve symlink target for {0}", path); + } return fi.Length; } From fb9a5efe051298f1f1c9d722696803db33b2fac1 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 1 Dec 2024 09:00:36 -0800 Subject: [PATCH 678/762] Add return type for series/lookup endpoint Closes #7438 --- src/Sonarr.Api.V3/Series/SeriesLookupController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/Series/SeriesLookupController.cs b/src/Sonarr.Api.V3/Series/SeriesLookupController.cs index a914bb9e6..0103163b6 100644 --- a/src/Sonarr.Api.V3/Series/SeriesLookupController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesLookupController.cs @@ -24,7 +24,7 @@ namespace Sonarr.Api.V3.Series } [HttpGet] - public object Search([FromQuery] string term) + public IEnumerable<SeriesResource> Search([FromQuery] string term) { var tvDbResults = _searchProxy.SearchForNewSeries(term); return MapToResource(tvDbResults); From c62fc9d05bb9e1fe51b454d78e80bd9250e31f89 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 1 Dec 2024 16:20:55 -0800 Subject: [PATCH 679/762] New: Kometa metadata file creation disabled Closes #7400 --- frontend/src/App/State/AppSectionState.ts | 10 ++ frontend/src/App/State/MetadataAppState.ts | 6 + frontend/src/App/State/SettingsAppState.ts | 2 + .../Components/Form/ProviderFieldFormGroup.js | 2 + .../Metadata/Metadata/EditMetadataModal.js | 27 ---- .../Metadata/Metadata/EditMetadataModal.tsx | 36 +++++ .../Metadata/EditMetadataModalConnector.js | 44 ----- .../Metadata/EditMetadataModalContent.css | 5 + .../EditMetadataModalContent.css.d.ts | 7 + .../Metadata/EditMetadataModalContent.js | 105 ------------ .../Metadata/EditMetadataModalContent.tsx | 128 +++++++++++++++ .../EditMetadataModalContentConnector.js | 95 ----------- .../Settings/Metadata/Metadata/Metadata.js | 150 ------------------ .../Settings/Metadata/Metadata/Metadata.tsx | 107 +++++++++++++ .../Settings/Metadata/Metadata/Metadatas.js | 44 ----- .../Settings/Metadata/Metadata/Metadatas.tsx | 52 ++++++ .../src/Settings/Metadata/MetadataSettings.js | 4 +- .../src/Store/Selectors/selectSettings.ts | 1 + frontend/src/typings/DownloadClient.ts | 11 +- frontend/src/typings/Field.ts | 2 + frontend/src/typings/ImportList.ts | 11 +- frontend/src/typings/Indexer.ts | 11 +- frontend/src/typings/Metadata.ts | 7 + frontend/src/typings/Notification.ts | 11 +- frontend/src/typings/Provider.ts | 20 +++ frontend/src/typings/pending.ts | 2 +- .../Consumers/Kometa/KometaMetadata.cs | 94 ++--------- .../Kometa/KometaMetadataSettings.cs | 14 +- .../HealthCheck/Checks/MetadataCheck.cs | 34 ++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + 30 files changed, 445 insertions(+), 599 deletions(-) create mode 100644 frontend/src/App/State/MetadataAppState.ts delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js delete mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.tsx create mode 100644 frontend/src/typings/Metadata.ts create mode 100644 frontend/src/typings/Provider.ts create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index fa55c8e38..4e9dbe7a0 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -63,6 +63,16 @@ export interface AppSectionItemState<T> { item: T; } +export interface AppSectionProviderState<T> + extends AppSectionDeleteState, + AppSectionSaveState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + items: T[]; + pendingChanges: Partial<T>; +} + interface AppSectionState<T> { isFetching: boolean; isPopulated: boolean; diff --git a/frontend/src/App/State/MetadataAppState.ts b/frontend/src/App/State/MetadataAppState.ts new file mode 100644 index 000000000..60d5c434c --- /dev/null +++ b/frontend/src/App/State/MetadataAppState.ts @@ -0,0 +1,6 @@ +import { AppSectionProviderState } from 'App/State/AppSectionState'; +import Metadata from 'typings/Metadata'; + +interface MetadataAppState extends AppSectionProviderState<Metadata> {} + +export default MetadataAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 6299b498d..28d3fc098 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig'; import NamingExample from 'typings/Settings/NamingExample'; import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; +import MetadataAppState from './MetadataAppState'; export interface DownloadClientAppState extends AppSectionState<DownloadClient>, @@ -97,6 +98,7 @@ interface SettingsAppState { indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; + metadata: MetadataAppState; naming: NamingAppState; namingExamples: NamingExamplesAppState; notifications: NotificationAppState; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index a4f13dbd1..f081f5906 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -138,6 +138,8 @@ ProviderFieldFormGroup.propTypes = { type: PropTypes.string.isRequired, advanced: PropTypes.bool.isRequired, hidden: PropTypes.string, + isDisabled: PropTypes.bool, + provider: PropTypes.string, pending: PropTypes.bool.isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired, warnings: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js deleted file mode 100644 index 4b33df528..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.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 EditMetadataModalContentConnector from './EditMetadataModalContentConnector'; - -function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) { - return ( - <Modal - size={sizes.MEDIUM} - isOpen={isOpen} - onModalClose={onModalClose} - > - <EditMetadataModalContentConnector - {...otherProps} - onModalClose={onModalClose} - /> - </Modal> - ); -} - -EditMetadataModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx new file mode 100644 index 000000000..6dd30ca78 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx @@ -0,0 +1,36 @@ +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 EditMetadataModalContent, { + EditMetadataModalContentProps, +} from './EditMetadataModalContent'; + +interface EditMetadataModalProps extends EditMetadataModalContentProps { + isOpen: boolean; +} + +function EditMetadataModal({ + isOpen, + onModalClose, + ...otherProps +}: EditMetadataModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section: 'metadata' })); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}> + <EditMetadataModalContent + {...otherProps} + onModalClose={handleModalClose} + /> + </Modal> + ); +} + +export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js deleted file mode 100644 index 7513bb82c..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditMetadataModal from './EditMetadataModal'; - -function createMapDispatchToProps(dispatch, props) { - const section = 'settings.metadata'; - - return { - dispatchClearPendingChanges() { - dispatch(clearPendingChanges({ section })); - } - }; -} - -class EditMetadataModalConnector extends Component { - // - // Listeners - - onModalClose = () => { - this.props.dispatchClearPendingChanges({ section: 'metadata' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - <EditMetadataModal - {...this.props} - onModalClose={this.onModalClose} - /> - ); - } -} - -EditMetadataModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css new file mode 100644 index 000000000..7393b9c35 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css @@ -0,0 +1,5 @@ +.message { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts new file mode 100644 index 000000000..65c237dff --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'message': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js deleted file mode 100644 index 221c6bcaf..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js +++ /dev/null @@ -1,105 +0,0 @@ -import PropTypes from 'prop-types'; -import React 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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -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 } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function EditMetadataModalContent(props) { - const { - advancedSettings, - isSaving, - saveError, - item, - onInputChange, - onFieldChange, - onModalClose, - onSavePress, - ...otherProps - } = props; - - const { - name, - enable, - fields - } = item; - - return ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('EditMetadata', { metadataType: name.value })} - </ModalHeader> - - <ModalBody> - <Form {...otherProps}> - <FormGroup> - <FormLabel>{translate('Enable')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="enable" - helpText={translate('EnableMetadataHelpText')} - {...enable} - onChange={onInputChange} - /> - </FormGroup> - - { - fields.map((field) => { - return ( - <ProviderFieldFormGroup - key={field.name} - advancedSettings={advancedSettings} - provider="metadata" - {...field} - isDisabled={!enable.value} - onChange={onFieldChange} - /> - ); - }) - } - - </Form> - </ModalBody> - - <ModalFooter> - <Button - onPress={onModalClose} - > - {translate('Cancel')} - </Button> - - <SpinnerErrorButton - isSpinning={isSaving} - error={saveError} - onPress={onSavePress} - > - {translate('Save')} - </SpinnerErrorButton> - </ModalFooter> - </ModalContent> - ); -} - -EditMetadataModalContent.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onFieldChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onDeleteMetadataPress: PropTypes.func -}; - -export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx new file mode 100644 index 000000000..997a4c39c --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx @@ -0,0 +1,128 @@ +import React, { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 } from 'Helpers/Props'; +import { + saveMetadata, + setMetadataFieldValue, + setMetadataValue, +} from 'Store/Actions/settingsActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditMetadataModalContent.css'; + +export interface EditMetadataModalContentProps { + id: number; + advancedSettings: boolean; + onModalClose: () => void; +} + +function EditMetadataModalContent({ + id, + advancedSettings, + onModalClose, +}: EditMetadataModalContentProps) { + const dispatch = useDispatch(); + + const { isSaving, saveError, pendingChanges, items } = useSelector( + (state: AppState) => state.settings.metadata + ); + + const { settings, ...otherSettings } = useMemo(() => { + const item = items.find((item) => item.id === id)!; + + return selectSettings(item, pendingChanges, saveError); + }, [id, items, pendingChanges, saveError]); + + const { name, enable, fields, message } = settings; + + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error not typed + dispatch(setMetadataValue({ name, value })); + }, + [dispatch] + ); + + const handleFieldChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error not typed + dispatch(setMetadataFieldValue({ name, value })); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch(saveMetadata({ id })); + }, [id, dispatch]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader> + {translate('EditMetadata', { metadataType: name.value })} + </ModalHeader> + + <ModalBody> + <Form {...otherSettings}> + {message ? ( + <Alert className={styles.message} kind={message.value.type}> + {message.value.message} + </Alert> + ) : null} + + <FormGroup> + <FormLabel>{translate('Enable')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="enable" + helpText={translate('EnableMetadataHelpText')} + {...enable} + onChange={handleInputChange} + /> + </FormGroup> + + {fields.map((field) => { + return ( + <ProviderFieldFormGroup + key={field.name} + advancedSettings={advancedSettings} + provider="metadata" + {...field} + isDisabled={!enable.value} + onChange={handleFieldChange} + /> + ); + })} + </Form> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Cancel')}</Button> + + <SpinnerErrorButton + isSpinning={isSaving} + error={saveError} + onPress={handleSavePress} + > + {translate('Save')} + </SpinnerErrorButton> + </ModalFooter> + </ModalContent> + ); +} + +export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js deleted file mode 100644 index 62dae94f6..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js +++ /dev/null @@ -1,95 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveMetadata, setMetadataFieldValue, setMetadataValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditMetadataModalContent from './EditMetadataModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - (state, { id }) => id, - (state) => state.settings.metadata, - (advancedSettings, id, metadata) => { - const { - isSaving, - saveError, - pendingChanges, - items - } = metadata; - - const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError); - - return { - advancedSettings, - id, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - setMetadataValue, - setMetadataFieldValue, - saveMetadata -}; - -class EditMetadataModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setMetadataValue({ name, value }); - }; - - onFieldChange = ({ name, value }) => { - this.props.setMetadataFieldValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveMetadata({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - <EditMetadataModalContent - {...this.props} - onSavePress={this.onSavePress} - onInputChange={this.onInputChange} - onFieldChange={this.onFieldChange} - /> - ); - } -} - -EditMetadataModalContentConnector.propTypes = { - id: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setMetadataValue: PropTypes.func.isRequired, - setMetadataFieldValue: PropTypes.func.isRequired, - saveMetadata: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.js b/frontend/src/Settings/Metadata/Metadata/Metadata.js deleted file mode 100644 index ffb0ab967..000000000 --- a/frontend/src/Settings/Metadata/Metadata/Metadata.js +++ /dev/null @@ -1,150 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditMetadataModalConnector from './EditMetadataModalConnector'; -import styles from './Metadata.css'; - -class Metadata extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditMetadataModalOpen: false - }; - } - - // - // Listeners - - onEditMetadataPress = () => { - this.setState({ isEditMetadataModalOpen: true }); - }; - - onEditMetadataModalClose = () => { - this.setState({ isEditMetadataModalOpen: false }); - }; - - // - // Render - - render() { - const { - id, - name, - enable, - fields - } = this.props; - - const metadataFields = []; - const imageFields = []; - - fields.forEach((field) => { - if (field.section === 'metadata') { - metadataFields.push(field); - } else { - imageFields.push(field); - } - }); - - return ( - <Card - className={styles.metadata} - overlayContent={true} - onPress={this.onEditMetadataPress} - > - <div className={styles.name}> - {name} - </div> - - <div> - { - enable ? - <Label kind={kinds.SUCCESS}> - {translate('Enabled')} - </Label> : - <Label - kind={kinds.DISABLED} - outline={true} - > - {translate('Disabled')} - </Label> - } - </div> - - { - enable && !!metadataFields.length && - <div> - <div className={styles.section}> - {translate('Metadata')} - </div> - - { - metadataFields.map((field) => { - if (!field.value) { - return null; - } - - return ( - <Label - key={field.label} - kind={kinds.SUCCESS} - > - {field.label} - </Label> - ); - }) - } - </div> - } - - { - enable && !!imageFields.length && - <div> - <div className={styles.section}> - {translate('Images')} - </div> - - { - imageFields.map((field) => { - if (!field.value) { - return null; - } - - return ( - <Label - key={field.label} - kind={kinds.SUCCESS} - > - {field.label} - </Label> - ); - }) - } - </div> - } - - <EditMetadataModalConnector - id={id} - isOpen={this.state.isEditMetadataModalOpen} - onModalClose={this.onEditMetadataModalClose} - /> - </Card> - ); - } -} - -Metadata.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - enable: PropTypes.bool.isRequired, - fields: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.tsx b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx new file mode 100644 index 000000000..52797218d --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx @@ -0,0 +1,107 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import { kinds } from 'Helpers/Props'; +import Field from 'typings/Field'; +import translate from 'Utilities/String/translate'; +import EditMetadataModal from './EditMetadataModal'; +import styles from './Metadata.css'; + +interface MetadataProps { + id: number; + name: string; + enable: boolean; + fields: Field[]; +} + +function Metadata({ id, name, enable, fields }: MetadataProps) { + const [isEditMetadataModalOpen, setIsEditMetadataModalOpen] = useState(false); + + const { metadataFields, imageFields } = useMemo(() => { + return fields.reduce<{ metadataFields: Field[]; imageFields: Field[] }>( + (acc, field) => { + if (field.section === 'metadata') { + acc.metadataFields.push(field); + } else { + acc.imageFields.push(field); + } + + return acc; + }, + { metadataFields: [], imageFields: [] } + ); + }, [fields]); + + const handleOpenPress = useCallback(() => { + setIsEditMetadataModalOpen(true); + }, []); + + const handleModalClose = useCallback(() => { + setIsEditMetadataModalOpen(false); + }, []); + + return ( + <Card + className={styles.metadata} + overlayContent={true} + onPress={handleOpenPress} + > + <div className={styles.name}>{name}</div> + + <div> + {enable ? ( + <Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label> + ) : ( + <Label kind={kinds.DISABLED} outline={true}> + {translate('Disabled')} + </Label> + )} + </div> + + {enable && metadataFields.length ? ( + <div> + <div className={styles.section}>{translate('Metadata')}</div> + + {metadataFields.map((field) => { + if (!field.value) { + return null; + } + + return ( + <Label key={field.label} kind={kinds.SUCCESS}> + {field.label} + </Label> + ); + })} + </div> + ) : null} + + {enable && imageFields.length ? ( + <div> + <div className={styles.section}>{translate('Images')}</div> + + {imageFields.map((field) => { + if (!field.value) { + return null; + } + + return ( + <Label key={field.label} kind={kinds.SUCCESS}> + {field.label} + </Label> + ); + })} + </div> + ) : null} + + <EditMetadataModal + advancedSettings={false} + id={id} + isOpen={isEditMetadataModalOpen} + onModalClose={handleModalClose} + /> + </Card> + ); +} + +export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js deleted file mode 100644 index a52275bcc..000000000 --- a/frontend/src/Settings/Metadata/Metadata/Metadatas.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import translate from 'Utilities/String/translate'; -import Metadata from './Metadata'; -import styles from './Metadatas.css'; - -function Metadatas(props) { - const { - items, - ...otherProps - } = props; - - return ( - <FieldSet legend={translate('Metadata')}> - <PageSectionContent - errorMessage={translate('MetadataLoadError')} - {...otherProps} - > - <div className={styles.metadatas}> - { - items.map((item) => { - return ( - <Metadata - key={item.id} - {...item} - /> - ); - }) - } - </div> - </PageSectionContent> - </FieldSet> - ); -} - -Metadatas.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Metadatas; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx b/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx new file mode 100644 index 000000000..befe207d8 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import MetadataAppState from 'App/State/MetadataAppState'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import { fetchMetadata } from 'Store/Actions/settingsActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import MetadataType from 'typings/Metadata'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import Metadata from './Metadata'; +import styles from './Metadatas.css'; + +function createMetadatasSelector() { + return createSelector( + createSortedSectionSelector<MetadataType>( + 'settings.metadata', + sortByProp('name') + ), + (metadata: MetadataAppState) => metadata + ); +} + +function Metadatas() { + const dispatch = useDispatch(); + const { isFetching, error, items, ...otherProps } = useSelector( + createMetadatasSelector() + ); + + useEffect(() => { + dispatch(fetchMetadata()); + }, [dispatch]); + + return ( + <FieldSet legend={translate('Metadata')}> + <PageSectionContent + isFetching={isFetching} + errorMessage={translate('MetadataLoadError')} + {...otherProps} + > + <div className={styles.metadatas}> + {items.map((item) => { + return <Metadata key={item.id} {...item} />; + })} + </div> + </PageSectionContent> + </FieldSet> + ); +} + +export default Metadatas; diff --git a/frontend/src/Settings/Metadata/MetadataSettings.js b/frontend/src/Settings/Metadata/MetadataSettings.js index 5c9f9ea82..143a05956 100644 --- a/frontend/src/Settings/Metadata/MetadataSettings.js +++ b/frontend/src/Settings/Metadata/MetadataSettings.js @@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import MetadatasConnector from './Metadata/MetadatasConnector'; +import Metadatas from './Metadata/Metadatas'; function MetadataSettings() { return ( @@ -13,7 +13,7 @@ function MetadataSettings() { /> <PageContentBody> - <MetadatasConnector /> + <Metadatas /> </PageContentBody> </PageContent> ); diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts index 2fb229e75..75665d73b 100644 --- a/frontend/src/Store/Selectors/selectSettings.ts +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -100,6 +100,7 @@ function selectSettings<T extends ModelBaseSetting>( const setting: Pending<T> = { value: item[key], + pending: false, errors: getFailures(errors, key), warnings: getFailures(warnings, key), }; diff --git a/frontend/src/typings/DownloadClient.ts b/frontend/src/typings/DownloadClient.ts index 547b8c620..417d74b0b 100644 --- a/frontend/src/typings/DownloadClient.ts +++ b/frontend/src/typings/DownloadClient.ts @@ -1,20 +1,13 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; export type Protocol = 'torrent' | 'usenet' | 'unknown'; -interface DownloadClient extends ModelBase { +interface DownloadClient extends Provider { enable: boolean; protocol: Protocol; priority: number; removeCompletedDownloads: boolean; removeFailedDownloads: boolean; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Field.ts b/frontend/src/typings/Field.ts index 24a0b35ac..404c436ef 100644 --- a/frontend/src/typings/Field.ts +++ b/frontend/src/typings/Field.ts @@ -13,6 +13,8 @@ interface Field { name: string; label: string; value: boolean | number | string | number[]; + section: string; + hidden: 'hidden' | 'hiddenIfNotSet' | 'visible'; type: string; advanced: boolean; privacy: string; diff --git a/frontend/src/typings/ImportList.ts b/frontend/src/typings/ImportList.ts index a7aa48f26..7e596b25d 100644 --- a/frontend/src/typings/ImportList.ts +++ b/frontend/src/typings/ImportList.ts @@ -1,17 +1,10 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface ImportList extends ModelBase { +interface ImportList extends Provider { enable: boolean; enableAutomaticAdd: boolean; qualityProfileId: number; rootFolderPath: string; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Indexer.ts b/frontend/src/typings/Indexer.ts index dbfed94a8..ea38651f4 100644 --- a/frontend/src/typings/Indexer.ts +++ b/frontend/src/typings/Indexer.ts @@ -1,18 +1,11 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface Indexer extends ModelBase { +interface Indexer extends Provider { enableRss: boolean; enableAutomaticSearch: boolean; enableInteractiveSearch: boolean; protocol: string; priority: number; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Metadata.ts b/frontend/src/typings/Metadata.ts new file mode 100644 index 000000000..b7d0cfb71 --- /dev/null +++ b/frontend/src/typings/Metadata.ts @@ -0,0 +1,7 @@ +import Provider from './Provider'; + +interface Metadata extends Provider { + enable: boolean; +} + +export default Metadata; diff --git a/frontend/src/typings/Notification.ts b/frontend/src/typings/Notification.ts index 3aa3a2d48..12057015b 100644 --- a/frontend/src/typings/Notification.ts +++ b/frontend/src/typings/Notification.ts @@ -1,14 +1,7 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface Notification extends ModelBase { +interface Notification extends Provider { enable: boolean; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Provider.ts b/frontend/src/typings/Provider.ts new file mode 100644 index 000000000..e9eabba0b --- /dev/null +++ b/frontend/src/typings/Provider.ts @@ -0,0 +1,20 @@ +import ModelBase from 'App/ModelBase'; +import { Kind } from 'Helpers/Props/kinds'; +import Field from './Field'; + +export interface ProviderMessage { + message: string; + type: Extract<Kind, 'info' | 'error' | 'warning'>; +} + +interface Provider extends ModelBase { + name: string; + fields: Field[]; + implementationName: string; + implementation: string; + configContract: string; + infoLink: string; + message: ProviderMessage; +} + +export default Provider; diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index af0dd95e1..480c35623 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -33,7 +33,7 @@ export interface Pending<T> { value: T; errors: Failure[]; warnings: Failure[]; - pending?: boolean; + pending: boolean; previousValue?: T; } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs index d994cef35..3386fa021 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,21 +5,21 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa { public class KometaMetadata : MetadataBase<KometaMetadataSettings> { + private readonly ILocalizationService _localizationService; private readonly Logger _logger; - private readonly IMapCoversToLocal _mediaCoverService; - public KometaMetadata(IMapCoversToLocal mediaCoverService, - Logger logger) + public KometaMetadata(ILocalizationService localizationService, Logger logger) { - _mediaCoverService = mediaCoverService; + _localizationService = localizationService; _logger = logger; } @@ -30,6 +29,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public override string Name => "Kometa"; + public override ProviderMessage Message => new (_localizationService.GetLocalizedString("MetadataKometaDeprecated"), ProviderMessageType.Warning); + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) { if (metadataFile.Type == MetadataType.EpisodeImage) @@ -104,92 +105,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public override List<ImageFileResult> SeriesImages(Series series) { - if (!Settings.SeriesImages) - { - return new List<ImageFileResult>(); - } - - return ProcessSeriesImages(series).ToList(); + return new List<ImageFileResult>(); } public override List<ImageFileResult> SeasonImages(Series series, Season season) { - if (!Settings.SeasonImages) - { - return new List<ImageFileResult>(); - } - - return ProcessSeasonImages(series, season).ToList(); + return new List<ImageFileResult>(); } public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) { - if (!Settings.EpisodeImages) - { - return new List<ImageFileResult>(); - } - - try - { - var firstEpisode = episodeFile.Episodes.Value.FirstOrDefault(); - - if (firstEpisode == null) - { - _logger.Debug("Episode file has no associated episodes, potentially a duplicate file"); - return new List<ImageFileResult>(); - } - - var screenshot = firstEpisode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - - if (screenshot == null) - { - _logger.Debug("Episode screenshot not available"); - return new List<ImageFileResult>(); - } - - return new List<ImageFileResult> - { - new ImageFileResult(GetEpisodeImageFilename(series, episodeFile), screenshot.RemoteUrl) - }; - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); - - return new List<ImageFileResult>(); - } - } - - private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) - { - foreach (var image in series.Images) - { - if (image.CoverType == MediaCoverTypes.Poster) - { - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); - var destination = image.CoverType + Path.GetExtension(source); - - yield return new ImageFileResult(destination, source); - } - } - } - - private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) - { - foreach (var image in season.Images) - { - if (image.CoverType == MediaCoverTypes.Poster) - { - var filename = string.Format("Season{0:00}.jpg", season.SeasonNumber); - - if (season.SeasonNumber == 0) - { - filename = "Season00.jpg"; - } - - yield return new ImageFileResult(filename, image.RemoteUrl); - } - } + return new List<ImageFileResult>(); } private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs index 8b84954f6..1a2011f6b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs @@ -15,19 +15,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public KometaMetadataSettings() { - SeriesImages = true; - SeasonImages = true; - EpisodeImages = true; + Deprecated = true; } - [FieldDefinition(0, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Poster.jpg")] - public bool SeriesImages { get; set; } - - [FieldDefinition(1, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##.jpg")] - public bool SeasonImages { get; set; } - - [FieldDefinition(2, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "S##E##.jpg")] - public bool EpisodeImages { get; set; } + [FieldDefinition(0, Label = "MetadataKometaDeprecatedSetting", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, Hidden = HiddenType.Hidden)] + public bool Deprecated { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs new file mode 100644 index 000000000..48fa924d9 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs @@ -0,0 +1,34 @@ +using System.Linq; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Kometa; +using NzbDrone.Core.Localization; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ProviderUpdatedEvent<IMetadata>))] + public class MetadataCheck : HealthCheckBase + { + private readonly IMetadataFactory _metadataFactory; + + public MetadataCheck(IMetadataFactory metadataFactory, ILocalizationService localizationService) + : base(localizationService) + { + _metadataFactory = metadataFactory; + } + + public override HealthCheck Check() + { + var enabled = _metadataFactory.Enabled(); + + if (enabled.Any(m => m.Definition.Implementation == nameof(KometaMetadata))) + { + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + $"{_localizationService.GetLocalizedString("MetadataKometaDeprecated")}"); + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5be713c1f..823e8356f 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1163,6 +1163,8 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataKometaDeprecated": "Kometa files will no longer be created, support will be removed completely in v5", + "MetadataKometaDeprecatedSetting": "Deprecated", "MetadataPlexSettingsEpisodeMappings": "Episode Mappings", "MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file", "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File", From ed536a85ad5f2062bf6f01f80efddb19fa935f63 Mon Sep 17 00:00:00 2001 From: Gylesie <86306812+Gylesie@users.noreply.github.com> Date: Mon, 2 Dec 2024 01:22:04 +0100 Subject: [PATCH 680/762] Remove unnecessary heap allocations in local IP check --- src/NzbDrone.Common/Extensions/IpAddressExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs index d329df61f..cbc1f5f83 100644 --- a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs @@ -39,18 +39,18 @@ namespace NzbDrone.Common.Extensions private static bool IsLocalIPv4(byte[] ipv4Bytes) { // Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16) - bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; + var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; // Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) - bool IsClassA() => ipv4Bytes[0] == 10; + var isClassA = ipv4Bytes[0] == 10; // Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12) - bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; + var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; // Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16) - bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; + var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; - return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); + return isLinkLocal || isClassA || isClassC || isClassB; } public static bool IsCgnatIpAddress(this IPAddress ipAddress) From 32f66922e79da15df6083dae2ae869a3ab054400 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 2 Dec 2024 00:22:58 +0000 Subject: [PATCH 681/762] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 61 +++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 95af4db2a..6ac128ffd 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -7049,7 +7049,33 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + } + } } } } @@ -9084,6 +9110,9 @@ "backupRetention": { "type": "integer", "format": "int32" + }, + "trustCgnatIpAddresses": { + "type": "boolean" } }, "additionalProperties": false @@ -9354,6 +9383,19 @@ ], "type": "string" }, + "ImportRejectionResource": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/RejectionType" + } + }, + "additionalProperties": false + }, "IndexerBulkResource": { "type": "object", "properties": { @@ -9810,7 +9852,7 @@ "rejections": { "type": "array", "items": { - "$ref": "#/components/schemas/Rejection" + "$ref": "#/components/schemas/ImportRejectionResource" }, "nullable": true } @@ -9907,7 +9949,7 @@ "rejections": { "type": "array", "items": { - "$ref": "#/components/schemas/Rejection" + "$ref": "#/components/schemas/ImportRejectionResource" }, "nullable": true } @@ -11035,19 +11077,6 @@ }, "additionalProperties": false }, - "Rejection": { - "type": "object", - "properties": { - "reason": { - "type": "string", - "nullable": true - }, - "type": { - "$ref": "#/components/schemas/RejectionType" - } - }, - "additionalProperties": false - }, "RejectionType": { "enum": [ "permanent", From c38debab1b4c405aa03ea0acfd25e22e3cfd3f06 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 2 Dec 2024 00:16:05 +0000 Subject: [PATCH 682/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Ardenet <1213193613@qq.com> Co-authored-by: mryx007 <mryx@mail.de> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 1065 ++++++++++++++++- .../Localization/Core/zh_CN.json | 4 +- 2 files changed, 1058 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 8a516afc3..02e3a38fe 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -205,7 +205,7 @@ "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", - "AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.", + "AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss übereinstimmen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht ein einziges {implementationName}-Match aus.", "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", @@ -314,7 +314,7 @@ "DeleteIndexerMessageText": "Bist du sicher, dass du den Indexer '{name}' wirklich löschen willst?", "DeleteQualityProfile": "Qualitätsprofil löschen", "DeleteReleaseProfile": "Release-Profil löschen", - "DeleteReleaseProfileMessageText": "Sind Sie sicher, dass Sie dieses Release-Profil „{name}“ löschen möchten?", + "DeleteReleaseProfileMessageText": "Bist du sicher, dass du das Release-Profil '{name}' löschen möchtest?", "DeleteRemotePathMapping": "Remote-Pfad-Zuordnung löschen", "DeleteSelectedDownloadClientsMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte Download-Clients löschen möchten?", "DeleteSelectedEpisodeFiles": "Ausgewählte Episodendateien löschen", @@ -365,7 +365,7 @@ "DownloadClientFreeboxAuthenticationError": "Die Authentifizierung bei der Freebox-API ist fehlgeschlagen. Grund: {errorDescription}", "DownloadClientFreeboxNotLoggedIn": "Nicht eingeloggt", "DownloadClientFreeboxSettingsApiUrl": "API-URL", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Definieren Sie die Freebox-API-Basis-URL mit der API-Version, z. B. „{url}“, standardmäßig ist „{defaultApiUrl}“.", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Definiere die Freebox-API-Basis-URL mit der API-Version, z. B. '{url}', standardmäßig '{defaultApiUrl}'.", "DownloadClientFreeboxSettingsAppId": "App-ID", "DownloadClientFreeboxSettingsAppIdHelpText": "App-ID, die beim Erstellen des Zugriffs auf die Freebox-API angegeben wird (z. B. „app_id“)", "DownloadClientFreeboxSettingsAppToken": "App-Token", @@ -403,7 +403,7 @@ "DownloadClientQbittorrentValidationQueueingNotEnabled": "Warteschlangen nicht aktiviert", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrent Warteschlange ist in Ihren qBittorrent-Einstellungen nicht aktiviert. Aktivieren Sie es in qBittorrent oder wählen Sie „Letzte“ als Priorität.", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent ist so konfiguriert, dass Torrents entfernt werden, wenn sie ihr Share-Ratio-Limit erreichen", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} kann die Behandlung abgeschlossener Downloads nicht wie konfiguriert durchführen. Sie können dies in qBittorrent beheben („Extras -> Optionen...“ im Menü), indem Sie „Optionen -> BitTorrent -> Freigabeverhältnisbegrenzung“ von „Entfernen“ in „Pause“ ändern.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} kann das Handling abgeschlossener Downloads wie konfiguriert nicht durchführen. Du kannst das in qBittorrent beheben ('Werkzeuge -> Optionen...' im Menü), indem du 'Optionen -> BitTorrent -> Share Ratio Limiting' von 'Entfernen' auf 'Pausieren' änderst.", "DownloadClientRTorrentSettingsAddStopped": "Hinzufügen gestoppt", "DownloadClientRTorrentSettingsAddStoppedHelpText": "Durch die Aktivierung werden Torrents und Magnete im gestoppten Zustand zu rTorrent hinzugefügt. Dadurch können Magnetdateien beschädigt werden.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden", @@ -510,7 +510,7 @@ "UnselectAll": "Alle abwählen", "UnsavedChanges": "Nicht gespeicherte Änderungen", "UpdateAutomaticallyHelpText": "Updates automatisch herunterladen und installieren. Sie können weiterhin über System: Updates installieren", - "UpdateAvailableHealthCheckMessage": "Neues Update ist verfügbar", + "UpdateAvailableHealthCheckMessage": "Ein neues Update ist verfügbar: {version}", "UpdateMechanismHelpText": "Verwenden Sie den integrierten Updater von {appName} oder ein Skript", "UpdateUiNotWritableHealthCheckMessage": "Das Update kann nicht installiert werden, da der Benutzeroberflächenordner „{uiFolder}“ für den Benutzer „{userName}“ nicht beschreibbar ist.", "Updates": "Aktualisierung", @@ -673,7 +673,7 @@ "DailyEpisodeTypeDescription": "Täglich oder seltener veröffentlichte Episoden mit Jahr-Monat-Tag (04.08.2023)", "DelayProfileSeriesTagsHelpText": "Gilt für Serien mit mindestens einem passenden Tag", "DeleteEmptySeriesFoldersHelpText": "Löschen Sie leere Serien- und Staffelordner während des Festplattenscans und beim Löschen von Episodendateien", - "DeletedReasonManual": "Die Datei wurde über die Benutzeroberfläche gelöscht", + "DeletedReasonManual": "Datei wurde mit {appName} gelöscht, entweder manuell oder durch ein anderes Tool über die API", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} konnte (eine) Episode(n) nicht importieren. Weitere Informationen finden Sie in Ihren Protokollen.", "Discord": "Discord", "Restart": "Neu starten", @@ -833,7 +833,7 @@ "DownloadClientValidationSslConnectFailure": "Verbindung über SSL nicht möglich", "ReleaseProfilesLoadError": "Release-Profile können nicht geladen werden", "DownloadClientDelugeSettingsDirectory": "Download Verzeichnis", - "DownloadClientDelugeSettingsDirectoryCompleted": "Verschieben, wenn Verzeichnis abgeschlossen", + "DownloadClientDelugeSettingsDirectoryCompleted": "Verschieben, wenn abgeschlossen Verzeichnis", "DownloadClientSettings": "Downloader Einstellungen", "IgnoreDownloadHint": "Hält {appName} von der weiteren Verarbeitung dieses Downloads ab", "ClearBlocklist": "Sperrliste leeren", @@ -1014,7 +1014,7 @@ "HardlinkCopyFiles": "Hardlink/Dateien kopieren", "ICalShowAsAllDayEvents": "Als ganztägige Ereignisse anzeigen", "IRC": "IRC", - "ImportCustomFormat": "Import Custom Format", + "ImportCustomFormat": "Benutzerdefiniertes Format importieren", "DownloadClientQbittorrentSettingsContentLayout": "Inhaltslayout", "DownloadClientValidationUnableToConnectDetail": "Bitte überprüfe den Hostnamen und den Port.", "Duplicate": "Duplizieren", @@ -1091,5 +1091,1052 @@ "ExtraFileExtensionsHelpTextsExamples": "Beispiele: '.sub, .nfo' oder 'sub,nfo'", "FolderNameTokens": "Ordnernamen Token", "GeneralSettings": "Allgemeine Einstellungen", - "ICalFeedHelpText": "Kopiere diese URL in deine(n) Client(s) oder klicke zum Abonnieren, wenn dein Browser webcal unterstützt" + "ICalFeedHelpText": "Kopiere diese URL in deine(n) Client(s) oder klicke zum Abonnieren, wenn dein Browser webcal unterstützt", + "IndexerSettingsMultiLanguageRelease": "Mehrsprachige Releases", + "IndexerStatusUnavailableHealthCheckMessage": "Indexer nicht verfügbar aufgrund von Fehlern: {indexerNames}", + "Indexers": "Indexer", + "InteractiveSearchSeason": "Interaktive Suche für alle Episoden dieser Staffel", + "LabelIsRequired": "Label ist erforderlich", + "ListExclusionsLoadError": "Kann Ausschlüsse der Liste nicht laden", + "MetadataSourceSettingsSeriesSummary": "Informationen, woher {appName} Serien- und Episodeninformationen erhält", + "MinimumFreeSpace": "Mindestfreier Speicherplatz", + "MonitorFirstSeason": "Erste Staffel", + "MonitorSpecialEpisodes": "Specials überwachen", + "NotificationTriggers": "Benachrichtigungs-Auslöser", + "NotificationsEmailSettingsUseEncryption": "Verschlüsselung verwenden", + "NotificationsSynologyValidationInvalidOs": "Muss ein Synology sein", + "OneSeason": "1 Staffel", + "OutputPath": "Ausgabe-Pfad", + "ParseModalErrorParsing": "Fehler beim Parsen, bitte versuche es erneut.", + "PendingChangesMessage": "Du hast ungespeicherte Änderungen, bist du sicher, dass du diese Seite verlassen möchtest?", + "PosterSize": "Postergröße", + "Presets": "Voreinstellungen", + "RemotePathMappingFileRemovedHealthCheckMessage": "Datei {path} wurde mitten im Verarbeitungsprozess entfernt.", + "RemotePathMappingLocalPathHelpText": "Pfad, den {appName} verwenden soll, um auf den Remote-Pfad lokal zuzugreifen.", + "RemoveQueueItemConfirmation": "Bist du sicher, dass du '{sourceTitle}' aus der Warteschlange entfernen möchtest?", + "RssSync": "RSS-Sync", + "SearchIsNotSupportedWithThisIndexer": "Suche wird von diesem Indexer nicht unterstützt", + "SearchMonitored": "Suche überwachte Episoden", + "ReleaseType": "Release-Typ", + "RemoveCompletedDownloadsHelpText": "Entferne importierte Downloads aus der Download-Client-Historie", + "RemoveFromQueue": "Aus der Warteschlange entfernen", + "RemoveQueueItem": "Entfernen - {sourceTitle}", + "SeriesType": "Serientyp", + "SubtitleLanguages": "Untertitelsprache", + "SeriesIsUnmonitored": "Serie ist nicht überwacht", + "ShowPath": "Pfad anzeigen", + "CutoffUnmetLoadError": "Fehler beim Laden der nicht erfüllten Cutoff-Elemente", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Mit MyAnimeList authentifizieren", + "ImportSeries": "Serie importieren", + "NotificationsNtfySettingsServerUrlHelpText": "Leer lassen, um den öffentlichen Server ({url}) zu verwenden", + "OverviewOptions": "Überblick-Optionen", + "Parse": "Parsen", + "PartialSeason": "Teil-Staffel", + "SearchForMonitoredEpisodesSeason": "Suche nach überwachten Episoden in dieser Staffel", + "ShowMonitored": "Überwachter Status anzeigen", + "Socks5": "Socks5 (Unterstützt TOR)", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Aktiviere die Handhabung abgeschlossener Downloads, wenn möglich (Multi-Computer wird nicht unterstützt)", + "ImportScriptPath": "Import-Skript-Pfad", + "MediaManagementSettingsSummary": "Einstellungen zu Benennung, Dateiverwaltung und Root-Ordnern", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName}-Pfad, wird verwendet, um Serienpfade zu ändern, wenn {serviceName} den Bibliothekspfad anders sieht als {appName} (benötigt 'Bibliothek aktualisieren')", + "EditImportListImplementation": "Import-Liste bearbeiten - {implementationName}", + "ImportListsSettingsRssUrl": "RSS-URL", + "ImportListsSonarrSettingsTagsHelpText": "Tags von der Quellinstanz zum Importieren", + "ImportListsTraktSettingsUserListTypeCollection": "Benutzer-Sammlungs-Liste", + "IndexerSettingsSeedTime": "Seed-Zeit", + "NoEpisodeOverview": "Kein Episoden-Überblick", + "NotificationsTwitterSettingsConnectToTwitter": "Mit Twitter / X verbinden", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Beachte, dass wenn du die Kategorie änderst, erforderliche/ausgeschlossene Regeln für die Subgruppen hinzufügen musst, um Fremdsprachen-Releases zu vermeiden.", + "IndexerSettingsRssUrlHelpText": "Gib die URL eines {indexer}-kompatiblen RSS-Feeds ein", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Die Zeit, die ein Saison-Paket-Torrent gesät werden sollte, bevor es gestoppt wird. Leer verwendet die Standardzeit des Download-Clients", + "IndexersLoadError": "Indexer konnten nicht geladen werden", + "NoChange": "Keine Änderung", + "NotificationsEmailSettingsCcAddressHelpText": "Durch Kommas getrennte Liste der CC-Empfänger", + "NotificationsNtfyValidationAuthorizationRequired": "Autorisation erforderlich", + "NotificationsSendGridSettingsApiKeyHelpText": "Der von SendGrid generierte API-Schlüssel", + "OrganizeLoadError": "Fehler beim Laden der Vorschauen", + "SupportedListsSeries": "{appName} unterstützt mehrere Listen für den Import von Serien in die Datenbank.", + "MonitorAllSeasons": "Alle Staffeln", + "MonitorAllSeasonsDescription": "Alle neuen Staffeln automatisch überwachen", + "MonitorLastSeasonDescription": "Alle Episoden der letzten Staffel überwachen", + "Name": "Name", + "IndexerValidationRequestLimitReached": "Anfragelimit erreicht: {exceptionMessage}", + "LastWriteTime": "Letzte Schreibzeit", + "MidseasonFinale": "Midseason-Finale", + "MissingNoItems": "Keine fehlenden Elemente", + "MultiSeason": "Mehrere Staffeln", + "NotificationsCustomScriptSettingsName": "Benutzerdefiniertes Skript", + "Password": "Passwort", + "Info": "Info", + "RemoveQueueItemRemovalMethod": "Entfernmethode", + "ShowMonitoredHelpText": "Überwachungsstatus unter dem Poster anzeigen", + "ShowQualityProfileHelpText": "Qualitätsprofil unter dem Poster anzeigen", + "DownloadClientQbittorrentTorrentStatePathError": "Import fehlgeschlagen. Der Pfad stimmt mit dem Basis-Download-Verzeichnis des Clients überein. Möglicherweise ist 'Obersten Ordner beibehalten' für diesen Torrent deaktiviert oder das 'Torrent-Inhaltslayout' ist nicht auf 'Original' oder 'Unterordner erstellen' eingestellt.", + "ImportListsAniListSettingsImportRepeating": "Import wiederholend", + "ImportListsImdbSettingsListId": "Listen-ID", + "IncludeHealthWarnings": "Gesundheitswarnungen einbeziehen", + "Logs": "Protokolle", + "MetadataPlexSettingsSeriesPlexMatchFile": "Plex-Match-Datei für Serie", + "MetadataSettingsSeriesMetadata": "Serienmetadaten", + "MissingEpisodes": "Fehlende Episoden", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Konfigurationsschlüssel für die Persistente Speicherlösung. Leer lassen, wenn Stateless URLs verwendet wird.", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Aktivieren, um die Benachrichtigung als \"Zeitkritisch\" zu markieren", + "NotificationsSignalSettingsSenderNumberHelpText": "Telefonnummer des Absenders, die bei signal-api registriert ist", + "NotificationsTelegramSettingsChatId": "Chat-ID", + "OverrideGrabNoSeries": "Serie muss ausgewählt werden", + "SeriesTypes": "Serientypen", + "ShowRelativeDates": "Relative Daten anzeigen", + "MetadataPlexSettingsEpisodeMappings": "Episoden-Zuordnungen", + "NextAiringDate": "Nächste Ausstrahlung: {date}", + "NoEpisodeInformation": "Keine Episoden-Informationen verfügbar.", + "NoEpisodesInThisSeason": "Keine Episoden in dieser Staffel", + "NoIssuesWithYourConfiguration": "Keine Probleme mit deiner Konfiguration", + "NoLimitForAnyRuntime": "Kein Limit für beliebige Laufzeit", + "NoSeriesHaveBeenAdded": "Du hast noch keine Serien hinzugefügt, möchtest du zuerst einige oder alle Serien importieren?", + "NoTagsHaveBeenAddedYet": "Es wurden noch keine Tags hinzugefügt", + "NotSeasonPack": "Kein Staffelpaket", + "NotificationStatusSingleClientHealthCheckMessage": "Benachrichtigungen nicht verfügbar wegen Fehlern: {notificationNames}", + "NotificationTriggersHelpText": "Wähle aus, welche Ereignisse diese Benachrichtigung auslösen sollen", + "Other": "Andere", + "NotificationsAppriseSettingsTagsHelpText": "Benachrichtige optional nur diejenigen, die entsprechend markiert sind.", + "NotificationsCustomScriptValidationFileDoesNotExist": "Datei existiert nicht", + "NotificationsDiscordSettingsAuthor": "Autor", + "NotificationsDiscordSettingsAuthorHelpText": "Überschreibe den Embed-Autor, der für diese Benachrichtigung angezeigt wird. Leer ist der Instanzname", + "NotificationsDiscordSettingsAvatar": "Avatar", + "NoBlocklistItems": "Keine Blocklist-Elemente", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Ändere die Felder, die für diese 'On Grab'-Benachrichtigung übergeben werden", + "MonitorRecentEpisodes": "Kürzliche Episoden", + "Search": "Suchen", + "ClickToChangeReleaseType": "Klicke, um den Release-Typ zu ändern", + "NotificationsAppriseSettingsConfigurationKey": "Apprise Konfigurationsschlüssel", + "ImportCountSeries": "Importiere {selectedCount} Serien", + "ImportListExclusions": "Ausschlüsse aus der Importliste", + "MegabytesPerMinute": "Megabyte pro Minute", + "ImportUsingScriptHelpText": "Dateien für den Import mit einem Skript kopieren (z. B. für Transkodierung)", + "Mechanism": "Mechanismus", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Ändere die Felder, die für diese 'On Manual Interaction'-Benachrichtigung übergeben werden", + "NotificationsDiscordSettingsUsernameHelpText": "Der Benutzername, unter dem gepostet wird, standardmäßig der Discord Webhook-Standard", + "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord Channel Webhook URL", + "NotificationsEmailSettingsBccAddress": "BCC-Adresse(n)", + "NotificationsEmailSettingsName": "E-Mail", + "NotificationsEmailSettingsRecipientAddress": "Empfängeradresse(n)", + "ImportListsAniListSettingsImportCompleted": "Import abgeschlossen", + "NotificationsEmailSettingsRecipientAddressHelpText": "Durch Kommas getrennte Liste der Empfängeradressen", + "NotificationsGotifySettingsPriorityHelpText": "Priorität der Benachrichtigung", + "MoveSeriesFoldersDontMoveFiles": "Nein, ich werde die Dateien selbst verschieben", + "NotificationsTwitterSettingsConsumerSecret": "Consumer Secret", + "NotificationsJoinSettingsDeviceNames": "Gerätenamen", + "NotificationsKodiSettingsCleanLibraryHelpText": "Bibliothek nach der Aktualisierung bereinigen", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Bibliothek bei Import & Umbenennung aktualisieren?", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Aktiviere, um den EU-MailGun-Endpunkt zu verwenden", + "LastSearched": "Letzte Suche", + "NotificationsNtfySettingsTopics": "Themen", + "NotificationsTelegramSettingsBotToken": "Bot-Token", + "OpenBrowserOnStart": "Browser beim Start öffnen", + "PreferredProtocol": "Bevorzugtes Protokoll", + "PreviewRenameSeason": "Vorschau Umbenennung für diese Staffel", + "ReleaseProfileIndexerHelpText": "Gib an, auf welchen Indexer das Profil zutrifft", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Du verwendest Docker; der Download-Client {downloadClientName} platziert Downloads in {path}, aber dieses Verzeichnis scheint nicht im Container zu existieren. Überprüfe deine Remote-Pfad-Abgleichungen und Container-Volume-Einstellungen.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Der Download-Client {downloadClientName} meldet Dateien in {path}, aber {appName} kann dieses Verzeichnis nicht sehen. Möglicherweise müssen die Ordnersberechtigungen angepasst werden.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Der lokale Download-Client {downloadClientName} meldet Dateien in {path}, aber dies ist kein gültiger {osName}-Pfad. Überprüfe die Einstellungen deines Download-Clients.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Der lokale Download-Client {downloadClientName} legt Downloads in {path} ab, aber dies ist kein gültiger {osName}-Pfad. Überprüfe die Einstellungen des Download-Clients.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Der Remote-Download-Client {downloadClientName} hat Dateien in {path} gemeldet, aber dieses Verzeichnis scheint nicht zu existieren. Wahrscheinlich fehlt eine Remote-Pfadzuordnung.", + "RemotePathMappingsInfo": "Remote-Pfadzuordnungen sind sehr selten erforderlich. Wenn {appName} und dein Download-Client auf demselben System sind, ist es besser, deine Pfade abzugleichen. Weitere Informationen findest du im [Wiki]({wikiLink}).", + "RemoveMultipleFromDownloadClientHint": "Entfernt Downloads und Dateien aus dem Download-Client", + "RemoveRootFolder": "Root-Ordner entfernen", + "RemoveSelectedBlocklistMessageText": "Bist du sicher, dass du die ausgewählten Elemente aus der Blockliste entfernen möchtest?", + "ReplaceWithSpaceDash": "Mit Leerzeichen Bindestrich ersetzen", + "UnmonitorDeletedEpisodes": "Nicht überwachte gelöschte Episoden", + "RootFolder": "Root-Ordner", + "RootFolderSelectFreeSpace": "{freeSpace} frei", + "SeasonsMonitoredNone": "Keine", + "SeasonsMonitoredStatus": "Überwachte Staffeln", + "SelectReleaseType": "Release-Typ auswählen", + "SeriesMonitoring": "Serienüberwachung", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Gesamt: {totalEpisodeCount}, Herunterladen: {downloadingCount})", + "SeriesTitle": "Serientitel", + "SeriesTitleToExcludeHelpText": "Der Name der zu schließenden Serie", + "SeriesTypesHelpText": "Der Serientyp wird für das Umbenennen, Parsen und Suchen verwendet", + "SetIndexerFlagsModalTitle": "{modalTitle} - Indexer-Flags festlegen", + "SystemTimeHealthCheckMessage": "Die Systemzeit ist mehr als einen Tag abweichend. Geplante Aufgaben könnten nicht korrekt ausgeführt werden, bis die Zeit korrigiert ist.", + "Table": "Tabelle", + "TomorrowAt": "Morgen um {time}", + "UnableToUpdateSonarrDirectly": "Kann {appName} nicht direkt aktualisieren", + "Underscore": "Unterstrich", + "Ungroup": "Gruppierung aufheben", + "UpdateFiltered": "Update gefiltert", + "NotificationsAppriseSettingsTags": "Apprise Tags", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Argumente, die an das Skript übergeben werden sollen", + "Sunday": "Sonntag", + "ImportListSettings": "Importlisteneinstellungen", + "NotificationsPushcutSettingsNotificationName": "Benachrichtigungsname", + "NotificationsPushoverSettingsDevices": "Geräte", + "NotificationsPushoverSettingsDevicesHelpText": "Liste von Gerätenamen (leer lassen, um an alle Geräte zu senden)", + "ImportListsCustomListSettingsUrlHelpText": "Die URL der Serienliste", + "IndexerSettingsMinimumSeedersHelpText": "Minimale Anzahl an Seedern, die erforderlich ist.", + "LatestSeason": "Neueste Staffel", + "LogFiles": "Protokolldateien", + "Monitoring": "Überwachung", + "NoEpisodeHistory": "Keine Episoden-Historie", + "NotificationsSettingsWebhookMethod": "Methode", + "NotificationsValidationInvalidUsernamePassword": "Ungültiger Benutzername oder Passwort", + "SetPermissionsLinuxHelpText": "Soll chmod beim Importieren/Umbenennen von Dateien ausgeführt werden?", + "OnFileUpgrade": "Bei Dateiupgrade", + "OnLatestVersion": "Die neueste Version von {appName} ist bereits installiert", + "DayOfWeekAt": "{day} um {time}", + "CountVotes": "{votes} Stimmen", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Qualitätsprofile von der Quellinstanz, von der importiert werden soll", + "ToggleMonitoredSeriesUnmonitored": "Kann den Überwachungsstatus nicht umschalten, wenn die Serie nicht überwacht wird", + "NotificationsTwitterSettingsDirectMessage": "Direktnachricht", + "LogSizeLimit": "Protokollgrößenlimit", + "NotificationsSlackSettingsIcon": "Symbol", + "ShowSearch": "Suche anzeigen", + "CustomFormatsSpecificationExceptLanguageHelpText": "Übereinstimmt, wenn eine andere Sprache als die ausgewählte Sprache vorhanden ist", + "EnableAutomaticAddSeriesHelpText": "Füge Serien aus dieser Liste zu {appName} hinzu, wenn Synchronisierungen über die UI oder von {appName} durchgeführt werden", + "FullColorEventsHelpText": "Änderung des Stils, um das gesamte Ereignis mit der Statusfarbe zu färben, anstatt nur den linken Rand. Gilt nicht für Agenda", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Mehrere Root-Ordner fehlen für Importlisten: {rootFolderInfo}", + "ImportListStatusAllUnavailableHealthCheckMessage": "Alle Listen sind aufgrund von Fehlern nicht verfügbar", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Erwartete Shows", + "NotificationsSlackSettingsChannelHelpText": "Überschreibt den Standardkanal für den eingehenden Webhook (#anderer-kanal)", + "DeleteSelectedCustomFormats": "Custom Format(s) löschen", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} bevorzugt es, dass jeder Download einen eigenen Ordner hat. Mit * am Ordner/Path wird Sabnzbd diese Job-Ordner nicht erstellen. Gehe zu Sabnzbd, um das zu beheben.", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Verwende 'Nicht bevorzugen', um benutzerdefinierte Formatbewertungen über Propers/Repacks zu sortieren", + "EnableRssHelpText": "Wird verwendet, wenn {appName} regelmäßig nach Releases über RSS-Sync sucht", + "EpisodeFilesLoadError": "Fehler beim Laden der Episode-Dateien", + "EpisodeGrabbedTooltip": "Episode von {indexer} abgerufen und an {downloadClient} gesendet", + "EpisodeHistoryLoadError": "Fehler beim Laden der Episoden-Historie", + "FullColorEvents": "Vollfarbige Ereignisse", + "Here": "hier", + "ImportExtraFilesEpisodeHelpText": "Importiere passende Zusatzdateien (Untertitel, nfo, etc.), nachdem eine Episodendatei importiert wurde", + "ImportListRootFolderMissingRootHealthCheckMessage": "Fehlender Root-Ordner für Importliste(n): {rootFolderInfo}", + "ImportListSearchForMissingEpisodesHelpText": "Nach dem Hinzufügen der Serie zu {appName} automatisch nach fehlenden Episoden suchen", + "ImportLists": "Importlisten", + "ImportListsAniListSettingsImportHiatus": "Import pausiert", + "NotificationsSettingsWebhookHeaders": "Header", + "ParseModalHelpTextDetails": "{appName} wird versuchen, den Titel zu parsen und dir Details darüber zu zeigen", + "Premiere": "Premiere", + "Priority": "Priorität", + "QualityCutoffNotMet": "Qualitätsgrenze wurde nicht erreicht", + "OptionalName": "Optionaler Name", + "Options": "Optionen", + "Or": "oder", + "Organize": "Organisieren", + "OrganizeModalHeader": "Organisieren & Umbenennen", + "OrganizeRenamingDisabled": "Umbenennen ist deaktiviert, nichts zum Umbenennen", + "OverrideGrabNoEpisode": "Mindestens eine Episode muss ausgewählt werden", + "Overview": "Überblick", + "PackageVersion": "Paketversion", + "Install": "Installieren", + "NotificationsTelegramSettingsMetadataLinks": "Metadaten-Links", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Aktiviere die Handhabung abgeschlossener Downloads", + "ListsLoadError": "Kann Listen nicht laden", + "UnmonitoredOnly": "Nur nicht überwacht", + "IndexerHDBitsSettingsMediums": "Medien", + "IndexerSettingsApiPath": "API-Pfad", + "IndexerSettingsApiUrl": "API-URL", + "ListWillRefreshEveryInterval": "Die Liste wird alle {refreshInterval} aktualisiert", + "MediaManagementSettingsLoadError": "Kann die Einstellungen zur Medienverwaltung nicht laden", + "MonitorNewItems": "Neue Elemente überwachen", + "MonitorPilotEpisode": "Pilotepisode", + "MonitorRecentEpisodesDescription": "Episoden überwachen, die in den letzten 90 Tagen ausgestrahlt wurden, und zukünftige Episoden", + "NotificationsGotifySettingsAppToken": "App-Token", + "NotificationsPushcutSettingsTimeSensitive": "Zeitkritisch", + "NotificationsSettingsWebhookUrl": "Webhook-URL", + "NotificationsTelegramSettingsTopicIdHelpText": "Gib eine Themen-ID an, um Benachrichtigungen an dieses Thema zu senden. Leer lassen, um das allgemeine Thema zu verwenden (nur Supergruppen)", + "NotificationsValidationUnableToSendTestMessage": "Kann keine Testnachricht senden: {exceptionMessage}", + "PreferProtocol": "Bevorzuge {preferredProtocol}", + "ReplaceIllegalCharacters": "Illegale Zeichen ersetzen", + "ShowRelativeDatesHelpText": "Relative (Heute/Gestern/etc.) oder absolute Daten anzeigen", + "SizeOnDisk": "Größe auf der Festplatte", + "Special": "Spezial", + "Status": "Status", + "SpecialEpisode": "Spezialepisode", + "CustomColonReplacement": "Benutzerdefinierter Kollon-Ersatz", + "CustomColonReplacementFormatHint": "Gültiges Dateisystemzeichen wie Kollon (Buchstabe)", + "Socks4": "Socks4", + "SingleEpisodeInvalidFormat": "Einzelne Episode: Ungültiges Format", + "SizeLimit": "Größenlimit", + "DownloadClientDelugeSettingsDirectoryHelpText": "Optionaler Ort für Downloads, leer lassen, um den Standard-Deluge-Ort zu verwenden", + "ReleaseProfile": "Release-Profil", + "ImportListsLoadError": "Konnte Importlisten nicht laden", + "Period": "Periode", + "ReleaseSceneIndicatorAssumingScene": "Gehe von einer Szene-Nummerierung aus.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Du verwendest Docker; der Download-Client {downloadClientName} meldet Dateien in {path}, aber dies ist kein gültiger {osName}-Pfad. Überprüfe deine Remote-Pfad-Abgleichungen und Download-Client-Einstellungen.", + "ScriptPath": "Skript-Pfad", + "RemotePathMappingRemotePathHelpText": "Root-Pfad zu dem Verzeichnis, auf das der Download-Client zugreift.", + "RemotePathMappingsLoadError": "Kann Remote-Pfadzuordnungen nicht laden", + "RemoveFromDownloadClientHint": "Entfernt den Download und die Datei(en) aus dem Download-Client", + "Repack": "Repacken", + "ReplaceWithDash": "Mit Bindestrich ersetzen", + "SeriesPremiere": "Serienpremiere", + "SetIndexerFlags": "Indexer-Flags festlegen", + "ShowPreviousAiring": "Vorherige Ausstrahlung anzeigen", + "ShowQualityProfile": "Qualitätsprofil anzeigen", + "Size": "Größe", + "RssSyncIntervalHelpText": "Intervall in Minuten. Setze auf null, um es zu deaktivieren (dies stoppt alle automatischen Release-Abfragen)", + "SearchForCutoffUnmetEpisodes": "Suche nach allen nicht erfüllten Cutoff-Episoden", + "SearchForMissing": "Suche nach fehlenden Episoden", + "TagIsNotUsedAndCanBeDeleted": "Tag wird nicht verwendet und kann gelöscht werden", + "YesterdayAt": "Gestern um {time}", + "IndexerSettingsWebsiteUrl": "Website-URL", + "InteractiveSearchModalHeaderSeason": "Interaktive Suche - {season}", + "ListRootFolderHelpText": "Root-Ordner-Listenelemente werden hinzugefügt zu", + "Manual": "Manuell", + "MarkAsFailed": "Als fehlgeschlagen markieren", + "MinimumCustomFormatScore": "Mindestwert für benutzerdefinierte Formate", + "MonitorSelected": "Ausgewählte überwachen", + "MustContainHelpText": "Das Release muss mindestens einen dieser Begriffe enthalten (Groß- und Kleinschreibung ignorieren)", + "OrganizeNamingPattern": "Namensmuster: `{episodeFormat}`", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Fügt der {clientName}-rpc-URL ein Präfix hinzu, z. B. {url}, standardmäßig '{defaultUrl}'", + "EditIndexerImplementation": "Indexer bearbeiten - {implementationName}", + "EnableColorImpairedMode": "Farbenblindmodus aktivieren", + "EpisodeImportedTooltip": "Episode erfolgreich heruntergeladen und vom Download-Client übernommen", + "EpisodeNaming": "Episodenbenennung", + "EpisodeProgress": "Episodenfortschritt", + "EpisodeRequested": "Episode angefordert", + "EpisodeHasNotAired": "Episode wurde noch nicht ausgestrahlt", + "EpisodeNumbers": "Episodennummer(n)", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Dein API-Schlüssel aus deinem Profil", + "NotificationsNtfySettingsAccessToken": "Zugriffs-Token", + "ExportCustomFormat": "Benutzerdefiniertes Format exportieren", + "FormatTimeSpanDays": "{days}d {time}", + "IRCLinkText": "#sonarr auf Libera", + "ImportList": "Importliste", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Alle Listen erfordern manuelle Interaktion aufgrund möglicher Teilabfragen", + "ImportListsAniListSettingsImportCancelledHelpText": "Medien: Serie wurde abgebrochen", + "ImportListsAniListSettingsImportDropped": "Import abgebrochen", + "ImportListsAniListSettingsImportFinishedHelpText": "Medien: Alle Episoden sind ausgestrahlt", + "ImportListsAniListSettingsImportHiatusHelpText": "Medien: Serie ist in der Pause", + "ImportListsAniListSettingsImportNotYetReleased": "Import noch nicht veröffentlicht", + "ImportListsAniListSettingsImportPausedHelpText": "Liste: Auf Halten", + "ImportListsAniListSettingsImportPlanningHelpText": "Liste: Geplant zu schauen", + "ImportListsAniListSettingsImportWatchingHelpText": "Liste: Wird gerade geschaut", + "ImportListsCustomListSettingsName": "Benutzerdefinierte Liste", + "ImportListsImdbSettingsListIdHelpText": "IMDb Listen-ID (z.B. ls12345678)", + "ImportListsPlexSettingsAuthenticateWithPlex": "Mit Plex.tv authentifizieren", + "ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS", + "ImportListsSettingsAccessToken": "Zugriffstoken", + "NotificationsPushBulletSettingSenderId": "Sender-ID", + "ImportListsSettingsAuthUser": "Authentifizierter Benutzer", + "ImportListsSettingsExpires": "Läuft ab", + "NotificationsPushcutSettingsApiKeyHelpText": "API-Schlüssel können in der Kontenansicht der Pushcut-App verwaltet werden", + "ImportListsSimklSettingsShowType": "Show-Typ", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Synchronisiere die Staffelüberwachung von der {appName}-Instanz. Wenn aktiviert, wird 'Überwachen' ignoriert.", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Mit Trakt authentifizieren", + "ImportListsTraktSettingsLimitHelpText": "Begrenze die Anzahl der zu holenden Serien", + "ReleaseGroup": "Release-Gruppe", + "ReleaseSceneIndicatorAssumingTvdb": "Gehe von einer TVDB-Nummerierung aus.", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Empfohlene Shows aller Zeiten", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Top-Serien des Jahres", + "ImportListsTraktSettingsUserListName": "Trakt-Benutzer", + "ImportListsTraktSettingsUserListTypeWatch": "Benutzer-Watch-Liste", + "ImportListsTraktSettingsWatchedListSorting": "Sortierung der Gesehenen Liste", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Wenn der Listentyp 'Gesehen' ist, wähle die Reihenfolge, in der die Liste sortiert werden soll", + "ImportScriptPathHelpText": "Der Pfad zum Skript, das für den Import verwendet wird", + "Importing": "Importiere", + "IndexerPriority": "Indexer-Priorität", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Alle RSS-fähigen Indexer sind aufgrund kürzlicher Indexer-Fehler vorübergehend nicht verfügbar", + "IndexerSettingsAdditionalParametersNyaa": "Zusätzliche Parameter", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Blockierte Torrent-Hashes beim Abrufen ablehnen", + "IndexerSettingsSeedTimeHelpText": "Die Zeit, die ein Torrent gesät werden sollte, bevor er gestoppt wird. Leer verwendet die Standardzeit des Download-Clients", + "IndexerValidationInvalidApiKey": "Ungültiger API-Schlüssel", + "IndexerValidationNoRssFeedQueryAvailable": "Kein RSS-Feed-Abfrage verfügbar. Dies könnte ein Problem mit dem Indexer oder deinen Indexer-Kategorieneinstellungen sein.", + "IndexerValidationUnableToConnect": "Verbindung zum Indexer konnte nicht hergestellt werden: {exceptionMessage}. Überprüfe das Log um diese Fehlermeldung für Details", + "InteractiveImportLoadError": "Manuelle Importitems konnten nicht geladen werden", + "InteractiveImportNoEpisode": "Es muss mindestens eine Episode für jede ausgewählte Datei gewählt werden", + "Links": "Links", + "InteractiveImportNoQuality": "Qualität muss für jede ausgewählte Datei gewählt werden", + "InvalidUILanguage": "Die UI ist auf eine ungültige Sprache eingestellt, korrigiere sie und speichere die Einstellungen", + "KeepAndUnmonitorSeries": "Serie behalten und nicht mehr überwachen", + "KeyboardShortcutsFocusSearchBox": "Suchfeld fokussieren", + "KeyboardShortcutsOpenModal": "Dieses Modal öffnen", + "Label": "Label", + "Large": "Groß", + "LibraryImportTips": "Einige Tipps, um sicherzustellen, dass der Import reibungslos verläuft:", + "ListSyncTag": "Listensynchronisations-Tag", + "ListTagsHelpText": "Tags, die beim Import aus dieser Liste hinzugefügt werden", + "LocalAirDate": "Lokal ausgestrahlt am", + "LogFilesLocation": "Protokolldateien befinden sich unter: {location}", + "LogLevel": "Protokollstufe", + "ManageEpisodesSeason": "Episoden in dieser Staffel verwalten", + "MassSearchCancelWarning": "Dies kann nicht abgebrochen werden, sobald es gestartet wurde, ohne {appName} neu zu starten oder alle Indexer zu deaktivieren.", + "MaximumLimits": "Maximale Grenzen", + "MediaInfo": "Medieninfo", + "MetadataProvidedBy": "Metadaten werden bereitgestellt von {provider}", + "MetadataSettings": "Einstellungen für Metadaten", + "MetadataSettingsEpisodeMetadata": "Episodenmetadaten", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo mit vollständigen Serienmetadaten", + "Min": "Min", + "MinimumCustomFormatScoreHelpText": "Mindestwert für benutzerdefinierte Formate, der zum Herunterladen zugelassen wird", + "Missing": "Fehlend", + "MonitorFirstSeasonDescription": "Alle Episoden der ersten Staffel überwachen. Alle anderen Staffeln werden ignoriert", + "MonitorNewSeasonsHelpText": "Welche neuen Staffeln sollen automatisch überwacht werden", + "MonitorPilotEpisodeDescription": "Nur die erste Episode der ersten Staffel überwachen", + "MonitorSeries": "Serie überwachen", + "MountSeriesHealthCheckMessage": "Das Mount-Verzeichnis mit dem Serienpfad ist nur lesbar: ", + "MultiEpisodeStyle": "Stil für mehrere Episoden", + "NotificationsCustomScriptSettingsArguments": "Argumente", + "NamingSettingsLoadError": "Benennungseinstellungen konnten nicht geladen werden", + "Negate": "Negieren", + "Network": "Netzwerk", + "NextAiring": "Nächste Ausstrahlung", + "NoBackupsAreAvailable": "Keine Sicherungen verfügbar", + "NoEpisodesFoundForSelectedSeason": "Keine Episoden für die ausgewählte Staffel gefunden", + "NoEventsFound": "Keine Ereignisse gefunden", + "NoLinks": "Keine Links", + "NoMatchFound": "Kein Treffer gefunden!", + "NoMonitoredEpisodes": "Keine überwachten Episoden in dieser Serie", + "NoResultsFound": "Keine Ergebnisse gefunden", + "NotificationStatusAllClientHealthCheckMessage": "Alle Benachrichtigungen sind aufgrund von Fehlern nicht verfügbar", + "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth Passwort", + "NotificationsAppriseSettingsServerUrl": "Apprise Server URL", + "NotificationsDiscordSettingsOnGrabFields": "On Grab Felder", + "NotificationsEmailSettingsBccAddressHelpText": "Durch Kommas getrennte Liste der BCC-Empfänger", + "NotificationsGotifySettingIncludeSeriesPoster": "Serienposter einbeziehen", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Schließe das Serienposter in die Nachricht ein", + "NotificationsGotifySettingsAppTokenHelpText": "Das von Gotify generierte Anwendungstoken", + "NotificationsGotifySettingsServer": "Gotify-Server", + "CustomFormatsSpecificationMaximumSizeHelpText": "Das Release muss kleiner oder gleich Groß sein", + "NotificationsJoinSettingsNotificationPriority": "Benachrichtigungspriorität", + "NotificationsJoinValidationInvalidDeviceId": "Geräte-IDs sind ungültig.", + "NotificationsKodiSettingsDisplayTimeHelpText": "Wie lange die Benachrichtigung angezeigt wird (in Sekunden)", + "NotificationsNtfySettingsPasswordHelpText": "Optionales Passwort", + "ErrorLoadingItem": "Beim Laden dieses Elements ist ein Fehler aufgetreten", + "NotificationsPlexSettingsAuthToken": "Auth-Token", + "NotificationsPlexValidationNoTvLibraryFound": "Mindestens eine TV-Bibliothek ist erforderlich", + "ImportListsSimklSettingsUserListTypeCompleted": "Abgeschlossen", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, einschließlich Port, der {appName}-Instanz, von der importiert werden soll", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Top-Serien des Monats", + "IncludeCustomFormatWhenRenaming": "Benutzerdefiniertes Format beim Umbenennen einbeziehen", + "IndexerSettingsApiPathHelpText": "Pfad zur API, normalerweise {url}", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackett's All-Endpunkt wird nicht unterstützt, bitte füge Indexer einzeln hinzu", + "KeyboardShortcutsCloseModal": "Aktuelles Modal schließen", + "LiberaWebchat": "Libera Webchat", + "ListQualityProfileHelpText": "Qualitätsprofil-Listenelemente werden mit hinzugefügt", + "LongDateFormat": "Langes Datumsformat", + "Lowercase": "Kleinbuchstaben", + "MatchedToEpisodes": "Abgeglichen mit Episoden", + "Max": "Max", + "MaximumSize": "Maximale Größe", + "Medium": "Medium", + "Metadata": "Metadaten", + "MetadataSettingsEpisodeMetadataImageThumbs": "Episodenmetadaten-Bildvorschauen", + "MetadataSettingsSeasonImages": "Staffelbilder", + "MinimumLimits": "Minimale Grenzen", + "MinutesSixty": "60 Minuten: {sixty}", + "MonitoredOnly": "Nur überwacht", + "MonitoringOptions": "Überwachungsoptionen", + "NoChanges": "Keine Änderungen", + "NoLeaveIt": "Nein, lass es", + "NotificationsDiscordSettingsOnImportFields": "On Import Felder", + "NotificationsNtfySettingsServerUrl": "Server-URL", + "NotificationsSlackSettingsIconHelpText": "Ändere das Symbol, das für Nachrichten verwendet wird, die in Slack gepostet werden (Emoji oder URL)", + "NotificationsTelegramSettingsTopicId": "Themen-ID", + "NotificationsTraktSettingsAccessToken": "Access-Token", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Consumer Secret aus einer Twitter-Anwendung", + "NotificationsValidationInvalidAuthenticationToken": "Authentifizierungstoken ist ungültig", + "NotificationsValidationUnableToConnect": "Verbindung konnte nicht hergestellt werden: {exceptionMessage}", + "OverrideGrabModalTitle": "Überschreiben und Abrufen - {title}", + "Paused": "Pausiert", + "Permissions": "Berechtigungen", + "RemoveDownloadsAlert": "Die Entfernen-Einstellungen wurden in die einzelnen Download-Client-Einstellungen in der Tabelle oben verschoben.", + "Renamed": "Umbenannt", + "Replace": "Ersetzen", + "RssSyncInterval": "RSS-Sync-Intervall", + "SeriesIndexFooterMissingUnmonitored": "Fehlende Episoden (Serie nicht überwacht)", + "ShowSizeOnDisk": "Größe auf der Festplatte anzeigen", + "Small": "Klein", + "SkipRedownloadHelpText": "Verhindert, dass {appName} einen alternativen Release für dieses Objekt herunterlädt", + "UnknownDownloadState": "Unbekannter Download-Status: {state}", + "UnmonitorSelected": "Nicht überwachen ausgewählter Episoden", + "CutoffUnmetNoItems": "Keine nicht erfüllten Cutoff-Elemente", + "NotificationsPushoverSettingsRetryHelpText": "Intervall, um Notfallwarnungen zu wiederholen, mindestens 30 Sekunden", + "EnableProfileHelpText": "Aktiviere das Release-Profil", + "Imported": "Importiert", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerIPTorrentsSettingsFeedUrl": "Feed-URL", + "IndexerSettings": "Indexer-Einstellungen", + "IndexerSettingsAllowZeroSize": "Größe Null erlauben", + "IndexerSettingsAnimeCategories": "Anime-Kategorien", + "IndexerSettingsSeasonPackSeedTime": "Seed-Zeit für Saison-Pakete", + "IndexerValidationTestAbortedDueToError": "Test wurde aufgrund eines Fehlers abgebrochen: {exceptionMessage}", + "InteractiveImportNoFilesFound": "Keine Videodateien im ausgewählten Ordner gefunden", + "LanguagesLoadError": "Sprachen konnten nicht geladen werden", + "ListOptionsLoadError": "Kann Listenoptionen nicht laden", + "Logout": "Abmelden", + "Mapping": "Zuordnung", + "MediaManagementSettings": "Einstellungen zur Medienverwaltung", + "MinutesThirty": "30 Minuten: {thirty}", + "MissingLoadError": "Fehler beim Laden der fehlenden Elemente", + "Monitor": "Überwachen", + "MustNotContainHelpText": "Das Release wird abgelehnt, wenn einer oder mehrere dieser Begriffe enthalten sind (Groß- und Kleinschreibung ignorieren)", + "NoHistory": "Keine Historie", + "None": "Keine", + "Original": "Original", + "OriginalLanguage": "Originalsprache", + "ReleaseSceneIndicatorSourceMessage": "{message} Releases existieren mit mehrdeutiger Nummerierung, Episode konnte nicht zuverlässig identifiziert werden.", + "Reload": "Neu laden", + "RemotePath": "Remote-Pfad", + "Remove": "Entfernen", + "NotificationsSettingsUpdateMapPathsTo": "Pfade zu", + "NotificationsSettingsUseSslHelpText": "Mit {serviceName} über HTTPS anstatt HTTP verbinden", + "RemoveSelected": "Ausgewählte entfernen", + "NotificationsSettingsWebhookMethodHelpText": "Welche HTTP-Methode zum Absenden an den Webservice verwendet werden soll", + "NotificationsSignalSettingsGroupIdPhoneNumber": "Gruppen-ID / Telefonnummer", + "SearchFailedError": "Suche fehlgeschlagen, versuche es später noch einmal.", + "Shutdown": "Herunterfahren", + "SingleEpisode": "Einzelne Episode", + "Trace": "Trace", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Gruppen-ID / Telefonnummer des Empfängers", + "NotificationsSimplepushSettingsEvent": "Ereignis", + "NotificationsSimplepushSettingsKey": "Schlüssel", + "NotificationsSlackSettingsChannel": "Kanal", + "NotificationsTraktSettingsRefreshToken": "Refresh-Token", + "NotificationsTwitterSettingsAccessToken": "Access-Token", + "NotificationsTwitterSettingsConsumerKey": "Consumer Key", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Consumer Key aus einer Twitter-Anwendung", + "NotificationsTwitterSettingsMention": "Erwähnen", + "NotificationsValidationInvalidAccessToken": "Access-Token ist ungültig", + "NotificationsValidationInvalidHttpCredentials": "HTTP-Auth-Daten sind ungültig: {exceptionMessage}", + "PasswordConfirmation": "Passwortbestätigung", + "PendingChangesStayReview": "Bleiben und Änderungen überprüfen", + "PreferTorrent": "Bevorzuge Torrent", + "PrefixedRange": "Vorangestellter Bereich", + "PreviouslyInstalled": "Früher installiert", + "ProcessingFolders": "Verarbeitende Ordner", + "OrganizeRelativePaths": "Alle Pfade sind relativ zu: `{path}`", + "EpisodeSearchResultsLoadError": "Fehler beim Laden der Ergebnisse für diese Episodensuche. Versuche es später noch einmal", + "EventType": "Ereignistyp", + "FormatRuntimeMinutes": "{minutes}m", + "FormatShortTimeSpanHours": "{hours} Stunde(n)", + "IndexerSearchNoAutomaticHealthCheckMessage": "Keine Indexer mit aktivierter automatischer Suche verfügbar, {appName} wird keine automatischen Suchergebnisse liefern", + "IndexerSettingsSeedRatio": "Seed-Verhältnis", + "Interval": "Intervall", + "LastExecution": "Letzte Ausführung", + "ManageEpisodes": "Episoden verwalten", + "MultiEpisode": "Mehrere Episoden", + "New": "Neu", + "NoLogFiles": "Keine Logdateien", + "OnEpisodeFileDelete": "Bei Löschung der Episodendatei", + "OneMinute": "1 Minute", + "OrganizeSelectedSeriesModalHeader": "Ausgewählte Serien organisieren", + "Path": "Pfad", + "PendingDownloadClientUnavailable": "Ausstehend - Download-Client nicht verfügbar", + "SetPermissions": "Berechtigungen festlegen", + "ShowSeasonCount": "Anzahl der Staffeln anzeigen", + "TagDetails": "Tag-Details - {label}", + "TorrentBlackhole": "Torrent Blackhole", + "UnmappedFilesOnly": "Nur nicht zugeordnete Dateien", + "CustomFormatsSpecificationMinimumSizeHelpText": "Das Release muss größer als diese Größe sein", + "EditMetadata": "{metadataType}-Metadaten bearbeiten", + "EnableHelpText": "Aktiviere die Erstellung von Metadaten-Dateien für diesen Metadaten-Typ", + "EpisodeMissingAbsoluteNumber": "Episode hat keine absolute Episodennummer", + "IconForFinalesHelpText": "Zeige Icon für Serien-/Staffelfinalen basierend auf verfügbaren Episodeninformationen", + "ImportListExclusionsLoadError": "Konnte Ausschlüsse aus der Importliste nicht laden", + "ImportListsAniListSettingsAuthenticateWithAniList": "Mit AniList authentifizieren", + "ImportListsAniListSettingsImportCancelled": "Import abgebrochen", + "ImportListsAniListSettingsImportCompletedHelpText": "Liste: Abgeschlossenes Schauen", + "ImportListsCustomListValidationConnectionError": "Fehler bei der Verbindung zur URL. StatusCode: {exceptionStatusCode}", + "ImportListsSimklSettingsName": "Simkl User Watchlist", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Geplant zu schauen", + "ImportListsTraktSettingsLimit": "Limit", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Empfohlene Shows der Woche", + "ImportListsTraktSettingsYearsHelpText": "Serien nach Jahr oder Jahrbereich filtern", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Aktiviere die Handhabung abgeschlossener Downloads, wenn möglich", + "IndexerSettingsAnimeStandardFormatSearch": "Anime Standard-Format-Suche", + "IndexerSettingsPasskey": "Passkey", + "IndexersSettingsSummary": "Indexer und Indexer-Optionen", + "InfoUrl": "Info-URL", + "InstallLatest": "Neueste Version installieren", + "Monday": "Montag", + "MonitorNoNewSeasons": "Keine neuen Staffeln", + "IncludeUnmonitored": "Unüberwachte einbeziehen", + "Indexer": "Indexer", + "IndexerDownloadClientHealthCheckMessage": "Indexer mit ungültigen Download-Clients: {indexerNames}.", + "OnlyTorrent": "Nur Torrent", + "Message": "Nachricht", + "OverrideGrabNoQuality": "Qualität muss ausgewählt werden", + "PreviewRename": "Vorschau Umbenennung", + "MonitorMissingEpisodes": "Fehlende Episoden", + "Monitored": "Überwacht", + "NotificationsPushBulletSettingSenderIdHelpText": "Die Geräte-ID, von der Benachrichtigungen gesendet werden, verwenden Sie device_iden in der Geräte-URL auf pushbullet.com (leer lassen, um von sich selbst zu senden)", + "RemoveSelectedItem": "Ausgewähltes Element entfernen", + "Required": "Erforderlich", + "Rss": "RSS", + "SearchAll": "Alle durchsuchen", + "SearchByTvdbId": "Du kannst auch mit der TVDB-ID einer Serie suchen, z. B. tvdb:71663", + "NotificationsPushcutSettingsNotificationNameHelpText": "Benachrichtigungsname aus dem Benachrichtigungsbereich der Pushcut-App", + "Style": "Stil", + "TableOptionsButton": "Tabellenoptionen-Schaltfläche", + "Unknown": "Unbekannt", + "Unmonitored": "Nicht überwacht", + "NotificationsSignalSettingsPasswordHelpText": "Passwort, das zur Authentifizierung von Anfragen an signal-api verwendet wird", + "NotificationsSignalSettingsSenderNumber": "Absendernummer", + "NotificationsSignalSettingsUsernameHelpText": "Benutzername, der zur Authentifizierung von Anfragen an signal-api verwendet wird", + "NotificationsTraktSettingsExpires": "Abläuft", + "NotificationsTwitterSettingsDirectMessageHelpText": "Sende eine Direktnachricht anstelle einer öffentlichen Nachricht", + "NotificationsTwitterSettingsMentionHelpText": "Erwähne diesen Benutzer in gesendeten Tweets", + "NotificationsValidationInvalidApiKey": "API-Schlüssel ist ungültig", + "NotificationsValidationUnableToConnectToService": "Kann keine Verbindung zu {serviceName} herstellen", + "OnApplicationUpdate": "Bei Anwendungsaktualisierung", + "OnEpisodeFileDeleteForUpgrade": "Bei Löschung der Episodendatei für Upgrade", + "OnSeriesDelete": "Bei Serienlöschung", + "OnlyForBulkSeasonReleases": "Nur für Bulk-Season-Releases", + "EnableColorImpairedModeHelpText": "Stiländerung, um es Farbenblinden Benutzern zu ermöglichen, farbcodierte Informationen besser zu unterscheiden", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Metadaten-Link für Clients, die nur einen Link unterstützen", + "InteractiveImportNoSeries": "Serie muss für jede ausgewählte Datei gewählt werden", + "NoSeasons": "Keine Staffeln", + "UnmonitorDeletedEpisodesHelpText": "Episoden, die von der Festplatte gelöscht wurden, werden automatisch in {appName} nicht mehr überwacht", + "UnmonitorSpecialEpisodes": "Nicht überwachen Specials", + "UpdateMonitoring": "Update-Überwachung", + "NoSeriesFoundImportOrAdd": "Keine Serien gefunden, um zu starten, solltest du deine bestehenden Serien importieren oder eine neue Serie hinzufügen.", + "NoUpdatesAreAvailable": "Es sind keine Updates verfügbar", + "RatingVotes": "Bewertungsstimmen", + "TablePageSizeMinimum": "Die Seitengröße muss mindestens {minimumValue} betragen", + "Tags": "Tags", + "TodayAt": "Heute um {time}", + "ImportFailed": "Import fehlgeschlagen: {sourceTitle}", + "ProfilesSettingsSummary": "Qualität, Sprachverzögerung und Release-Profile", + "ManageFormats": "Formate verwalten", + "ManualImportItemsLoadError": "Kann manuelle Importartikel nicht laden", + "MappedNetworkDrivesWindowsService": "Zugriff auf gemappte Netzlaufwerke ist nicht verfügbar, wenn als Windows-Dienst ausgeführt. Weitere Informationen findest du in den [FAQ]({url}).", + "MatchedToSeason": "Abgeglichen mit Staffel", + "MatchedToSeries": "Abgeglichen mit Serie", + "MaximumSingleEpisodeAge": "Maximales Alter einer einzelnen Episode", + "MaximumSingleEpisodeAgeHelpText": "Während einer vollständigen Saison-Suche werden nur Staffel-Pakete erlaubt, wenn die letzte Episode der Staffel älter ist als diese Einstellung. Nur für Standardserien. Verwende 0, um es zu deaktivieren.", + "MaximumSizeHelpText": "Maximale Größe für einen Release, der heruntergeladen wird, in MB. Setze auf Null, um es auf unbegrenzt zu setzen.", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages unterstützen einen `:EN+DE`-Suffix, der es dir ermöglicht, die im Dateinamen enthaltenen Sprachen zu filtern. Verwende `-DE`, um spezifische Sprachen auszuschließen. Das Anhängen von `+` (z.B. `:EN+`) wird `[EN]`/`[EN+--]`/`[--]` ausgeben, je nachdem, welche Sprachen ausgeschlossen sind. Zum Beispiel `{MediaInfo Full:EN+DE}`.", + "MetadataLoadError": "Kann Metadaten nicht laden", + "MetadataSettingsSeriesImages": "Serienbilder", + "MonitorFutureEpisodes": "Zukünftige Episoden", + "MonitorFutureEpisodesDescription": "Episoden überwachen, die noch nicht ausgestrahlt wurden", + "MyComputer": "Mein Computer", + "NamingSettings": "Benennungseinstellungen", + "NegateHelpText": "Wenn aktiviert, wird das benutzerdefinierte Format nicht angewendet, wenn diese {implementationName}-Bedingung zutrifft.", + "Negated": "Negiert", + "NoMinimumForAnyRuntime": "Kein Minimum für beliebige Laufzeit", + "NotificationsMailgunSettingsSenderDomain": "Absenderdomain", + "NotificationsEmailSettingsUseEncryptionHelpText": "Ob bevorzugt Verschlüsselung verwendet werden soll, wenn auf dem Server konfiguriert, ob immer Verschlüsselung über SSL (nur Port 465) oder StartTLS (anderer Port) verwendet wird oder keine Verschlüsselung verwendet wird", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Bibliothek bei Import, Umbenennung oder Löschung aktualisieren", + "NotificationsMailgunSettingsApiKeyHelpText": "Der von MailGun generierte API-Schlüssel", + "NotificationsSettingsUpdateMapPathsFrom": "Pfade von", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}-Pfad, wird verwendet, um Serienpfade zu ändern, wenn {serviceName} den Bibliothekspfad anders sieht als {appName} (benötigt 'Bibliothek aktualisieren')", + "NotificationsSignalValidationSslRequired": "SSL scheint erforderlich zu sein", + "NotificationsSlackSettingsUsernameHelpText": "Benutzername, der in Slack gepostet wird", + "NotificationsSlackSettingsWebhookUrlHelpText": "Webhook-URL des Slack-Kanals", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Der Remote-Download-Client {downloadClientName} legt Downloads in {path} ab, aber dieses Verzeichnis scheint nicht zu existieren. Wahrscheinlich fehlt eine Remote-Pfadzuordnung oder sie ist falsch.", + "RemotePathMappings": "Remote-Pfadzuordnungen", + "RemoveCompleted": "Abgeschlossene entfernen", + "SelectIndexerFlags": "Indexer-Flags auswählen", + "ShowTags": "Tags anzeigen", + "ShowEpisodes": "Episoden anzeigen", + "ShowTagsHelpText": "Tags unter dem Poster anzeigen", + "ShowTitle": "Titel anzeigen", + "ShowUnknownSeriesItems": "Unbekannte Serienobjekte anzeigen", + "Specials": "Spezialfolgen", + "StopSelecting": "Auswahl stoppen", + "SupportedImportListsMoreInfo": "Für mehr Informationen zu den einzelnen Importlisten klicke auf die 'Mehr Infos'-Schaltflächen.", + "SupportedIndexersMoreInfo": "Für mehr Informationen zu den einzelnen Indexern klicke auf die 'Mehr Infos'-Schaltflächen.", + "No": "Nein", + "OnGrab": "Bei Abruf", + "RootFolderMultipleMissingHealthCheckMessage": "Mehrere Root-Ordner fehlen: {rootFolderPaths}", + "MultiEpisodeInvalidFormat": "Mehrere Episoden: Ungültiges Format", + "MustContain": "Muss enthalten", + "NotificationsNtfySettingsClickUrl": "Klick-URL", + "FormatRuntimeHours": "{hours}h", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Top-Serien der Woche", + "IndexerHDBitsSettingsCategories": "Kategorien", + "InvalidFormat": "Ungültiges Format", + "KeepAndTagSeries": "Serie behalten und taggen", + "Location": "Standort", + "MoveSeriesFoldersMoveFiles": "Ja, die Dateien verschieben", + "NotificationsDiscordSettingsOnManualInteractionFields": "On Manual Interaction Felder", + "NotificationsEmailSettingsFromAddress": "Von-Adresse", + "ParseModalUnableToParse": "Kann den angegebenen Titel nicht parsen, bitte versuche es erneut.", + "Peers": "Peers", + "Pending": "Ausstehend", + "RecentFolders": "Kürzliche Ordner", + "Repeat": "Wiederholen", + "NotificationsGotifySettingsMetadataLinks": "Metadaten-Links", + "SeriesFootNote": "Optionale Steuerung der Trunkierung auf eine maximale Anzahl von Bytes einschließlich Auslassungspunkten (`...`). Das Trunkieren vom Ende (z. B. `{Series Title:30}`) oder vom Anfang (z. B. `{Series Title:-30}`) wird unterstützt.", + "SeriesLoadError": "Kann Serie nicht laden", + "SeriesMatchType": "Serien-Match-Typ", + "ShowNetwork": "Netzwerk anzeigen", + "SkipFreeSpaceCheckHelpText": "Verwenden, wenn {appName} den freien Speicherplatz des Root-Ordners nicht erkennen kann", + "Never": "Nie", + "NextExecution": "Nächste Ausführung", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Keine Indexer mit aktiviertem RSS-Sync verfügbar, {appName} wird keine neuen Releases automatisch abrufen", + "ShowUnknownSeriesItemsHelpText": "Objekte ohne Serie in der Warteschlange anzeigen, dies kann entfernte Serien, Filme oder andere Objekte in der {appName}-Kategorie umfassen", + "MinimumAge": "Mindestalter", + "Port": "Port", + "DownloadClientVuzeValidationErrorVersion": "Protokollversion nicht unterstützt, verwende Vuze 5.0.0.0 oder höher mit dem Vuze Web Remote Plugin.", + "ExternalUpdater": "{appName} ist so konfiguriert, dass es einen externen Aktualisierungsmechanismus verwendet", + "ExtraFileExtensionsHelpText": "Kommagetrennte Liste von zusätzlichen Dateien, die importiert werden sollen (.nfo wird als .nfo-orig importiert)", + "IndexerSettingsCategories": "Kategorien", + "InstallMajorVersionUpdateMessage": "Dieses Update wird eine neue Hauptversion installieren und ist möglicherweise nicht mit deinem System kompatibel. Bist du sicher, dass du dieses Update installieren möchtest?", + "Unlimited": "Unbegrenzt", + "MetadataSettingsSeriesMetadataUrl": "URL für Serienmetadaten", + "MetadataSettingsSeriesSummary": "Erstellt Metadatendateien, wenn Episoden importiert oder Serien aktualisiert werden", + "MetadataSource": "Metadatenquelle", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Suche nach nicht erfüllten Cutoff-Episoden starten", + "CountCustomFormatsSelected": "{count} benutzerdefiniertes Format(e) ausgewählt", + "DeleteSelectedCustomFormatsMessageText": "Bist du sicher, dass du {count} ausgewählte Custom Format(s) löschen möchtest?", + "DeleteSelectedImportListExclusionsMessageText": "Bist du sicher, dass du die ausgewählten Importlistenausschlüsse löschen möchtest?", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optionaler Ort, an den abgeschlossene Downloads verschoben werden, leer lassen, um den Standard-Deluge-Ort zu verwenden", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Ob das konfigurierte Inhaltslayout von qBittorrent, das ursprüngliche Layout des Torrents oder immer ein Unterordner erstellt werden soll (qBittorrent 4.3.2+)", + "ImportListsSettingsSummary": "Importiere von einer anderen {appName}-Instanz oder Trakt-Listen und verwalte Listen-Ausschlüsse", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Mit Simkl authentifizieren", + "ImportListsSimklSettingsListType": "Listen-Typ", + "Mode": "Modus", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Du musst die Film-Sortierung für die Kategorie deaktivieren, die {appName} verwendet, um Importprobleme zu vermeiden. Gehe zu Sabnzbd, um das zu beheben.", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Du musst die TV-Sortierung für die Kategorie deaktivieren, die {appName} verwendet, um Importprobleme zu vermeiden. Gehe zu Sabnzbd, um das zu beheben.", + "DownloadClientValidationAuthenticationFailureDetail": "Bitte überprüfe deinen Benutzernamen und dein Passwort. Stelle auch sicher, dass der Host, der {appName} ausführt, nicht durch WhiteList-Beschränkungen in der {clientName}-Konfiguration blockiert wird.", + "DownloadClientValidationSslConnectFailureDetail": "{appName} kann keine Verbindung zu {clientName} über SSL herstellen. Dieses Problem könnte mit dem Computer zusammenhängen. Versuche, sowohl {appName} als auch {clientName} so zu konfigurieren, dass sie kein SSL verwenden.", + "DownloadClientsSettingsSummary": "Download Clients, Download-Verwaltung und Remote-Pfadzuordnungen", + "EditListExclusion": "Listen-Ausschluss bearbeiten", + "EnableMediaInfoHelpText": "Extrahiere Video-Informationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien. Dies erfordert, dass {appName} Teile der Datei liest, was während der Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.", + "EpisodeTitleFootNote": "Optional steuere das Trunkieren auf eine maximale Anzahl von Bytes inklusive Auslassungszeichen (`...`). Das Trunkieren vom Ende (z.B. `{Episode Title:30}`) oder vom Anfang (z.B. `{Episode Title:-30}`) ist beides möglich. Episodentitel werden bei Bedarf automatisch auf Dateisystem-Beschränkungen gekürzt.", + "EpisodeTitleRequiredHelpText": "Verhindere den Import für bis zu 48 Stunden, wenn der Episodentitel im Benennungsformat ist und der Episodentitel TBA ist", + "FormatShortTimeSpanMinutes": "{minutes} Minute(n)", + "FormatShortTimeSpanSeconds": "{seconds} Sekunde(n)", + "HealthMessagesInfoBox": "Weitere Informationen zur Ursache dieser Gesundheitsprüfungsnachrichten findest du, indem du auf den Wiki-Link (Buch-Symbol) am Ende der Zeile klickst oder deine [Protokolle]({link}) überprüfst. Wenn du Schwierigkeiten hast, diese Nachrichten zu interpretieren, kannst du unseren Support kontaktieren, über die Links unten.", + "ICalSeasonPremieresOnlyHelpText": "Nur die erste Episode einer Staffel wird im Feed erscheinen", + "ImportListSearchForMissingEpisodes": "Nach fehlenden Episoden suchen", + "ImportListStatusUnavailableHealthCheckMessage": "Listen nicht verfügbar aufgrund von Fehlern: {importListNames}", + "ImportListsAniListSettingsImportDroppedHelpText": "Liste: Abgebrochen", + "ImportListsAniListSettingsImportFinished": "Import beendet", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Medien: Ausstrahlung hat noch nicht begonnen", + "ImportListsAniListSettingsImportPaused": "Import pausiert", + "ImportListsAniListSettingsImportPlanning": "Import geplant", + "ImportListsAniListSettingsImportReleasing": "Import wird veröffentlicht", + "ImportListsAniListSettingsImportReleasingHelpText": "Medien: Aktuell werden neue Episoden ausgestrahlt", + "ImportListsAniListSettingsImportRepeatingHelpText": "Liste: Aktuell wird wieder geschaut", + "ImportListsAniListSettingsImportWatching": "Import am Schauen", + "ImportListsAniListSettingsUsernameHelpText": "Benutzername für die Liste, von der importiert werden soll", + "ImportListsCustomListSettingsUrl": "Listen-URL", + "ImportListsCustomListValidationAuthenticationFailure": "Authentifizierungsfehler", + "ImportListsMyAnimeListSettingsListStatus": "Listenstatus", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Art der Liste, von der importiert werden soll, setze auf 'Alle' für alle Listen", + "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", + "ImportListsSettingsRefreshToken": "Aktualisierungstoken", + "ImportListsSimklSettingsListTypeHelpText": "Art der Liste, von der du importieren möchtest", + "ImportListsSimklSettingsShowTypeHelpText": "Art der Shows, von denen du importieren möchtest", + "ImportListsSimklSettingsUserListTypeDropped": "Abgebrochen", + "ImportListsSimklSettingsUserListTypeHold": "Auf Halten", + "ImportListsSimklSettingsUserListTypeWatching": "Schauend", + "ImportListsSonarrSettingsApiKeyHelpText": "API-Schlüssel der {appName}-Instanz, von der importiert werden soll", + "ImportListsSonarrSettingsFullUrl": "Vollständige URL", + "ImportListsSonarrSettingsRootFoldersHelpText": "Root-Ordner von der Quellinstanz, von der importiert werden soll", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Saisonüberwachung synchronisieren", + "ImportListsSonarrValidationInvalidUrl": "{appName}-URL ist ungültig, fehlt eine URL-Basis?", + "ImportListsTraktSettingsAdditionalParameters": "Zusätzliche Parameter", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Zusätzliche Trakt-API-Parameter", + "ImportListsTraktSettingsGenres": "Genres", + "ImportListsTraktSettingsGenresHelpText": "Serien nach Trakt-Genre-Slug filtern (durch Kommas getrennt), nur für beliebte Listen", + "ImportListsTraktSettingsListName": "Listenname", + "ImportListsTraktSettingsListNameHelpText": "Listenname für den Import, die Liste muss öffentlich sein oder du musst Zugriff auf die Liste haben", + "ImportListsTraktSettingsListType": "Listentyp", + "ImportListsTraktSettingsListTypeHelpText": "Typ der Liste, von der du importieren möchtest", + "ImportListsTraktSettingsPopularListTypePopularShows": "Beliebte Shows", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Empfohlene Shows des Monats", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Empfohlene Shows des Jahres", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Top-Serien aller Zeiten", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trendige Shows", + "ImportListsTraktSettingsPopularName": "Trakt-Beliebte Liste", + "ImportListsTraktSettingsRating": "Bewertung", + "ImportListsTraktSettingsRatingHelpText": "Serien nach Bewertungsbereich filtern (0-100)", + "ImportListsTraktSettingsUserListTypeWatched": "Benutzer-Gesehene-Liste", + "ImportListsTraktSettingsUserListUsernameHelpText": "Benutzername für die Liste, von der du importieren möchtest (leer lassen, um den Authentifizierten Benutzer zu verwenden)", + "ImportListsTraktSettingsUsernameHelpText": "Benutzername für die Liste, von der du importieren möchtest", + "ImportListsTraktSettingsWatchedListFilter": "Gefilterte Gesehene Liste", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Wenn der Listentyp 'Gesehen' ist, wähle den Serientyp, den du importieren möchtest", + "ImportListsTraktSettingsWatchedListTypeAll": "Alle", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Gesehen", + "ImportListsTraktSettingsWatchedListTypeInProgress": "In Arbeit", + "ImportListsTraktSettingsYears": "Jahre", + "ImportListsValidationInvalidApiKey": "API-Schlüssel ist ungültig", + "ImportListsValidationTestFailed": "Der Test wurde aufgrund eines Fehlers abgebrochen: {exceptionMessage}", + "ImportListsValidationUnableToConnectException": "Verbindung zur Importliste nicht möglich: {exceptionMessage}. Überprüfe das Protokoll auf Details.", + "ImportUsingScript": "Mit Skript importieren", + "ImportedTo": "Importiert nach", + "IncludeCustomFormatWhenRenamingHelpText": "In {Custom Formats} Umbenennungsformat einbeziehen", + "IndexerDownloadClientHelpText": "Gib an, welcher Download-Client für Abrufe von diesem Indexer verwendet wird", + "IndexerFlags": "Indexer-Flags", + "IndexerHDBitsSettingsCategoriesHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "IndexerHDBitsSettingsCodecsHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "IndexerHDBitsSettingsMediumsHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "Die vollständige RSS-Feed-URL, die von IPTorrents generiert wird, nur mit den ausgewählten Kategorien (HD, SD, x264 usw.)", + "IndexerJackettAllHealthCheckMessage": "Indexer, die den nicht unterstützten Jackett 'all' Endpunkt verwenden: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind aufgrund von Fehlern länger als 6 Stunden nicht verfügbar", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexer sind aufgrund von Fehlern länger als 6 Stunden nicht verfügbar: {indexerNames}", + "IndexerOptionsLoadError": "Indexer-Optionen konnten nicht geladen werden", + "IndexerPriorityHelpText": "Indexer-Priorität von 1 (Höchste) bis 50 (Niedrigste). Standard: 25. Wird verwendet, um bei gleichwertigen Releases eine Entscheidung zu treffen, {appName} wird jedoch weiterhin alle aktivierten Indexer für RSS-Sync und Suche verwenden", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Alle suchfähigen Indexer sind aufgrund kürzlicher Indexer-Fehler vorübergehend nicht verfügbar", + "IndexerSearchNoInteractiveHealthCheckMessage": "Keine Indexer mit aktiviertem interaktivem Suchen verfügbar, {appName} wird keine interaktiven Suchergebnisse liefern", + "IndexerSettingsAdditionalParameters": "Zusätzliche Parameter", + "IndexerSettingsAllowZeroSizeHelpText": "Wenn du diese Option aktivierst, kannst du Feeds verwenden, die keine Release-Größe angeben, aber sei vorsichtig, da größenbezogene Prüfungen nicht durchgeführt werden.", + "IndexerSettingsAnimeCategoriesHelpText": "Dropdown-Liste, leer lassen, um Anime zu deaktivieren", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Suche auch nach Anime mit der Standardnummerierung", + "IndexerSettingsApiUrlHelpText": "Ändere dies nicht, es sei denn, du weißt, was du tust. Dein API-Schlüssel wird an diesen Host gesendet.", + "IndexerSettingsCategoriesHelpText": "Dropdown-Liste, leer lassen, um Standard-/Tages-Serien zu deaktivieren", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "Wenn deine Seite ein Login-Cookie benötigt, um auf den RSS-Feed zuzugreifen, musst du es über einen Browser abrufen.", + "IndexerSettingsMinimumSeeders": "Minimale Seeder", + "IndexerSettingsMultiLanguageReleaseHelpText": "Welche Sprachen sind normalerweise in einem Release mit mehreren Sprachen auf diesem Indexer?", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Wenn ein Torrent durch einen Hash blockiert wird, wird er möglicherweise nicht korrekt abgelehnt während RSS/Recherche für einige Indexer. Diese Option aktiviert die Ablehnung des Torrents nach dem Abrufen, aber bevor er an den Client gesendet wird.", + "IndexerSettingsRssUrl": "RSS-URL", + "IndexerSettingsSeedRatioHelpText": "Das Verhältnis, das ein Torrent erreichen muss, bevor er gestoppt wird. Leer verwendet das Standardverhältnis des Download-Clients. Das Verhältnis sollte mindestens 1,0 betragen und den Regeln des Indexers folgen.", + "IndexerStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar", + "IndexerTagSeriesHelpText": "Verwende diesen Indexer nur für Serien mit mindestens einem passenden Tag. Leer lassen, um ihn mit allen Serien zu verwenden.", + "IndexerValidationCloudFlareCaptchaExpired": "CloudFlare CAPTCHA-Token abgelaufen, bitte aktualisiere es.", + "IndexerValidationCloudFlareCaptchaRequired": "Die Seite ist durch CloudFlare CAPTCHA geschützt. Gültiges CAPTCHA-Token erforderlich.", + "IndexerValidationFeedNotSupported": "Indexer-Feed wird nicht unterstützt: {exceptionMessage}", + "IndexerValidationJackettAllNotSupported": "Jackett's All-Endpunkt wird nicht unterstützt, bitte füge Indexer einzeln hinzu", + "IndexerValidationNoResultsInConfiguredCategories": "Abfrage erfolgreich, aber es wurden keine Ergebnisse in den konfigurierten Kategorien vom Indexer zurückgegeben. Dies könnte ein Problem mit dem Indexer oder deinen Indexer-Kategorieneinstellungen sein.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Indexer unterstützt die aktuelle Abfrage nicht. Überprüfe, ob die Kategorien und/oder die Suche nach Staffeln/Episoden unterstützt wird. Überprüfe das Log für weitere Details.", + "IndexerValidationSearchParametersNotSupported": "Indexer unterstützt die erforderlichen Suchparameter nicht", + "IndexerValidationUnableToConnectHttpError": "Verbindung zum Indexer konnte nicht hergestellt werden, überprüfe deine DNS-Einstellungen und stelle sicher, dass IPv6 funktioniert oder deaktiviert ist. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "Verbindung zum Indexer konnte nicht hergestellt werden, ungültige Anmeldeinformationen. {exceptionMessage}.", + "IndexerValidationUnableToConnectResolutionFailure": "Verbindung zum Indexer konnte nicht hergestellt werden, Verbindungsfehler. Überprüfe deine Verbindung zum Server des Indexers und DNS. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Verbindung zum Indexer konnte nicht hergestellt werden, der Server des Indexers ist nicht verfügbar. Versuche es später erneut. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Verbindung zum Indexer konnte nicht hergestellt werden, möglicherweise aufgrund eines Timeouts. Versuche es erneut oder überprüfe deine Netzwerkeinstellungen. {exceptionMessage}.", + "InstallMajorVersionUpdate": "Update installieren", + "InstallMajorVersionUpdateMessageLink": "Weitere Informationen findest du unter [{domain}]({url}).", + "InstanceName": "Instanzname", + "InstanceNameHelpText": "Instanzname im Tab und für den Syslog-App-Namen", + "InteractiveImport": "Interaktive Importe", + "InteractiveImportNoImportMode": "Ein Importmodus muss ausgewählt werden", + "InteractiveImportNoLanguage": "Sprache(n) müssen für jede ausgewählte Datei gewählt werden", + "InteractiveImportNoSeason": "Staffel muss für jede ausgewählte Datei gewählt werden", + "InteractiveSearch": "Interaktive Suche", + "InteractiveSearchModalHeader": "Interaktive Suche", + "InteractiveSearchResultsSeriesFailedErrorMessage": "Die Suche ist fehlgeschlagen, weil {message}. Versuche, die Serieninformationen zu aktualisieren und überprüfe, ob alle notwendigen Informationen vorhanden sind, bevor du erneut suchst.", + "KeyboardShortcuts": "Tastenkürzel", + "KeyboardShortcutsConfirmModal": "Bestätigungsmodal akzeptieren", + "KeyboardShortcutsSaveSettings": "Einstellungen speichern", + "Languages": "Sprachen", + "LastDuration": "Letzte Dauer", + "LastUsed": "Zuletzt verwendet", + "Level": "Level", + "LibraryImport": "Bibliothek importieren", + "LibraryImportSeriesHeader": "Importiere Serien, die du bereits hast", + "LibraryImportTipsDontUseDownloadsFolder": "Verwende dies nicht zum Importieren von Downloads aus deinem Download-Client, es ist nur für bereits organisierte Bibliotheken, nicht für unsortierte Dateien.", + "LibraryImportTipsQualityInEpisodeFilename": "Stelle sicher, dass deine Dateien die Qualität im Dateinamen enthalten, z.B. `episode.s02e15.bluray.mkv`.", + "LibraryImportTipsSeriesUseRootFolder": "Verweise {appName} auf den Ordner, der alle deine TV-Shows enthält, nicht auf einen spezifischen. Z.B. \"`{goodFolderExample}`\" und nicht \"`{badFolderExample}`\". Jede Serie muss außerdem in ihrem eigenen Ordner innerhalb des Root-/Bibliotheksordners sein.", + "ListSyncLevelHelpText": "Serien in der Bibliothek werden basierend auf deiner Auswahl behandelt, wenn sie von deiner Liste fallen oder nicht darauf erscheinen", + "ListSyncTagHelpText": "Dieses Tag wird hinzugefügt, wenn eine Serie von deiner Liste fällt oder nicht mehr darauf ist", + "Local": "Lokal", + "LocalPath": "Lokaler Pfad", + "LocalStorageIsNotSupported": "Lokale Speicherung wird nicht unterstützt oder ist deaktiviert. Ein Plugin oder privates Browsen könnte sie deaktiviert haben.", + "LogLevelTraceHelpTextWarning": "Die Trace-Protokollierung sollte nur vorübergehend aktiviert werden", + "LogOnly": "Nur Protokollieren", + "LogSizeLimitHelpText": "Maximale Protokolldateigröße in MB, bevor archiviert wird. Standard ist 1MB.", + "Logging": "Protokollierung", + "ManageCustomFormats": "Benutzerdefinierte Formate verwalten", + "ManualGrab": "Manuelles Greifen", + "ManualImport": "Manueller Import", + "MarkAsFailedConfirmation": "Bist du sicher, dass du '{sourceTitle}' als fehlgeschlagen markieren möchtest?", + "Menu": "Menü", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Episoden-Zuordnungen für alle Dateien in der .plexmatch-Datei einfügen", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Erstellt eine .plexmatch-Datei im Serienordner", + "MetadataSettingsEpisodeImages": "Episodenbilder", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Episodenführer für Serienmetadaten", + "MonitorAllEpisodes": "Alle Episoden", + "MetadataSourceSettings": "Einstellungen für Metadatenquelle", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Fügt Bildvorschauen in <filename>.nfo ein (benötigt 'Episodenmetadaten')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Fügt ein JSON-formatiertes Episodenführer-Element in tvshow.nfo ein (benötigt 'Serienmetadaten')", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Fügt die TheTVDB-Show-URL in tvshow.nfo ein (kann mit 'Serienmetadaten' kombiniert werden)", + "MinimumAgeHelpText": "Nur Usenet: Mindestalter in Minuten von NZBs, bevor sie heruntergeladen werden. Verwende dies, um neuen Releases Zeit zu geben, zu deinem Usenet-Anbieter zu propagieren.", + "MinimumCustomFormatScoreIncrement": "Mindeststeigerung des benutzerdefinierten Formatwerts", + "MinimumCustomFormatScoreIncrementHelpText": "Mindestanforderung an die Verbesserung des benutzerdefinierten Formatwerts zwischen bestehenden und neuen Releases, bevor {appName} es als Upgrade betrachtet", + "MinimumFreeSpaceHelpText": "Verhindere den Import, wenn dadurch weniger als dieser Speicherplatz auf der Festplatte verfügbar bleibt", + "MinutesFortyFive": "45 Minuten: {fortyFive}", + "Mixed": "Gemischt", + "MonitorAllEpisodesDescription": "Alle Episoden überwachen, außer Specials", + "MonitorExistingEpisodes": "Vorhandene Episoden", + "MonitorExistingEpisodesDescription": "Episoden überwachen, die Dateien haben oder noch nicht ausgestrahlt wurden", + "MonitorLastSeason": "Letzte Staffel", + "MonitorMissingEpisodesDescription": "Episoden überwachen, die keine Dateien haben oder noch nicht ausgestrahlt wurden", + "MonitorNewSeasons": "Neue Staffeln überwachen", + "MonitorNoEpisodes": "Keine", + "MonitorNoEpisodesDescription": "Es werden keine Episoden überwacht", + "MonitorNoNewSeasonsDescription": "Keine neuen Staffeln automatisch überwachen", + "MonitorSpecialEpisodesDescription": "Alle Special-Episoden überwachen, ohne den überwachten Status anderer Episoden zu ändern", + "MonitoredEpisodesHelpText": "Überwachte Episoden in dieser Serie herunterladen", + "MonitoredStatus": "Überwacht/Status", + "Month": "Monat", + "More": "Mehr", + "MoreDetails": "Mehr Details", + "MoreInfo": "Mehr Infos", + "MoveAutomatically": "Automatisch verschieben", + "MoveFiles": "Dateien verschieben", + "MoveSeriesFoldersToNewPath": "Möchtest du die Serien-Dateien von '{originalPath}' nach '{destinationPath}' verschieben?", + "MoveSeriesFoldersToRootFolder": "Möchtest du die Serienordner nach '{destinationRootFolder}' verschieben?", + "MultiLanguages": "Mehrere Sprachen", + "MustNotContain": "Darf nicht enthalten", + "NoCustomFormatsFound": "Keine benutzerdefinierten Formate gefunden", + "NoDelay": "Keine Verzögerung", + "NoHistoryFound": "Keine Historie gefunden", + "NoMonitoredEpisodesSeason": "Keine überwachten Episoden in dieser Staffel", + "NotificationsAppriseSettingsNotificationType": "Apprise Benachrichtigungstyp", + "NotificationsAppriseSettingsServerUrlHelpText": "Apprise Server-URL, einschließlich http(s):// und Port, falls erforderlich", + "NotificationsAppriseSettingsStatelessUrls": "Apprise Stateless URLs", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Eine oder mehrere URLs, durch Kommas getrennt, die angeben, wohin die Benachrichtigung gesendet werden soll. Leer lassen, wenn persistenter Speicher verwendet wird.", + "NotificationsAppriseSettingsUsernameHelpText": "HTTP Basic Auth Benutzername", + "NotificationsCustomScriptSettingsProviderMessage": "Das Testen führt das Skript mit dem EventType {eventTypeTest} aus, stelle sicher, dass dein Skript dies korrekt verarbeitet", + "NotificationsDiscordSettingsAvatarHelpText": "Ändere den Avatar, der für Nachrichten dieser Integration verwendet wird", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Ändere die Felder, die für diese 'On Import'-Benachrichtigung übergeben werden", + "NotificationsEmailSettingsCcAddress": "CC-Adresse(n)", + "NotificationsEmailSettingsServer": "Server", + "NotificationsEmailSettingsServerHelpText": "Hostname oder IP des E-Mail-Servers", + "NotificationsEmbySettingsSendNotifications": "Benachrichtigungen senden", + "NotificationsEmbySettingsSendNotificationsHelpText": "Lass Emby Benachrichtigungen an konfigurierte Anbieter senden. Nicht unterstützt auf Jellyfin.", + "NotificationsGotifySettingsMetadataLinksHelpText": "Füge Links zu Serienmetadaten hinzu, wenn Benachrichtigungen gesendet werden", + "NotificationsGotifySettingsPreferredMetadataLink": "Bevorzugter Metadaten-Link", + "NotificationsGotifySettingsServerHelpText": "Gotify Server URL, einschließlich http(s):// und Port, falls erforderlich", + "NotificationsJoinSettingsApiKeyHelpText": "Der API-Schlüssel aus deinen Join-Kontoeinstellungen (klicke auf den Join API-Button).", + "NotificationsJoinSettingsDeviceIds": "Geräte-IDs", + "NotificationsJoinSettingsDeviceIdsHelpText": "Veraltet, verwende stattdessen Gerätenamen. Durch Kommas getrennte Liste der Geräte-IDs, an die du Benachrichtigungen senden möchtest. Wenn nicht gesetzt, erhalten alle Geräte Benachrichtigungen.", + "NotificationsJoinSettingsDeviceNamesHelpText": "Durch Kommas getrennte Liste der vollständigen oder teilweisen Gerätenamen, an die du Benachrichtigungen senden möchtest. Wenn nicht gesetzt, erhalten alle Geräte Benachrichtigungen.", + "NotificationsKodiSettingAlwaysUpdate": "Immer aktualisieren", + "NotificationsKodiSettingAlwaysUpdateHelpText": "Bibliothek auch aktualisieren, wenn ein Video abgespielt wird?", + "NotificationsKodiSettingsCleanLibrary": "Bibliothek bereinigen", + "NotificationsKodiSettingsDisplayTime": "Anzeigezeit", + "NotificationsKodiSettingsGuiNotification": "GUI-Benachrichtigung", + "NotificationsLoadError": "Benachrichtigungen können nicht geladen werden", + "NotificationsMailgunSettingsUseEuEndpoint": "EU-Endpunkt verwenden", + "NotificationsNtfySettingsAccessTokenHelpText": "Optionale tokenbasierte Authentifizierung. Hat Vorrang vor Benutzername/Passwort", + "NotificationsNtfySettingsClickUrlHelpText": "Optionaler Link, wenn der Benutzer auf die Benachrichtigung klickt", + "NotificationsNtfySettingsTagsEmojis": "Ntfy-Tags und Emojis", + "NotificationsNtfySettingsTagsEmojisHelpText": "Optionale Liste von Tags oder Emojis zur Verwendung", + "NotificationsNtfySettingsTopicsHelpText": "Liste von Themen, an die Benachrichtigungen gesendet werden sollen", + "NotificationsNtfySettingsUsernameHelpText": "Optionaler Benutzername", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Mit Plex.tv authentifizieren", + "NotificationsPlexSettingsServer": "Server", + "NotificationsPlexSettingsServerHelpText": "Wählen Sie den Server aus dem Plex.tv-Konto nach der Authentifizierung", + "NotificationsPushoverSettingsSound": "Ton", + "NotificationsPushBulletSettingsAccessToken": "Access-Token", + "NotificationsPushBulletSettingsChannelTags": "Kanal-Tags", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Liste von Kanal-Tags, an die Benachrichtigungen gesendet werden sollen", + "NotificationsPushBulletSettingsDeviceIds": "Geräte-IDs", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Liste von Geräte-IDs (leer lassen, um an alle Geräte zu senden)", + "NotificationsPushoverSettingsExpire": "Ablauf", + "NotificationsPushoverSettingsExpireHelpText": "Maximale Zeit, um Notfallwarnungen zu wiederholen, maximal 86400 Sekunden", + "NotificationsPushoverSettingsRetry": "Wiederholen", + "NotificationsPushoverSettingsSoundHelpText": "Benachrichtigungston, leer lassen, um den Standardton zu verwenden", + "NotificationsPushoverSettingsUserKey": "Benutzer-Schlüssel", + "NotificationsSettingsUpdateLibrary": "Bibliothek aktualisieren", + "NotificationsSimplepushSettingsEventHelpText": "Verhalten der Push-Benachrichtigungen anpassen", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Synoindex auf dem lokalen Host aufrufen, um eine Bibliotheksdatei zu aktualisieren", + "NotificationsSynologyValidationTestFailed": "Kein Synology oder synoindex nicht verfügbar", + "NotificationsTagsSeriesHelpText": "Nur Benachrichtigungen für Serien mit mindestens einem passenden Tag senden", + "NotificationsTelegramSettingsChatIdHelpText": "Du musst eine Unterhaltung mit dem Bot starten oder ihn zu deiner Gruppe hinzufügen, um Nachrichten zu erhalten", + "NotificationsTelegramSettingsIncludeAppName": "{appName} im Titel einfügen", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optional den Nachrichtentitel mit {appName} voranstellen, um Benachrichtigungen von verschiedenen Anwendungen zu unterscheiden", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Füge Links zu den Serienmetadaten hinzu, wenn Benachrichtigungen gesendet werden", + "NotificationsTelegramSettingsSendSilently": "Still senden", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Sendet die Nachricht still. Nutzer erhalten eine Benachrichtigung ohne Ton", + "NotificationsTraktSettingsAuthUser": "Auth-Benutzer", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Mit Trakt authentifizieren", + "NotificationsTwitterSettingsAccessTokenSecret": "Access-Token-Geheimnis", + "NotificationsValidationInvalidApiKeyExceptionMessage": "API-Schlüssel ist ungültig: {exceptionMessage}", + "NotificationsValidationUnableToConnectToApi": "Verbindung zum {service} API konnte nicht hergestellt werden. Serververbindung fehlgeschlagen: ({responseCode}) {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Kann keine Testnachricht senden. Antwort von der API: {error}", + "NzbgetHistoryItemMessage": "PAR-Status: {parStatus} - Entpack-Status: {unpackStatus} - Verschiebe-Status: {moveStatus} - Script-Status: {scriptStatus} - Lösch-Status: {deleteStatus} - Markierungs-Status: {markStatus}", + "Ok": "Ok", + "OnFileImport": "Bei Dateiimport", + "OnHealthIssue": "Bei Gesundheitsproblem", + "OnHealthRestored": "Bei Wiederherstellung der Gesundheit", + "OnImportComplete": "Bei abgeschlossenem Import", + "OnManualInteractionRequired": "Bei erforderlicher manueller Interaktion", + "OnSeriesAdd": "Bei Serienhinzufügung", + "OnlyUsenet": "Nur Usenet", + "OpenBrowserOnStartHelpText": " Öffne einen Webbrowser und navigiere zur {appName}-Homepage beim Start der App.", + "OpenSeries": "Serie öffnen", + "OrganizeModalHeaderSeason": "Organisieren & Umbenennen - {season}", + "OrganizeNothingToRename": "Erfolg! Meine Arbeit ist erledigt, keine Dateien zum Umbenennen.", + "OrganizeSelectedSeriesModalAlert": "Tipp: Um eine Umbenennung in der Vorschau zu sehen, wähle 'Abbrechen' und dann einen Serientitel aus und benutze dieses Symbol:", + "OrganizeSelectedSeriesModalConfirmation": "Bist du sicher, dass du alle Dateien in den {count} ausgewählten Serien organisieren möchtest?", + "OverrideAndAddToDownloadQueue": "Überschreiben und in die Download-Warteschlange einfügen", + "OverrideGrabNoLanguage": "Mindestens eine Sprache muss ausgewählt werden", + "PackageVersionInfo": "{packageVersion} von {packageAuthor}", + "ParseModalHelpText": "Gib einen Release-Titel im Eingabefeld oben ein", + "PendingChangesDiscardChanges": "Änderungen verwerfen und verlassen", + "PortNumber": "Portnummer", + "PostImportCategory": "Post-Import-Kategorie", + "PosterOptions": "Poster-Optionen", + "Posters": "Poster", + "PreferAndUpgrade": "Bevorzugen und Upgrade", + "PreferUsenet": "Bevorzuge Usenet", + "Preferred": "Bevorzugt", + "PreferredSize": "Bevorzugte Größe", + "PreviousAiring": "Vorherige Ausstrahlung", + "PreviousAiringDate": "Vorherige Ausstrahlung: {date}", + "PrioritySettings": "Priorität: {priority}", + "Profiles": "Profile", + "QualitiesHelpText": "Qualitäten, die weiter oben in der Liste stehen, werden bevorzugt. Qualitäten innerhalb derselben Gruppe sind gleichwertig. Nur angekreuzte Qualitäten werden gewünscht", + "ReleaseGroupFootNote": "Optional die Trunkierung auf eine maximale Byteanzahl inkl. Auslassungszeichen (`...`) steuern. Trunkierung vom Ende (z. B. `{Release Group:30}`) oder vom Anfang (z. B. `{Release Group:-30}`) sind beide möglich.", + "ReleaseProfileIndexerHelpTextWarning": "Das Festlegen eines bestimmten Indexers für ein Release-Profil sorgt dafür, dass dieses Profil nur für Releases von diesem Indexer gilt.", + "ReleaseProfileTagSeriesHelpText": "Release-Profile gelten für Serien mit mindestens einem passenden Tag. Lass das Feld leer, um es auf alle Serien anzuwenden", + "ReleaseProfiles": "Release-Profile", + "ReleaseSceneIndicatorMappedNotRequested": "Abgebildete Episode wurde in dieser Suche nicht angefordert.", + "ReleaseSceneIndicatorUnknownMessage": "Die Nummerierung variiert für diese Episode, und das Release entspricht keiner bekannten Abbildung.", + "ReleaseTitle": "Release-Titel", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Du verwendest Docker; der Download-Client {downloadClientName} platziert Downloads in {path}, aber dies ist kein gültiger {osName}-Pfad. Überprüfe deine Remote-Pfad-Abgleichungen und Download-Client-Einstellungen.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} kann die heruntergeladene Episode {path} sehen, aber nicht darauf zugreifen. Wahrscheinlich ein Berechtigungsfehler.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Der Remote-Download-Client {downloadClientName} meldet Dateien in {path}, aber dies ist kein gültiger {osName}-Pfad. Überprüfe deine Remote-Pfad-Abgleichungen und Download-Client-Einstellungen.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} kann das Download-Verzeichnis {downloadPath} sehen, aber nicht darauf zugreifen. Wahrscheinlich ein Berechtigungsfehler.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Der Download-Client {downloadClientName} platziert Downloads in {path}, aber {appName} kann dieses Verzeichnis nicht sehen. Möglicherweise müssen die Ordnersberechtigungen angepasst werden.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Der Remote-Download-Client {downloadClientName} legt Downloads in {path} ab, aber dies ist kein gültiger {osName}-Pfad. Überprüfe die Remote-Pfadzuordnungen und die Einstellungen des Download-Clients.", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Aus dem Download-Client entfernen' wird den Download und die Datei(en) aus dem Download-Client löschen.", + "RssIsNotSupportedWithThisIndexer": "RSS wird mit diesem Indexer nicht unterstützt", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Aus dem Download-Client entfernen' wird die Downloads und die Dateien aus dem Download-Client löschen.", + "RenameEpisodesHelpText": "{appName} wird den bestehenden Dateinamen verwenden, wenn das Umbenennen deaktiviert ist", + "RenameFiles": "Dateien umbenennen", + "Reorder": "Neu anordnen", + "ReplaceWithSpaceDashSpace": "Mit Leerzeichen Bindestrich Leerzeichen ersetzen", + "RequiredHelpText": "Diese {implementationName}-Bedingung muss übereinstimmen, damit das benutzerdefinierte Format angewendet wird. Andernfalls genügt eine einzelne {implementationName}-Übereinstimmung.", + "Retention": "Aufbewahrung", + "RootFolderMissingHealthCheckMessage": "Fehlender Root-Ordner: {rootFolderPath}", + "RootFolderPath": "Root-Ordner-Pfad", + "RootFolders": "Root-Ordner", + "RootFoldersLoadError": "Kann Root-Ordner nicht laden", + "RssSyncIntervalHelpTextWarning": "Dies gilt für alle Indexer. Bitte befolge die von ihnen festgelegten Regeln", + "SearchForAllMissingEpisodes": "Suche nach allen fehlenden Episoden", + "SearchForAllMissingEpisodesConfirmationCount": "Bist du sicher, dass du nach allen {totalRecords} fehlenden Episoden suchen möchtest?", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Bist du sicher, dass du nach allen {totalRecords} nicht erfüllten Cutoff-Episoden suchen möchtest?", + "SearchForMonitoredEpisodes": "Suche nach überwachten Episoden", + "SearchForQuery": "Suche nach {query}", + "SearchSelected": "Ausgewählte durchsuchen", + "SeasonsMonitoredAll": "Alle", + "SeasonsMonitoredPartial": "Teilweise", + "SeriesIsMonitored": "Serie ist überwacht", + "ShowSearchHelpText": "Suchschaltfläche beim Überfahren anzeigen", + "ShowSeriesTitleHelpText": "Serientitel unter dem Poster anzeigen", + "SkipRedownload": "Neu-Download überspringen", + "SmartReplace": "Smart Replace", + "SmartReplaceHint": "Dash oder Space Dash je nach Name", + "SupportedAutoTaggingProperties": "{appName} unterstützt die folgenden Eigenschaften für Auto-Tagging-Regeln", + "SupportedCustomConditions": "{appName} unterstützt benutzerdefinierte Bedingungen für die Release-Eigenschaften unten.", + "SupportedDownloadClients": "{appName} unterstützt viele populäre Torrent- und Usenet-Download-Clients.", + "SupportedDownloadClientsMoreInfo": "Für mehr Informationen zu den einzelnen Download-Clients klicke auf die 'Mehr Infos'-Schaltflächen.", + "SupportedIndexers": "{appName} unterstützt jeden Indexer, der den Newznab-Standard verwendet, sowie andere Indexer, die unten aufgelistet sind.", + "SupportedListsMoreInfo": "Für mehr Informationen zu den einzelnen Listen klicke auf die 'Mehr Infos'-Schaltflächen.", + "TableColumns": "Spalten", + "TableColumnsHelpText": "Wähle aus, welche Spalten sichtbar sind und in welcher Reihenfolge sie angezeigt werden", + "TableOptions": "Tabellenoptionen", + "TablePageSize": "Seitengröße", + "TablePageSizeHelpText": "Anzahl der Elemente, die auf jeder Seite angezeigt werden", + "TablePageSizeMaximum": "Die Seitengröße darf {maximumValue} nicht überschreiten", + "TagCannotBeDeletedWhileInUse": "Tag kann nicht gelöscht werden, solange es verwendet wird", + "UnableToImportAutomatically": "Kann nicht automatisch importiert werden", + "UnknownEventTooltip": "Unbekanntes Ereignis", + "UnmappedFolders": "Nicht zugeordnete Ordner", + "UnmonitorSpecialsEpisodesDescription": "Alle Special-Episoden nicht mehr überwachen, ohne den Überwachungsstatus anderer Episoden zu ändern", + "UpdatePath": "Update-Pfad", + "UpdateSeriesPath": "Update-Serie-Pfad", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Sobald dieser benutzerdefinierte Formatwert erreicht ist, wird {appName} keine Episoden-Releases mehr herunterladen", + "UpgradesAllowedHelpText": "Wenn deaktiviert, werden Qualitäten nicht aktualisiert.", + "VideoDynamicRange": "Video-Dynamikbereich", + "Warning": "Warnung" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 52777ae4f..c23bf9d2e 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -45,7 +45,7 @@ "TestParsing": "测试解析", "ApplyTagsHelpTextAdd": "添加: 添加标签至已有的标签列表中", "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选中的索引器", - "AppDataLocationHealthCheckMessage": "无法更新,以防止在更新时删除 AppData", + "AppDataLocationHealthCheckMessage": "为防止在更新时删除 AppData,更新将无法进行", "BlocklistRelease": "发布资源黑名单", "BlocklistReleases": "发布资源黑名单", "CloneCustomFormat": "复制自定义格式", @@ -961,7 +961,7 @@ "LocalStorageIsNotSupported": "不支持或禁用本地存储。插件或私人浏览可能已将其禁用。", "ManualGrab": "手动抓取", "ManualImport": "手动导入", - "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答]({url})获取更多信息。", + "MappedNetworkDrivesWindowsService": "作为 Windows 服务运行时,映射的网络驱动器不可用,请参阅 [FAQ]({url}) 获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", From 04ebf03fb58c446fc95736e05579d80281676a2d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 8 Dec 2024 05:22:54 +0000 Subject: [PATCH 683/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Michaa85 <michael.seipel@gmx.de> Co-authored-by: Rodion <rodyon009@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: farebyting <farelbyting@gmail.com> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: keysuck <joshkkim@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/id/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ko/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 6 +- src/NzbDrone.Core/Localization/Core/id.json | 5 +- src/NzbDrone.Core/Localization/Core/ko.json | 33 +- .../Localization/Core/pt_BR.json | 6 +- src/NzbDrone.Core/Localization/Core/tr.json | 1303 ++++++++++++++++- src/NzbDrone.Core/Localization/Core/uk.json | 7 +- 7 files changed, 1325 insertions(+), 37 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 02e3a38fe..53fb847b7 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -22,7 +22,7 @@ "Language": "Sprache", "CloneCondition": "Bedingung klonen", "DeleteCondition": "Bedingung löschen", - "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?", + "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{0}' löschen willst?", "DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?", "RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?", "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index d1135cb29..a629993ff 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2137,5 +2137,9 @@ "Menu": "Menú", "Premiere": "Estreno", "UpdateSeriesPath": "Actualizar Ruta de Series", - "UpdatePath": "Actualizar Ruta" + "UpdatePath": "Actualizar Ruta", + "MetadataKometaDeprecatedSetting": "Obsoleto", + "MetadataKometaDeprecated": "Los archivos de Kometa no seguirán siendo creados, se eliminará completamente el soporte en la v5", + "IndexerSettingsFailDownloadsHelpText": "Mientras se procesan las descargas completadas, {appName} tratará los errores seleccionados evitando la importación como descargas fallidas.", + "IndexerSettingsFailDownloads": "Fallo de Descargas" } diff --git a/src/NzbDrone.Core/Localization/Core/id.json b/src/NzbDrone.Core/Localization/Core/id.json index 7a47d2b22..f0f5f3ebb 100644 --- a/src/NzbDrone.Core/Localization/Core/id.json +++ b/src/NzbDrone.Core/Localization/Core/id.json @@ -134,5 +134,8 @@ "Search": "Cari", "ShowEpisodes": "Tampilkan episode", "Refresh": "Muat Ulang", - "CalendarLegendEpisodeOnAirTooltip": "Episode sedang tayang" + "CalendarLegendEpisodeOnAirTooltip": "Episode sedang tayang", + "AddCustomFormatError": "Tidak dapat menambahkan format khusus baru, coba lagi.", + "AddDelayProfile": "Tambah Delay Profile", + "AddDownloadClient": "Tambahkan Download Client" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 9956d460d..132a133c5 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -8,7 +8,7 @@ "AddToDownloadQueue": "다운로드 대기열에 추가됨", "NoHistory": "내역 없음", "SelectAll": "모두 선택", - "View": "표시 변경", + "View": "화면", "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다", "AddNew": "새로 추가하기", "History": "내역", @@ -26,5 +26,34 @@ "AddingTag": "태그 추가", "Analytics": "분석", "Age": "연령", - "All": "모두" + "All": "모두", + "UsenetBlackholeNzbFolder": "Nzb 폴더", + "UrlBase": "URL 기반", + "TypeOfList": "{typeOfList} 목록", + "UpdateMechanismHelpText": "{appName}의 내장 업데이트 도구 또는 스크립트 사용", + "TorrentDelayHelpText": "토렌트를 잡기 전에 대기까지 소요되는 지연 (분)", + "TorrentDelay": "토렌트 지연", + "Torrents": "토렌트", + "TorrentDelayTime": "토렌트 지연: {0torrentDelay}", + "Unavailable": "사용 불가능", + "UnknownEventTooltip": "알 수 없는 이벤트", + "Warning": "경고", + "VideoDynamicRange": "동영상 다이나믹 레인지", + "VisitTheWikiForMoreDetails": "자세한 내용은 위키를 방문하세요: ", + "WouldYouLikeToRestoreBackup": "'{name}' 백업을 복원하시겠습니까?", + "XmlRpcPath": "XML RPC 경로", + "YesterdayAt": "어제 {time}", + "AddDelayProfileError": "새 지연 프로필을 추가할 수 없습니다. 다시 시도해주세요.", + "AddConditionError": "새 조건을 추가 할 수 없습니다. 다시 시도해주세요.", + "AddConditionImplementation": "조건 추가 - {implementationName}", + "AddImportList": "가져오기 목록 추가", + "AddImportListImplementation": "가져오기 목록 추가 - {implementationName}", + "UpdateAvailableHealthCheckMessage": "새 업데이트 사용 가능: {version}", + "UsenetBlackhole": "유즈넷 블랙홀", + "AddAutoTag": "자동 태그 추가", + "AddAutoTagError": "새 자동 태그을 추가 할 수 없습니다. 다시 시도해주세요.", + "AddCondition": "조건 추가", + "AddIndexerError": "새 인덱서를 추가 할 수 없습니다. 다시 시도해주세요.", + "TorrentBlackholeTorrentFolder": "토렌트 폴더", + "UseSsl": "SSL 사용" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 7221f82d3..d090b4e41 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2138,5 +2138,9 @@ "Menu": "Menu", "NotificationsSettingsWebhookHeaders": "Cabeçalhos", "UpdatePath": "Caminho da Atualização", - "UpdateSeriesPath": "Atualizar Caminho da Série" + "UpdateSeriesPath": "Atualizar Caminho da Série", + "MetadataKometaDeprecated": "Os arquivos Kometa não serão mais criados, o suporte será completamente removido na v5", + "MetadataKometaDeprecatedSetting": "Deprecado", + "IndexerSettingsFailDownloads": "Downloads com Falhas", + "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará os erros selecionados, impedindo a importação, como downloads com falha." } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 0d34bb52f..75ebbb2e3 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -5,9 +5,9 @@ "AddAutoTag": "Otomatik Etiket Ekle", "AddConnection": "Bağlantı Ekle", "AddConditionImplementation": "Koşul Ekle - {implementationName}", - "EditConnectionImplementation": "Koşul Ekle - {implementationName}", + "EditConnectionImplementation": "Bildirimi Düzenle - {implementationName}", "AddConnectionImplementation": "Bağlantı Ekle - {implementationName}", - "AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}", + "AddIndexerImplementation": "Yeni Dizinleyici Ekle - {implementationName}", "EditIndexerImplementation": "Koşul Ekle - {implementationName}", "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", "AddedToDownloadQueue": "İndirme kuyruğuna eklendi", @@ -29,7 +29,7 @@ "AddDelayProfileError": "Yeni bir gecikme profili eklenemiyor, lütfen tekrar deneyin.", "AddImportList": "İçe Aktarım Listesi Ekle", "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", - "AddImportListExclusionError": "Yeni bir içe aktarım listesi dışlaması eklenemiyor, lütfen tekrar deneyin.", + "AddImportListExclusionError": "Yeni bir liste dışlaması eklenemiyor, lütfen tekrar deneyin.", "AddImportListImplementation": "İçe Aktarım Listesi Ekle -{implementationName}", "AddIndexer": "Dizinleyici Ekle", "AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın", @@ -48,7 +48,7 @@ "AddAutoTagError": "Yeni bir otomatik etiket eklenemiyor, lütfen tekrar deneyin.", "AddConditionError": "Yeni bir koşul eklenemiyor, lütfen tekrar deneyin.", "AddCustomFormat": "Özel Format Ekle", - "AddCustomFormatError": "Yeni bir özel biçim eklenemiyor, lütfen tekrar deneyin.", + "AddCustomFormatError": "Yeni bir özel format eklenemiyor, lütfen tekrar deneyin.", "AddDelayProfile": "Gecikme Profili Ekleme", "AddNewSeries": "Yeni Dizi Ekle", "AddNewSeriesError": "Arama sonuçları yüklenemedi, lütfen tekrar deneyin.", @@ -115,7 +115,7 @@ "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}'", + "CustomFormatUnknownCondition": "Bilinmeyen Özel Format koşulu '{implementation}'", "CustomFormatsSpecificationRegularExpression": "Düzenli ifade", "AppDataDirectory": "Uygulama Veri Dizini", "ChownGroup": "Chown Grubu", @@ -156,7 +156,7 @@ "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", "DeleteReleaseProfile": "Yayımlama Profilini Sil", "DeleteSelectedIndexers": "Dizinleyicileri Sil", - "Directory": "Rehber", + "Directory": "Dizin", "Donate": "Bağış yap", "DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil", "DownloadClientFloodSettingsAdditionalTags": "Ek Etiketler", @@ -171,7 +171,7 @@ "DeleteRootFolder": "Kök Klasörü Sil", "DeleteSpecification": "Spesifikasyonu Sil", "DeletedReasonManual": "Dosya, {appName} kullanılarak manuel olarak veya API aracılığıyla başka bir araçla silindi", - "DeleteCustomFormatMessageText": "'{name}' özel biçimini silmek istediğinizden emin misiniz?", + "DeleteCustomFormatMessageText": "'{name}' özel formatı silmek istediğinizden emin misiniz?", "DefaultNameCopiedSpecification": "{name} - Kopyala", "DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?", "DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", @@ -255,12 +255,12 @@ "DownloadClientQbittorrentTorrentStateUnknown": "Bilinmeyen indirme durumu: {state}", "DownloadClientQbittorrentValidationCategoryRecommended": "Kategori önerilir", "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName}, tamamlanan indirmeleri kategorisi olmadan içe aktarmaya çalışmaz.", - "DownloadClientRTorrentSettingsAddStopped": "Ekleme Durduruldu", + "DownloadClientRTorrentSettingsAddStopped": "Durdurulana Ekle", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Tarih Sıralamayı Devre Dışı Bırak", "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Film sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.", "DownloadClientSabnzbdValidationUnknownVersion": "Bilinmeyen Sürüm: {rawVersion}", "DownloadClientSabnzbdValidationEnableJobFolders": "İş klasörlerini etkinleştir", - "DownloadClientSettingsAddPaused": "Ekleme Durduruldu", + "DownloadClientSettingsAddPaused": "Duraklatılana Ekle", "DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın", "DownloadClientSettingsInitialState": "Başlangıç Durumu", "DownloadClientSettingsPostImportCategoryHelpText": "{appName}'in indirmeyi içe aktardıktan sonra ayarlayacağı kategori. {appName}, tohumlama tamamlansa bile bu kategorideki torrentleri kaldırmaz. Aynı kategoriyi korumak için boş bırakın.", @@ -273,7 +273,7 @@ "DownloadClientPneumaticSettingsStrmFolder": "Strm Klasörü", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Bu klasöre XBMC'den erişilmesi gerekecek", "DownloadClientPriorityHelpText": "İstemci Önceliğini 1'den (En Yüksek) 50'ye (En Düşük) indirin. Varsayılan: 1. Aynı önceliğe sahip istemciler için Round-Robin kullanılır.", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName}, etiketi qBittorrent'e ekleyemedi.", "DownloadClientQbittorrentValidationQueueingNotEnabled": "kuyruğa Alma Etkin Değil", @@ -299,7 +299,7 @@ "DownloadClientValidationUnableToConnectDetail": "Lütfen ana bilgisayar adını ve bağlantı noktasını doğrulayın.", "EditImportListImplementation": "İçe Aktarma Listesini Düzenle - {implementationName}", "DownloadClientQbittorrentSettingsContentLayout": "İçerik Düzeni", - "DownloadClientSettingsRecentPriority": "Yeni Öncelik", + "DownloadClientSettingsRecentPriority": "Yeni Önceliği", "DownloadClientUTorrentTorrentStateError": "uTorrent bir hata bildirdi", "DownloadClientValidationApiKeyIncorrect": "API Anahtarı Yanlış", "DownloadClientValidationGroupMissingDetail": "Girdiğiniz grup {clientName} içinde mevcut değil. Önce bunu {clientName} içinde oluşturun.", @@ -406,13 +406,13 @@ "FormatDateTime": "{formattedDate} {formattedTime}", "FormatRuntimeHours": "{hours}s", "LanguagesLoadError": "Diller yüklenemiyor", - "ListWillRefreshEveryInterval": "Liste her {refreshInterval} yenilenecektir", + "ListWillRefreshEveryInterval": "Liste yenileme periyodu {refreshInterval}dır", "ManageIndexers": "Dizinleyicileri Yönet", "ManualGrab": "Manuel Yakalama", "DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri", "DownloadClients": "İndirme İstemcileri", "InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı", - "ListQualityProfileHelpText": "Kalite Profili listesi öğeleri şu şekilde eklenecektir:", + "ListQualityProfileHelpText": "Kalite Profili liste öğeleri eklenecektir", "MustNotContainHelpText": "Yayın, bir veya daha fazla terim içeriyorsa reddedilir (büyük/ küçük harfe duyarsız)", "NoDownloadClientsFound": "İndirme istemcisi bulunamadı", "NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}", @@ -434,7 +434,7 @@ "LogFilesLocation": "Günlük dosyaları şu konumda bulunur: {location}", "ImportUsingScript": "Komut Dosyası Kullanarak İçe Aktar", "IncludeHealthWarnings": "Sağlık Uyarılarını Dahil Et", - "IndexerSettingsMultiLanguageRelease": "Çok dil", + "IndexerSettingsMultiLanguageRelease": "Çoklu Dil", "IndexerSettingsMultiLanguageReleaseHelpText": "Bu indeksleyicideki çoklu sürümde normalde hangi diller bulunur?", "InteractiveImportNoImportMode": "Bir içe aktarma modu seçilmelidir", "InteractiveImportNoQuality": "Seçilen her dosya için kalite seçilmelidir", @@ -483,7 +483,7 @@ "NotificationsGotifySettingsAppToken": "Uygulama Jetonu", "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", - "Test": "Sına", + "Test": "Test Et", "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", "IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent yakalandıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", @@ -536,7 +536,7 @@ "DeleteRemotePathMappingMessageText": "Bu uzak yol eşlemesini silmek istediğinizden emin misiniz?", "QualitiesLoadError": "Nitelikler yüklenemiyor", "SelectAll": "Hepsini Seç", - "SslCertPasswordHelpText": "Pfx dosyasının şifresi", + "SslCertPasswordHelpText": "Pfx dosyası için şifre", "SslCertPathHelpText": "Pfx dosyasının yolu", "TorrentBlackholeSaveMagnetFilesReadOnly": "Sadece oku", "NotificationsSlackSettingsChannelHelpText": "Gelen webhook için varsayılan kanalı geçersiz kılar (#diğer kanal)", @@ -558,7 +558,7 @@ "SetIndexerFlags": "Dizinleyici Bayraklarını Ayarla", "SetReleaseGroup": "Yayımlama Grubunu Ayarla", "SetIndexerFlagsModalTitle": "{modalTitle} - Dizinleyici Bayraklarını Ayarla", - "SslCertPassword": "SSL Sertifika Şifresi", + "SslCertPassword": "SSL Sertifika Parolası", "Rating": "Puan", "GrabRelease": "Yayın Yakalama", "NotificationsNtfyValidationAuthorizationRequired": "Yetkilendirme gerekli", @@ -659,13 +659,13 @@ "ResetDefinitions": "Tanımları Sıfırla", "RestartRequiredToApplyChanges": "{appName}, değişikliklerin uygulanabilmesi için yeniden başlatmayı gerektiriyor. Şimdi yeniden başlatmak istiyor musunuz?", "RetryingDownloadOn": "{date} tarihinde, {time} itibarıyla indirme işlemi yeniden deneniyor", - "RssSyncIntervalHelpText": "Dakika cinsinden periyot. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın yakalamayı durduracaktır)", + "RssSyncIntervalHelpText": "Dakika cinsinden aralık. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın yakalamayı durduracaktır)", "TorrentBlackhole": "Blackhole Torrent", "PrioritySettings": "Öncelik: {priority}", "SubtitleLanguages": "Altyazı Dilleri", "OrganizeNothingToRename": "Başarılı! İşim bitti, yeniden adlandırılacak dosya yok.", "MinimumCustomFormatScore": "Minimum Özel Format Puanı", - "MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin alınmadan önceki dakika cinsinden minimum yaşı. Yeni yayınların usenet sağlayıcınıza yayılması için zaman tanımak için bunu kullanın.", + "MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin yakalanmadan önceki minimum geçen süre (dakika cinsinden). Bunu, yeni sürümlerin usenet sağlayıcınıza yayılması için zaman vermek amacıyla kullanın.", "QueueLoadError": "Kuyruk yüklenemedi", "SelectDropdown": "Seçimler...", "NotificationsSettingsUpdateMapPathsTo": "Harita Yolları", @@ -786,7 +786,7 @@ "NotificationsSlackSettingsUsernameHelpText": "Slack'e gönderilecek kullanıcı adı", "QueueFilterHasNoItems": "Seçilen kuyruk filtresinde hiç öğe yok", "ReleaseGroups": "Yayımlama Grupları", - "IncludeCustomFormatWhenRenamingHelpText": "{Custom Formats} yeniden adlandırma formatına dahil et", + "IncludeCustomFormatWhenRenamingHelpText": "Özel formatları yeniden adlandırma formatına dahil et", "Logging": "Loglama", "MinutesSixty": "60 Dakika: {sixty}", "SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin", @@ -816,13 +816,13 @@ "MissingNoItems": "Eksik öğe yok", "CutoffUnmet": "Kesinti Karşılanmayan", "Monitor": "Takip", - "CutoffUnmetLoadError": "Karşılanmamış kesinti öğeleri yüklenirken hata oluştu", + "CutoffUnmetLoadError": "Kesinti karşılanmayan öğeleri yükleme hatası", "Monitored": "Takip Ediliyor", "MonitoredOnly": "Sadece Takip Edilen", "External": "Harici", "MassSearchCancelWarning": "Bu işlem, {appName} yeniden başlatılmadan veya tüm dizin oluşturucularınız devre dışı bırakılmadan başlatılır ise iptal edilemez.", "IncludeUnmonitored": "Takip Edilmeyenleri Dahil Et", - "MonitorSelected": "Takip Edilen Seçildi", + "MonitorSelected": "Seçilenleri Bırak", "MonitoredStatus": "Takip Edilen/Durum", "UnmonitorSelected": "Seçili Takipleri Kaldır", "ShowMonitored": "Takip Edilenleri Göster", @@ -838,10 +838,10 @@ "Required": "Gerekli", "AirsTbaOn": "Daha sonra duyurulacak {networkLabel}'de", "AllFiles": "Tüm dosyalar", - "AllSeriesAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir", + "AllSeriesAreHiddenByTheAppliedFilter": "Tüm sonuçlar uygulanan filtre tarafından gizlendi", "Always": "Her zaman", "AirsDateAtTimeOn": "{date} saat {time} {networkLabel}'de", - "AllResultsAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir", + "AllResultsAreHiddenByTheAppliedFilter": "Tüm sonuçlar uygulanan filtre tarafından gizlendi", "AllSeriesInRootFolderHaveBeenImported": "{path} içerisindeki tüm diziler içeri aktarıldı", "AlternateTitles": "Alternatif Başlıklar", "AnEpisodeIsDownloading": "Bir bölüm indiriliyor", @@ -856,7 +856,7 @@ "CountVotes": "{votes} oy", "UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}", "MinimumCustomFormatScoreIncrement": "Minimum Özel Format Puanı Artışı", - "MinimumCustomFormatScoreIncrementHelpText": "{appName}'in bunu bir yükseltme olarak değerlendirmesi için mevcut ve yeni sürümler arasında özel biçim puanında gereken minimum iyileştirme", + "MinimumCustomFormatScoreIncrementHelpText": "{appName}'in bunu bir yükseltme olarak değerlendirmesi için mevcut ve yeni sürümler arasında özel format puanında gereken minimum iyileştirme", "SkipFreeSpaceCheckHelpText": "{appName} kök klasörünüzde boş alan tespit edemediğinde bunu kullansın", "DayOfWeekAt": "{day}, {time} saatinde", "Logout": "Çıkış", @@ -884,20 +884,1263 @@ "Warning": "Uyarı", "AirsTimeOn": "{time} {networkLabel} üzerinde", "AirsTomorrowOn": "Yarın {time}'da {networkLabel}'da", - "AppUpdatedVersion": "{appName} `{version}` sürümüne güncellendi, en son değişiklikleri almak için {appName} uygulamasını yeniden başlatmanız gerekiyor ", + "AppUpdatedVersion": "{appName}, `{version}` sürümüne güncellendi; en son değişikliklerin etkin olabilmesi için {appName} uygulamasını yeniden başlatmanız gerekli ", "ApplyTags": "Etiketleri Uygula", - "AnalyseVideoFiles": "Video dosyalarını analiz edin", + "AnalyseVideoFiles": "Video Dosyalarını Analiz Et", "Anime": "Anime", "AnimeEpisodeFormat": "Anime Bölüm Formatı", "AudioInfo": "Ses Bilgisi", "AuthBasic": "Temel (Tarayıcı Açılır Penceresi)", - "ApplyTagsHelpTextHowToApplySeries": "Seçili serilere etiketler nasıl uygulanır?", + "ApplyTagsHelpTextHowToApplySeries": "Seçili dizilere etiketler nasıl uygulanır?", "AptUpdater": "Güncellemeyi yüklemek için apt'ı kullanın", "AnalyseVideoFilesHelpText": "Çözünürlük, çalışma zamanı ve kodek bilgileri gibi video bilgilerini dosyalardan çıkarın. Bu, {appName} uygulamasının taramalar sırasında yüksek disk veya ağ etkinliğine neden olabilecek dosyanın bölümlerini okumasını gerektirir.", "Delay": "Gecikme", "Fallback": "Geri Çek", - "ManageFormats": "Biçimleri Yönet", + "ManageFormats": "Formatları Yönet", "FavoriteFolderAdd": "Favori Klasör Ekle", "FavoriteFolderRemove": "Favori Klasörü Kaldır", - "FavoriteFolders": "Favori Klasörler" + "FavoriteFolders": "Favori Klasörler", + "ShowDateAdded": "Eklenme Tarihi Göster", + "System": "Sistem", + "Wiki": "Wiki", + "Yesterday": "Dün", + "MinimumFreeSpace": "Minimum Boş Alan", + "ExtraFileExtensionsHelpText": "İçe aktarılacak ekstra dosyaların virgülle ayrılmış listesi (.nfo, .nfo-orig olarak içe aktarılacaktır)", + "AutoTaggingSpecificationTag": "Etiket", + "CancelPendingTask": "Bekleyen görevi iptal etmek istediğinizden emin misiniz?", + "CheckDownloadClientForDetails": "daha fazla ayrıntı için indirme istemcisini kontrol edin", + "ChownGroupHelpText": "Grup adı veya gid. Uzak dosya sistemleri için gid kullanın.", + "ErrorLoadingContents": "İçerik yüklenirken hata oluştu", + "HttpHttps": "HTTP (S)", + "EpisodeFileDeleted": "Bölüm Dosyası Silindi", + "ShowPath": "Yolu Göster", + "AutoTaggingSpecificationGenre": "Tür(ler)", + "AutoTaggingSpecificationMaximumYear": "Maksimum Yıl", + "AutoTaggingSpecificationMinimumYear": "Minimum Yıl", + "AutoTaggingSpecificationOriginalLanguage": "Dil", + "AutoTaggingSpecificationQualityProfile": "Kalite Profili", + "AutoTaggingSpecificationRootFolder": "Kök Klasör", + "AutoTaggingSpecificationSeriesType": "Dizi Türü", + "AutoTaggingSpecificationStatus": "Durum", + "BuiltIn": "Dahili", + "CalendarFeed": "{appName} Takvim Beslemesi", + "CalendarLegendEpisodeDownloadingTooltip": "Bölüm şu anda indiriliyor", + "CalendarLegendEpisodeMissingTooltip": "Bölüm yayınlandı ancak diskte yok", + "CalendarLegendEpisodeOnAirTooltip": "Bölüm şu anda yayınlanıyor", + "CalendarLegendSeriesFinaleTooltip": "Dizi veya sezon finali", + "CalendarLegendEpisodeUnairedTooltip": "Bölüm henüz yayınlanmadı", + "CalendarLegendEpisodeUnmonitoredTooltip": "Bölüm takip edilmiyor", + "CalendarLegendSeriesPremiereTooltip": "Dizi veya sezon prömiyeri", + "Certification": "Sertifika", + "ClickToChangeLanguage": "Dili değiştirmek için tıklayın", + "ClickToChangeEpisode": "Bölümü değiştirmek için tıklayın", + "CloneCustomFormat": "Özel Formatı Klonla", + "CloneIndexer": "Klon İndeksleyici", + "CollapseAll": "Tümünü Daralt", + "CollapseMultipleEpisodes": "Birden Fazla Bölümü Daralt", + "CompletedDownloadHandling": "Tamamlanan İndirme İşlemleri", + "CollapseMultipleEpisodesHelpText": "Aynı gün yayınlanan birden fazla bölümü daralt", + "CollectionsLoadError": "Koleksiyon yüklenemiyor", + "ColonReplacement": "Kolon Değiştirme", + "Conditions": "Koşullar", + "CountSelectedFile": "{selectedCount} seçili dosya", + "CountSelectedFiles": "{selectedCount} seçili dosya", + "CreateEmptySeriesFolders": "Boş Dizi Klasörleri Oluştur", + "CreateEmptySeriesFoldersHelpText": "Disk taraması sırasında eksik dizi klasörleri oluştur", + "CreateGroup": "Grup oluştur", + "CustomFilters": "Özel Filtreler", + "CustomFormatsSpecificationSource": "Kaynak", + "GeneralSettingsLoadError": "Genel ayarlar yüklenemiyor", + "UsenetDisabled": "Usenet Devre Dışı", + "Authentication": "Doğrulama", + "BackupNow": "Şimdi Yedekle", + "BackupsLoadError": "Yedeklemeler yüklenemiyor", + "DownloadClientRootFolderHealthCheckMessage": "İndirme istemcisi {downloadClientName}, indirmeleri kök klasöre yerleştirir {rootFolderPath}. Bir kök klasöre indirmemelisiniz.", + "DeleteBackup": "Yedeklemeyi Sil", + "CustomColonReplacementFormatHelpText": "İki nokta üst üste yerine kullanılacak karakterler", + "CustomColonReplacement": "Özel Kolon Değişimi", + "CustomColonReplacementFormatHint": "İki Nokta (Harf) gibi geçerli dosya sistemi karakteri", + "CustomFormatsSpecificationReleaseGroup": "Yayın Grubu", + "CustomFormatsSpecificationResolution": "Çözünürlük", + "DefaultDelayProfileSeries": "Bu varsayılan profildir. Açık profili olmayan tüm diziler için geçerlidir.", + "DeleteDelayProfile": "Gecikme Profilini Sil", + "DeleteEmptyFolders": "Boş klasörleri silin", + "DeleteEmptySeriesFoldersHelpText": "Disk taraması sırasında ve bölüm dosyaları silindiğinde boş dizi ve sezon klasörlerini silin", + "DeleteEpisodesFiles": "{episodeFileCount} Bölüm Dosyasını Sil", + "DeleteImportListExclusion": "İçe Aktarma Listesi Hariç Tutmasını Sil", + "DeleteIndexer": "Dizinleyiciyi Sil", + "Docker": "Docker", + "DockerUpdater": "güncellemeyi almak için docker konteynerini güncelleyin", + "DeleteSelectedSeries": "Seçili Serileri Sil", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "14 günden uzun süre önce yayınlanan bölümleri almaya öncelik verin", + "DownloadClientStatusAllClientHealthCheckMessage": "Tüm indirme istemcileri hatalar nedeniyle kullanılamıyor", + "DownloadPropersAndRepacks": "Uygunluj ve Yeniden Paketlemeler", + "EditRemotePathMapping": "Uzak Yol Eşlemeyi Düzenle", + "EditSelectedSeries": "Seçili Diziyi Düzenle", + "EnableAutomaticSearchHelpText": "Kullanıcı arayüzü veya {appName} tarafından otomatik aramalar yapıldığında kullanılacaktır", + "EnableColorImpairedMode": "Renk Bozukluğu Modunu Etkinleştir", + "EnableAutomaticSearchHelpTextWarning": "Etkileşimli arama kullanıldığında kullanılacaktır", + "EnableInteractiveSearchHelpText": "Etkileşimli arama kullanıldığında kullanılacak", + "EpisodeInfo": "Bölüm Bilgisi", + "ErrorRestoringBackup": "Yedeği geri yüklerken hata", + "ExternalUpdater": "{appName}, harici bir güncelleme mekanizması kullanacak şekilde yapılandırıldı", + "Filename": "Dosya adı", + "From": "itibaren", + "ImportExtraFiles": "Ekstra Dosyaları İçe Aktar", + "ImportListsLoadError": "Listeler yüklenemiyor", + "MediaManagementSettings": "Medya Yönetimi Ayarları", + "MetadataSettings": "Meta Veri Ayarları", + "Negated": "Reddedildi", + "FileManagement": "Dosya Yönetimi", + "FirstDayOfWeek": "Haftanın ilk günü", + "Fixed": "Sabit", + "InstallLatest": "En Sonu Yükle", + "RemotePathMappings": "Uzak Yol Eşlemeleri", + "Retention": "Saklama", + "ShortDateFormat": "Kısa Tarih Formatı", + "ShowQualityProfile": "Kalite Profilini Göster", + "Tags": "Etiketler", + "UrlBase": "URL Tabanı", + "BranchUpdate": "{appName} uygulamasını güncellemek için kullanılacak şube", + "MoveFiles": "Dosyaları Taşı", + "Reset": "Sıfırla", + "Date": "Tarih", + "Debug": "Hata ayıklama", + "DailyEpisodeTypeFormat": "Tarih ({format})", + "DeleteSeriesFolders": "Dizi Klasörlerini Sil", + "Discord": "Uyuşmazlık", + "DeleteSeriesFoldersHelpText": "Dizi klasörlerini ve tüm içeriklerini silin", + "QualitySettings": "Kalite Ayarları", + "ReplaceWithSpaceDashSpace": "Space Dash Space ile değiştirin", + "Continuing": "Devam Ediyor", + "CleanLibraryLevel": "Kütüphane Seviyesini Temizle", + "ClickToChangeSeason": "Sezonu değiştirmek için tıklayın", + "ContinuingOnly": "Sadece Devam Eden", + "Downloaded": "İndirildi", + "FolderNameTokens": "Dosya Adı Belirteçleri", + "Folders": "Klasörler", + "Episode": "Bölüm", + "Language": "Dil", + "MediaManagementSettingsLoadError": "Medya Yönetimi ayarları yüklenemiyor", + "Ok": "Tamam", + "Overview": "Genel Bakış", + "ProxyPasswordHelpText": "Gerekirse yalnızca bir kullanıcı adı ve şifre girmeniz gerekir. Aksi takdirde boş bırakın.", + "Refresh": "Yenile", + "Existing": "Mevcut", + "ImportListSettings": "Liste Ayarları", + "Protocol": "Protokol", + "Today": "Bugün", + "Min": "Min", + "UiSettingsSummary": "Takvim, tarih ve renk körlüğü seçenekleri", + "Importing": "İçe Aktarma", + "MinimumAge": "Minimum Geçen Süre", + "MinimumFreeSpaceHelpText": "Bu miktardan daha az kullanılabilir disk alanı bırakacaksa içe aktarmayı önleyin", + "MinimumLimits": "Minimum Limitler", + "Missing": "Eksik", + "NotificationsGotifySettingsMetadataLinks": "Meta Veri Bağlantıları", + "NotificationsPlexSettingsServer": "Sunucu", + "Path": "Yol", + "Peers": "Akranlar", + "Pending": "Bekliyor", + "PendingChangesDiscardChanges": "Değişiklikleri at ve ayrıl", + "PendingChangesStayReview": "Kalın ve değişiklikleri inceleyin", + "RejectionCount": "Reddetme Sayısı", + "RemotePathMappingHostHelpText": "Uzaktan İndirme İstemcisi için belirttiğiniz ana bilgisayar", + "RemotePathMappingsInfo": "Uzak Yol Eşlemeleri çok nadiren gereklidir, {appName} ve indirme istemciniz aynı sistemdeyse yollarınızı eşleştirmeniz daha iyidir. Daha fazla bilgi için [wiki]({wikiLink}) adresini ziyaret edin", + "RemotePathMappingsLoadError": "Uzak Yol Eşlemeleri yüklenemiyor", + "RemoveCompletedDownloadsHelpText": "İçe aktarılan indirmeleri indirme istemcisi geçmişinden kaldırın", + "ResetAPIKey": "API Anahtarını Sıfırla", + "Restart": "Tekrar başlat", + "RestartNow": "Şimdi yeniden başlat", + "RestartReloadNote": "Not: {appName}, geri yükleme işlemi sırasında kullanıcı arayüzünü otomatik olarak yeniden başlatacak ve yeniden yükleyecektir.", + "Restore": "Onarmak", + "Result": "Sonuç", + "TagIsNotUsedAndCanBeDeleted": "Etiket kullanılmaz ve silinebilir", + "CountSeriesSelected": "{count} dizi seçildi", + "DeleteSeriesModalHeader": "Sil - {title}", + "DownloadFailed": "Yükleme başarısız", + "DownloadFailedEpisodeTooltip": "Bölüm indirme başarısız oldu", + "DownloadIgnoredEpisodeTooltip": "Bölüm İndirme Göz Ardı Edildi", + "More": "Daha", + "MaximumLimits": "Maksimum Sınırlar", + "Presets": "Ön ayarlar", + "ProcessingFolders": "Klasörleri İşleme", + "ProtocolHelpText": "Hangi protokol (ler) in kullanılacağını ve başka türlü eşit sürümler arasında seçim yaparken hangisinin tercih edileceğini seçin", + "RecentFolders": "Son Klasörler", + "NotificationsSettingsWebhookHeaders": "Başlıklar", + "RemotePathMappingLocalPathHelpText": "{appName}'ın uzak yola yerel olarak erişmek için kullanması gereken yol", + "RemotePathMappingRemotePathHelpText": "İndirme İstemcisinin eriştiği dizinin kök yolu", + "Events": "Olaylar", + "CountSeasons": "{count} Sezon", + "CustomFormatsSpecificationLanguage": "Dil", + "Delete": "Sil", + "ReplaceIllegalCharacters": "Geçersiz Karakterleri Değiştirin", + "LongDateFormat": "Uzun Tarih Formatı", + "ManageCustomFormats": "Özel Formatları Yönet", + "MediaInfo": "Medya bilgisi", + "NoChange": "Değişiklik yok", + "NoChanges": "Değişiklikler yok", + "ClickToChangeQuality": "Kaliteyi değiştirmek için tıklayın", + "EditDelayProfile": "Gecikme Profilini Düzenle", + "EventType": "Etkinlik tipi", + "ImportedTo": "İçeri Aktarıldı", + "IndexerDownloadClientHealthCheckMessage": "Geçersiz indirme istemcilerine sahip dizinleyiciler: {indexerNames}.", + "Component": "Bileşen", + "Connection": "Bağlantılar", + "Reorder": "Yeniden sırala", + "RestoreBackup": "Yedeği Geri Yükle", + "Automatic": "Otomatik", + "AutomaticSearch": "Otomatik Arama", + "BackupFolderHelpText": "Bağıl yollar {appName}'ın AppData dizini altında olacak", + "Backups": "Yedeklemeler", + "BeforeUpdate": "Güncellemeden önce", + "Branch": "Şube", + "CalendarLoadError": "Takvim yüklenemiyor", + "SetPermissionsLinuxHelpTextWarning": "Bu ayarların ne yaptığından emin değilseniz, değiştirmeyin.", + "Clear": "Temizle", + "CurrentlyInstalled": "Şu anda Yüklü", + "Custom": "Özel", + "CustomFormatScore": "Özel Format Puanı", + "DeleteEpisodeFromDisk": "Bölümü diskten sil", + "DeleteSeriesFolder": "Seri Klasörünü Sil", + "Details": "Detaylar", + "DotNetVersion": ".NET", + "WeekColumnHeader": "Hafta Sütun Başlığı", + "EpisodeImportedTooltip": "Bölüm başarıyla indirildi ve indirme istemcisinden alındı", + "Links": "Bağlantılar", + "Blocklist": "Engellenenler listesi", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "İndirme istemcisi mevcut değil", + "HideAdvanced": "Gelişmiş'i Gizle", + "EditSeries": "Diziyi Düzenle", + "SendAnonymousUsageData": "Anonim Kullanım Verilerini Gönderin", + "ShownClickToHide": "Gösterildi, gizlemek için tıklayın", + "Error": "Hata", + "DelayProfile": "Gecikme Profilleri", + "DoNotPrefer": "Tercih etmeme", + "DeleteNotification": "Bildirimi Sil", + "DeleteSeriesFolderCountConfirmation": "Seçili {count} diziyi silmek istediğinizden emin misiniz?", + "Deleted": "Silindi", + "DeleteSeriesFolderCountWithFilesConfirmation": "Seçili {count} diziyi ve tüm içerikleri silmek istediğinizden emin misiniz?", + "EpisodeFileRenamed": "Bölüm Dosyası Yeniden Adlandırıldı", + "DownloadClientOptionsLoadError": "İndirme istemcisi seçenekleri yüklenemiyor", + "EditSeriesModalHeader": "Düzenle - {title}", + "EnableInteractiveSearch": "Etkileşimli Aramayı Etkinleştir", + "EpisodeFileDeletedTooltip": "Bölüm dosyası silindi", + "FileBrowserPlaceholderText": "Yazmaya başlayın veya aşağıdan bir yol seçin", + "Global": "Küresel", + "ImportErrors": "Hataları İçe Aktar", + "ImportFailed": "İçe aktarma başarısız oldu: {sourceTitle}", + "FileNames": "Dosya Adları", + "Filter": "Filtre", + "Forecast": "Tahmin", + "ICalLink": "iCal Bağlantısı", + "RegularExpressionsCanBeTested": "Düzenli ifadeler [burada]({url}) test edilebilir.", + "RemoveFilter": "Filtreyi kaldır", + "StartImport": "İçe Aktarmayı Başlat", + "TableOptions": "Masa Seçenekleri", + "UpdateAll": "Tümünü Güncelle", + "Paused": "Duraklatıldı", + "RescanAfterRefreshHelpTextWarning": "{appName}, 'Her Zaman' olarak ayarlanmadığında dosyalardaki değişiklikleri otomatik olarak algılamayacaktır", + "Search": "Ara", + "SelectLanguage": "Dil Seçin", + "Warn": "Uyar", + "Ignored": "Yok sayıldı", + "DeleteSeriesFolderHelpText": "Dizi klasörünü ve içeriğini silin", + "Close": "Kapat", + "Local": "Yerel", + "AutoAdd": "Otomatik Ekle", + "Edit": "Düzenle", + "EpisodeImported": "Bölüm içe aktarıldı", + "General": "Genel", + "GeneralSettings": "Genel Ayarlar", + "HardlinkCopyFiles": "Hardlink / Dosyaları Kopyala", + "Health": "Sağlık", + "ImportCustomFormat": "Özel Formatı İçe Aktar", + "IncludeCustomFormatWhenRenaming": "Yeniden Adlandırırken Özel Formatı Dahil Et", + "Manual": "Manuel", + "DeleteCustomFormat": "Özel Formatı Sil", + "DetailedProgressBar": "Ayrıntılı İlerleme Çubuğu", + "DetailedProgressBarHelpText": "İlerleme çubuğundaki metni göster", + "DoneEditingGroups": "Grupları Düzenleme Bitti", + "DownloadClientSettings": "İndirme İstemcisi Ayarlarını", + "DownloadClientStatusSingleClientHealthCheckMessage": "Hatalar nedeniyle indirme istemcileri kullanılamıyor: {downloadClientNames}", + "BackupRetentionHelpText": "Saklama süresinden daha eski otomatik yedeklemeler otomatik olarak temizlenecektir", + "Calendar": "Takvim", + "NotificationsLoadError": "Bildirimler yüklenemiyor", + "ClientPriority": "Müşteri Önceliği", + "EnableAutomaticSearch": "Otomatik Aramayı Etkinleştir", + "EpisodeNaming": "Bölüm Adlandırması", + "Host": "Sunucu", + "EditGroups": "Grupları Düzenle", + "BypassProxyForLocalAddresses": "Yerel Adresler için Proxy'yi Kullanma", + "ChangeFileDate": "Dosya Tarihini Değiştir", + "ChmodFolder": "chmod Klasörü", + "ChooseAnotherFolder": "Başka bir dosya seç", + "ClickToChangeSeries": "Diziyi değiştirmek için tıklayın", + "NamingSettings": "Adlandırma Ayarları", + "Negate": "Reddet", + "CustomFormatsSpecificationMinimumSizeHelpText": "Sürüm bu boyuttan daha büyük olmalıdır", + "DatabaseMigration": "DB Geçişi", + "DefaultCase": "Varsayılan Durum", + "DeleteQualityProfile": "Kalite Profilini Sil", + "DeleteSelectedEpisodeFiles": "Seçili Bölüm Dosyalarını Sil", + "DeleteSeriesFolderConfirmation": "{path}` dizi klasörü ve içindeki tüm içerik silinecektir.", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} bölüm dosyası toplamı {size}", + "DestinationPath": "Hedef yol", + "Disabled": "Devre dışı", + "ColonReplacementFormatHelpText": "{appName}'ın kolon değişimini nasıl işlediğini değiştirin", + "DeleteTag": "Etiketi Sil", + "NamingSettingsLoadError": "Adlandırma ayarları yüklenemiyor", + "NotificationTriggers": "Bildirim Tetikleyicileri", + "ShowSizeOnDisk": "Diskte Boyutu Göster", + "Status": "Durum", + "Condition": "Koşul", + "Downloading": "İndiriliyor", + "Duplicate": "Kopyala", + "CancelProcessing": "İşlemi İptal Et", + "EditQualityProfile": "Kalite Profilini Düzenle", + "Files": "Dosyalar", + "DelayProfilesLoadError": "Gecikme Profilleri yüklenemiyor", + "DeleteDownloadClient": "İndirme İstemcisini Sil", + "EnableAutomaticAddSeriesHelpText": "Senkronizasyonlar kullanıcı arayüzü veya {appName} üzerinden gerçekleştirildiğinde bu listeden {appName} uygulamasına dizi ekleyin", + "CertificateValidation": "Sertifika Doğrulaması", + "EditSelectedCustomFormats": "Seçilen Özel Formatları Düzenle", + "EnableHelpText": "Bu meta veri türü için meta veri dosyası oluşturmayı etkinleştir", + "EnableCompletedDownloadHandlingHelpText": "Tamamlanan indirmeleri indirme istemcisinden otomatik olarak içe aktarın", + "EnableMetadataHelpText": "Bu meta veri türü için meta veri dosyası oluşturmayı etkinleştirin", + "Ended": "Biten", + "EndedOnly": "Sadece Biten", + "EndedSeriesDescription": "Ek bölüm veya sezon beklenmiyor", + "EpisodeCount": "Bölüm Sayısı", + "EpisodeDownloaded": "Bölüm İndirildi", + "EpisodeFileRenamedTooltip": "Bölüm dosyası yeniden adlandırıldı", + "EnableSsl": "SSL'yi etkinleştir", + "EpisodeGrabbedTooltip": "Bölüm {indexer}'dan alındı ve {downloadClient}'a gönderildi", + "EpisodeHasNotAired": "Bölüm yayınlanmadı", + "EpisodeIsDownloading": "Bölüm indiriliyor", + "EpisodeIsNotMonitored": "Bölüm takip edilmiyor", + "EpisodeMissingFromDisk": "Bölüm diskte eksik", + "EpisodeFilesLoadError": "Bölüm dosyaları yüklenemiyor", + "EpisodeHistoryLoadError": "Bölüm geçmişi yüklenemiyor", + "DownloadClient": "İndirme İstemcisi", + "EditListExclusion": "Liste Dışlama Düzenlemesi", + "EnableAutomaticAdd": "Otomatik Eklemeyi Etkinleştir", + "Enabled": "Etkin", + "ExistingTag": "Mevcut etiket", + "ICalFeed": "iCal Beslemesi", + "FileNameTokens": "Dosya Adı Belirteçleri", + "FreeSpace": "Boş alan", + "Grab": "Kapmak", + "SearchForMissing": "Kayıpları Ara", + "ICalShowAsAllDayEventsHelpText": "Etkinlikler, takviminizde tüm gün süren etkinlikler olarak görünecek", + "IconForCutoffUnmet": "Sınırlandırılmamış için Simge", + "FileBrowser": "Dosya Yöneticisi", + "Exception": "İstisna", + "Failed": "Başarısız oldu", + "Hostname": "Hostname", + "HomePage": "Ana Sayfa", + "Max": "Max", + "DailyEpisodeFormat": "Günlük Bölüm Formatı", + "CustomFormatsSpecificationMaximumSizeHelpText": "Sürüm bu boyuttan küçük veya eşit olmalıdır", + "CustomFormatsSpecificationMinimumSize": "Minimum Boyut", + "Info": "Bilgi", + "IconForCutoffUnmetHelpText": "Sınıra ulaşılmadığında dosyalar için simge göster", + "MetadataKometaDeprecatedSetting": "Kullanım Dışı", + "AuthForm": "Form (Giriş Sayfası)", + "BrowserReloadRequired": "Tarayıcının Yeniden Yüklenmesi Gerekiyor", + "CopyUsingHardlinksSeriesHelpText": "Sabit bağlantılar, {appName} uygulamasının, fazladan disk alanı kaplamadan veya dosyanın tüm içeriğini kopyalamadan, eklenmiş torrentleri dizi klasörüne aktarmasına olanak tanır. Sabit bağlantılar yalnızca kaynak ve hedef aynı birimdeyse çalışır", + "DailyEpisodeTypeDescription": "Yıl-ay-gün kullanan günlük veya daha az sıklıkta yayınlanan bölümler (2023-08-04)", + "DelayProfileSeriesTagsHelpText": "En az bir eşleşen etiketi olan diziler için geçerlidir", + "DeletedSeriesDescription": "Dizi TheTVDB'den silindi", + "DownloadClientSeriesTagHelpText": "Bu indirme istemcisini yalnızca en az bir eşleşen etiketi olan diziler için kullanın. Tüm dizilerle kullanmak için boş bırakın.", + "BranchUpdateMechanism": "Harici güncelleme mekanizması tarafından kullanılan şube", + "CloneProfile": "Klon Profili", + "Completed": "Tamamla", + "ConnectSettings": "Bağlantı Ayarları", + "ConnectionLost": "Bağlantı koptu", + "Connections": "Bağlantılar", + "CopyToClipboard": "Panoya kopyala", + "CopyUsingHardlinksHelpTextWarning": "Bazen, dosya kilitleri, başlatılan dosyaların yeniden adlandırılmasını engelleyebilir. Tohumlamayı geçici olarak devre dışı bırakabilir ve geçici olarak {appName}'ın yeniden adlandırma işlevini kullanabilirsiniz.", + "CountCustomFormatsSelected": "{count} özel format seçildi", + "CutoffNotMet": "Kesinti Karşılanmadı", + "Dates": "Tarih", + "Day": "Gün", + "DelayProfiles": "Gecikme Profilleri", + "DeleteSelectedCustomFormats": "Özel Formatı Sil", + "DeleteSelectedCustomFormatsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?", + "DeleteSelectedImportListExclusionsMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", + "DestinationRelativePath": "Hedef Göreli Yol", + "DiskSpace": "Disk Alanı", + "DoNotUpgradeAutomatically": "Otomatik Olarak Yükseltme", + "Donations": "Bağışlar", + "Download": "İndir", + "DownloadClientUnavailable": "İndirme istemcisi kullanılamıyor", + "DownloadPropersAndRepacksHelpText": "Propers / Repacks'e otomatik olarak yükseltme yapılıp yapılmayacağı", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Uygunluk ve Yeniden Paketlemeler üzerinden özel format puanına göre sıralamak için \"Tercih Etme\" seçeneğini kullanın", + "DownloadPropersAndRepacksHelpTextWarning": "Propers / Repacks'e otomatik yükseltmeler için özel formatlar kullanın", + "EditCustomFormat": "Özel Formatı Düzenle", + "EditRestriction": "Kısıtlamayı Düzenle", + "AuthenticationMethodHelpText": "{appName}'e erişmek için Kullanıcı Adı ve Parola gereklidir", + "CalendarLegendEpisodeDownloadedTooltip": "Bölüm indirildi ve sıralandı", + "ChangeFileDateHelpText": "İçe aktarma/yeniden tarama sırasında dosya tarihini değiştir", + "ChmodFolderHelpTextWarning": "Bu yalnızca {appName} uygulamasını çalıştıran kullanıcı, dosyanın sahibiyse işe yarar. İndirme istemci izinlerinin doğruluğundan emin olmak önerilir.", + "CustomFormatsSpecificationMaximumSize": "Maksimum Boyut", + "Daily": "Günlük", + "DeleteEpisodeFileMessage": "'{path}' dosyasını silmek istediğinizden emin misiniz?", + "DeleteEpisodesFilesHelpText": "Bölüm dosyalarını ve dizi klasörünü silin", + "DeletedReasonEpisodeMissingFromDisk": "{appName} dosyayı diskte bulamadığından dosyanın veritabanındaki bölümden bağlantısı kaldırıldı", + "DownloadClientSortingHealthCheckMessage": "{downloadClientName} indirme istemcisi {appName} kategorisi için {sortingMode} sıralamasını etkinleştirdi. İçe aktarma sorunlarını önlemek için indirme istemcinizde sıralamayı devre dışı bırakmalısınız.", + "ClickToChangeReleaseType": "Sürüm türünü değiştirmek için tıklayın", + "EpisodeFileMissingTooltip": "Bölüm dosyası eksik", + "ChownGroupHelpTextWarning": "Bu yalnızca {appName} uygulamasını çalıştıran kullanıcı, dosyanın sahibiyse işe yarar. İndirme istemcisinin {appName} ile aynı grubu kullandığından emin olmak önerilir.", + "DeleteSelectedEpisodeFilesHelpText": "Seçili bölüm dosyalarını silmek istediğinizden emin misiniz?", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "{downloadClientName} ile iletişim kurulamıyor. {errorMessage}", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Son 14 gün içinde yayınlanan bölümleri almaya öncelik verin", + "EnableInteractiveSearchHelpTextWarning": "Bu dizinleyici ile arama desteklenmiyor", + "EpisodeAirDate": "Bölüm Yayın Tarihi", + "EnableColorImpairedModeHelpText": "Renk engelli kullanıcıların renkleri daha iyi ayırt edebilmelerini sağlamak için değiştirilmiş stil", + "EnableRss": "RSS'yi etkinleştir", + "EnableSslHelpText": "Etkili olması için yönetici olarak yeniden çalıştırmayı gerektirir", + "ExportCustomFormat": "Özel Formatı Dışa Aktar", + "ExtraFileExtensionsHelpTextsExamples": "Örnekler: \".sub, .nfo\" veya \"sub, nfo\"", + "FeatureRequests": "Özellik talepleri", + "Folder": "Klasör", + "GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analitikler ve güncellemeler", + "Genres": "Türler", + "GrabSelected": "Seçilenleri Kap", + "Grabbed": "Yakalandı", + "Group": "Grup", + "HiddenClickToShow": "Gizli, göstermek için tıklayın", + "ICalFeedHelpText": "Bu URL'yi müşterilerinize kopyalayın veya tarayıcınız webcal'i destekliyorsa abone olmak için tıklayın", + "ICalShowAsAllDayEvents": "Tüm Gün Olayları Olarak Göster", + "IgnoredAddresses": "Yoksayılan Adresler", + "Images": "Görüntüler", + "ImportList": "Listeler", + "ImportListExclusionsLoadError": "Hariç Tutulanlar Listesi yüklenemiyor", + "ImportLists": "Listeler", + "IndexersLoadError": "Dizinleyiciler yüklenemiyor", + "IndexersSettingsSummary": "Dizinleyiciler ve yayımlama kısıtlamaları", + "InteractiveImport": "Etkileşimli İçe Aktarma", + "InteractiveImportNoLanguage": "Seçilen her dosya için dil seçilmelidir", + "InteractiveSearch": "Etkileşimli Arama", + "InvalidFormat": "Geçersiz format", + "KeyboardShortcuts": "Klavye kısayolları", + "Languages": "Diller", + "Large": "Büyük", + "LastExecution": "Son Yürütme", + "LastUsed": "Son kullanılan", + "LastWriteTime": "Son Yazma Zamanı", + "Level": "Seviye", + "ListOptionsLoadError": "Liste seçenekleri yüklenemiyor", + "ListTagsHelpText": "Etiketler listesi öğeleri eklenecek", + "LogFiles": "Log dosyaları", + "LogLevel": "Günlük Düzeyi", + "LogLevelTraceHelpTextWarning": "İzleme günlük kaydı yalnızca geçici olarak etkinleştirilmelidir", + "LogOnly": "Sadece hesap aç", + "Logs": "Günlükler", + "Lowercase": "Küçük Harf", + "MaintenanceRelease": "Bakım Sürümü: hata düzeltmeleri ve diğer iyileştirmeler. Daha fazla ayrıntı için Github İşlem Geçmişine bakın", + "MappedNetworkDrivesWindowsService": "Windows Hizmeti olarak çalıştırıldığında eşlenen ağ sürücüleri kullanılamaz, daha fazla bilgi için [SSS]({url}) bölümüne bakın.", + "MarkAsFailed": "Başarısız olarak işaretle", + "MaximumSize": "Maksimum Boyut", + "MaximumSizeHelpText": "MB cinsinden alınacak bir sürüm için maksimum boyut. Sınırsız olarak ayarlamak için sıfıra ayarlayın", + "MediaManagement": "Medya Yönetimi", + "MetadataKometaDeprecated": "Kometa dosyaları artık oluşturulmayacak, destek v6'da tamamen kaldırılacak", + "MinimumCustomFormatScoreHelpText": "İndirmeye izin verilen minimum özel format puanı", + "Month": "Ay", + "MoreDetails": "Daha fazla detay", + "MoreInfo": "Daha fazla bilgi", + "Name": "İsim", + "New": "Yeni", + "NextExecution": "Sonraki Yürütme", + "NoBackupsAreAvailable": "Kullanılabilir yedek yok", + "NoCustomFormatsFound": "Özel format bulunamadı", + "NoEventsFound": "Etkinlik bulunamadı", + "NoIssuesWithYourConfiguration": "Yapılandırmanızla ilgili sorun yok", + "NoLeaveIt": "Hayır, Bırak", + "NoLimitForAnyRuntime": "Herhangi bir çalışma zamanı için sınır yok", + "NoLinks": "Bağlantı Yok", + "NoLogFiles": "Günlük dosyası yok", + "NoMatchFound": "Eşleşme bulunamadı!", + "NoMinimumForAnyRuntime": "Herhangi bir çalışma süresi için minimum değer yok", + "NoResultsFound": "Sonuç bulunamadı", + "NoTagsHaveBeenAddedYet": "Henüz etiket eklenmedi", + "NoUpdatesAreAvailable": "Güncelleme yok", + "None": "Yok", + "NotificationsGotifySettingsPreferredMetadataLink": "Tercih Edilen Meta Veri Bağlantısı", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Yalnızca tek bir bağlantıyı destekleyen istemciler için meta veri bağlantısı", + "NotificationsPlexSettingsServerHelpText": "Kimlik doğrulamasından sonra plex.tv hesabından sunucuyu seçin", + "OnGrab": "Yakalandığında", + "OnHealthIssue": "Sağlık Sorunu Hakkında", + "OnLatestVersion": "{appName}'ın en son sürümü kurulu", + "OnRename": "Yeniden Adlandırıldığında", + "OnlyTorrent": "Sadece Torrent", + "OnlyUsenet": "Sadece Usenet", + "OpenBrowserOnStart": "Başlangıçta tarayıcıyı aç", + "Options": "Seçenekler", + "OrganizeNamingPattern": "Adlandırma düzeni: `{episodeFormat}`", + "Original": "Orijinal", + "OutputPath": "Çıkış yolu", + "OverviewOptions": "Genel Bakış Seçenekler", + "PendingChangesMessage": "Kaydedilmemiş değişiklikleriniz var, bu sayfadan ayrılmak istediğinizden emin misiniz?", + "Permissions": "İzinler", + "Port": "Port No", + "PortNumber": "Port numarası", + "PosterOptions": "Poster Seçenekleri", + "PosterSize": "Poster Boyutu", + "Posters": "Posterler", + "PreferAndUpgrade": "Tercih Et ve Yükselt", + "PreferTorrent": "Torrent'i tercih et", + "PreferUsenet": "Usenet'i tercih et", + "Preferred": "Tercihli", + "PreferredSize": "Tercih Edilen Boyut", + "Priority": "Öncelik", + "Profiles": "Profiller", + "ProfilesSettingsSummary": "Kalite, Dil, Gecikme ve Yayımlama profilleri", + "Progress": "İlerleme", + "Proper": "Uygun", + "Proxy": "Proxy", + "ProxyBypassFilterHelpText": "Ayırıcı olarak \",\" ve \"*\" kullanın. alt alan adları için joker karakter olarak", + "ProxyType": "Proxy Türü", + "ProxyUsernameHelpText": "Gerekirse yalnızca bir kullanıcı adı ve şifre girmeniz gerekir. Aksi takdirde boş bırakın.", + "Qualities": "Nitelikler", + "QualitiesHelpText": "Listede üst sıralarda yer alan nitelikler işaretlenmese bile en çok tercih edilendir. Aynı grup içindeki nitelikler eşittir. Yalnızca kontrol edilen nitelikler aranır", + "Quality": "Kalite", + "QualityDefinitions": "Kalite Tanımları", + "QualityDefinitionsLoadError": "Kalite Tanımları yüklenemiyor", + "QualityProfile": "Kalite Profili", + "QualityProfiles": "Kalite Profileri", + "QualityProfilesLoadError": "Kalite Profilleri yüklenemiyor", + "QualitySettingsSummary": "Kalite boyutları ve adlandırma", + "ReadTheWikiForMoreInformation": "Daha fazla bilgi için Wiki'yi okuyun", + "Real": "Gerçek", + "Reason": "Nedeni", + "RecentChanges": "Son değişiklikler", + "RecyclingBin": "Geri dönüşüm Kutusu", + "RecyclingBinCleanup": "Geri Dönüşüm Kutusu Temizle", + "RecyclingBinCleanupHelpText": "Otomatik temizlemeyi devre dışı bırakmak için 0'a ayarlayın", + "RecyclingBinCleanupHelpTextWarning": "Geri dönüşüm kutusundaki, seçilen gün sayısından daha eski olan dosyalar otomatik olarak temizlenecektir", + "RecyclingBinHelpText": "Film dosyaları, kalıcı olarak silinmek yerine silindiğinde buraya gider", + "RefreshAndScan": "Yenile ve Tara", + "RegularExpressionsTutorialLink": "Düzenli ifadeler hakkında daha fazla ayrıntı [burada]({url}) bulunabilir.", + "RelativePath": "Göreceli yol", + "ReleaseRejected": "Reddedildi", + "ReleaseTitle": "Yayin Başlığı", + "Reload": "Tekrar yükle", + "RemoveFailedDownloadsHelpText": "Başarısız indirmeleri indirme istemcisi geçmişinden kaldırın", + "RemoveFromBlocklist": "Kara listeden kaldır", + "RemoveRootFolder": "Kök klasörü kaldır", + "RemoveSelected": "Seçilenleri Kaldır", + "RemovingTag": "Etiket kaldırılıyor", + "RenameFiles": "Yeniden Adlandır", + "Renamed": "Yeniden adlandırıldı", + "Replace": "Değiştir", + "ReplaceWithDash": "Dash ile değiştir", + "ReplaceWithSpaceDash": "Space Dash ile değiştirin", + "RequiredHelpText": "Özel formatın uygulanabilmesi için bu {implementationName} koşulunun eşleşmesi gerekir. Aksi takdirde tek bir {implementationName} eşleşmesi yeterlidir.", + "RestartRequiredHelpTextWarning": "Etkili olması için yeniden başlatma gerektirir", + "RetentionHelpText": "Yalnızca Usenet: Sınırsız saklamaya ayarlamak için sıfıra ayarlayın", + "RootFolder": "Kök Klasör", + "RootFolders": "Kök klasörler", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS, bu indeksleyici ile desteklenmiyor", + "RssSync": "RSS Senkronizasyonu", + "RssSyncInterval": "RSS Senkronizasyon Aralığı", + "Runtime": "Süresi", + "SaveChanges": "Değişiklikleri Kaydet", + "SaveSettings": "Ayarları kaydet", + "Score": "Puan", + "ScriptPath": "Komut Dosyası Yolu", + "SearchAll": "Tümünü ara", + "SearchIsNotSupportedWithThisIndexer": "Bu indeksleyici ile arama desteklenmiyor", + "SearchSelected": "Seçilenleri Ara", + "Security": "Güvenlik", + "Seeders": "Ekme makineleri", + "SelectFolder": "Dosya Seç", + "SelectQuality": "Kaliteyi Seçin", + "SetPermissions": "İzinleri Ayarla", + "SetPermissionsLinuxHelpText": "Dosyalar içe aktarıldığında / yeniden adlandırıldığında chmod çalıştırılmalı mı?", + "SetTags": "Etiketleri Ayarla", + "Settings": "Ayarlar", + "ShowAdvanced": "Gelişmiş'i Göster", + "ShowQualityProfileHelpText": "Poster altında kalite profilini göster", + "ShowRelativeDates": "Göreli Tarihleri Göster", + "ShowRelativeDatesHelpText": "Göreli (Bugün / Dün / vb.) Veya mutlak tarihleri göster", + "ShowSearch": "Aramayı Göster", + "ShowSearchHelpText": "Fareyle üzerine gelindiğinde arama düğmesini göster", + "ShowTitle": "Başlığı göster", + "Shutdown": "Kapat", + "SizeOnDisk": "Diskteki boyut", + "SkipFreeSpaceCheck": "Boş Alan Kontrolünü Atla", + "Small": "Küçük", + "Socks4": "Çorap4", + "Socks5": "Socks5 (TOR Desteği)", + "Source": "Kaynak", + "SourcePath": "Kaynak Yolu", + "SourceRelativePath": "Kaynak Göreli Yol", + "SourceTitle": "Kaynak başlığı", + "StartProcessing": "İşlemeye Başla", + "Style": "Tarz", + "Sunday": "Pazar", + "SupportedDownloadClients": "{appName}, Newznab standardını kullanan herhangi bir indirme istemcisinin yanı sıra aşağıda listelenen diğer indirme istemcilerini de destekler.", + "SupportedDownloadClientsMoreInfo": "Bireysel indirme istemcileri hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.", + "SupportedIndexers": "{appName}, Newznab standardını kullanan tüm indeksleyicileri ve aşağıda listelenen diğer indeksleyicileri destekler.", + "SupportedIndexersMoreInfo": "Bireysel indeksleyiciler hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.", + "SupportedListsMoreInfo": "Ayrı ayrı içe aktarma listeleri hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.", + "SystemTimeHealthCheckMessage": "Sistem saati 1 günden fazla kapalı. Zamanlanan görevler, saat düzeltilene kadar doğru çalışmayabilir", + "Table": "Tablo", + "TagCannotBeDeletedWhileInUse": "Kullanımdayken silinemez", + "TagsLoadError": "Etiketler yüklenemiyor", + "TagsSettingsSummary": "Tüm etiketleri ve nasıl kullanıldıklarını göster. Kullanılmayan etiketler kaldırılabilinir", + "Tasks": "Görevler", + "TestAll": "Tümünü Test Et", + "TestAllClients": "Tüm İstemcileri Test Et", + "TestAllIndexers": "Tüm Dizinleyicileri Test Et", + "TestAllLists": "Tüm Listeleri Test Et", + "Time": "Zaman", + "TimeFormat": "Zaman formatı", + "Title": "Başlık", + "Titles": "Başlıklar", + "Tomorrow": "Yarın", + "TorrentDelay": "Torrent Gecikmesi", + "TorrentDelayHelpText": "Bir torrent almadan önce beklemek için dakikalar içinde gecikme", + "Torrents": "Torrentler", + "TorrentsDisabled": "Torrentler Devre Dışı", + "TotalFileSize": "Toplam Dosya Boyutu", + "TotalSpace": "Toplam alan", + "Trace": "İzleme", + "Type": "Tür", + "Unavailable": "Kullanım dışı", + "Ungroup": "Grubu çöz", + "Unlimited": "Sınırsız", + "UnmappedFilesOnly": "Yalnızca Eşlenmemiş Dosyalar", + "UnmappedFolders": "Eşlenmemiş Klasörler", + "UnsavedChanges": "Kaydedilmemiş Değişiklikler", + "UnselectAll": "Tüm Seçimleri Kaldır", + "UpdateAppDirectlyLoadError": "{appName} doğrudan güncellenemiyor,", + "UpdateScriptPathHelpText": "Çıkarılan bir güncelleme paketini alan ve güncelleme işleminin geri kalanını işleyen özel bir komut dosyasına giden yol", + "UpdateSelected": "Seçilmişleri güncelle", + "Updates": "Güncellemeler", + "UpgradeUntil": "Kaliteye Kadar Yükseltme", + "UpgradeUntilCustomFormatScore": "Özel Format Puanına Kadar Yükseltme", + "UpgradeUntilThisQualityIsMetOrExceeded": "Bu kalite karşılanana veya aşılana kadar yükseltin", + "UpgradesAllowed": "Yükseltmelere İzin Ver", + "UpgradesAllowedHelpText": "Devre dışı bırakılırsa nitelikler yükseltilmez", + "Uppercase": "Büyük Harf", + "UrlBaseHelpText": "Ters proxy desteği için varsayılan boştur", + "UseHardlinksInsteadOfCopy": "Kopyalama yerine Sabit Bağlantıları Kullanın", + "UseProxy": "Proxy kullan", + "UsenetDelay": "Usenet Gecikmesi", + "Username": "Kullanıcı adı", + "Version": "Sürüm", + "VideoCodec": "Video Kodek", + "View": "Görünüm", + "VisitTheWikiForMoreDetails": "Daha fazla ayrıntı için wiki'yi ziyaret edin: ", + "WaitingToImport": "İçe Aktarma Bekleniyor", + "WaitingToProcess": "İşlenmek için Bekleniyor", + "Week": "Hafta", + "WeekColumnHeaderHelpText": "Aktif görünüm hafta olduğunda her bir sütunun üzerinde gösterilir", + "WhatsNew": "Ne var ne yok?", + "Year": "Yıl", + "YesCancel": "Evet İptal", + "ContinuingSeriesDescription": "Daha fazla bölüm/başka bir sezon bekleniyor", + "DeleteEpisodeFile": "Bölüm Dosyasını Sil", + "EnableMediaInfoHelpText": "Çözünürlük, çalışma zamanı ve kodek bilgileri gibi video bilgilerini dosyalardan çıkarın. Bu, {appName} uygulamasının taramalar sırasında yüksek disk veya ağ etkinliğine neden olabilecek dosyanın bölümlerini okumasını gerektirir.", + "EpisodeMissingAbsoluteNumber": "Bölümün kesin bir bölüm numarası yoktur", + "Mode": "Mod", + "Monday": "Pazartesi", + "ListSyncLevelHelpText": "Kitaplıktaki filmlerin listenizde/listelerinizde görünmemesi durumunda seçiminize göre işlem yapılacaktır", + "Location": "Konum", + "MediaManagementSettingsSummary": "Adlandırma ve dosya yönetimi ayarları", + "Medium": "Orta", + "MegabytesPerMinute": "Dakika Başına Megabayt", + "Menu": "Menü", + "Message": "Mesaj", + "Metadata": "Meta veri", + "MetadataLoadError": "Meta Veriler yüklenemiyor", + "Series": "Diziler", + "IRC": "IRC", + "ImportListsAniListSettingsUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı", + "ImportListsTraktSettingsRatingHelpText": "Diziyi derecelendirme aralığına göre filtrele (0-100)", + "EpisodeSearchResultsLoadError": "Bu bölüm araması için sonuçlar yüklenemedi. Daha sonra tekrar deneyin", + "FailedToLoadSystemStatusFromApi": "API'den sistem durumu alınamadı", + "ImportListsSettingsAuthUser": "Yetkili Kullanıcı", + "ImportListsSettingsExpires": "Sona erme", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Trakt ile kimlik doğrulaması yapın", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Aylara Göre En Çok İzlenen Programlar", + "ImportListsTraktSettingsUserListName": "Trakt Kullanıcısı", + "ImportListsTraktSettingsWatchedListTypeCompleted": "%100 İzlendi", + "ImportListsAniListSettingsImportDropped": "İçe Aktarma Engellendi", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "MyAnimeList ile kimlik doğrulaması yapın", + "ImportListsMyAnimeListSettingsListStatusHelpText": "İçe aktarmak istediğiniz liste türü, tüm listeler için 'Tümü' olarak ayarlanmalıdır", + "ImportListsSettingsRefreshToken": "Token'ı Yenile", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Simkl ile kimlik doğrulaması yapın", + "ImportListsSettingsRssUrl": "RSS URL'si", + "ImportListsSimklSettingsName": "Simkl Kullanıcı İzleme Listesi", + "ImportListsSimklSettingsShowType": "Gösteri Türü", + "ImportListsSimklSettingsShowTypeHelpText": "İçe aktarmak istediğiniz gösterinin türü", + "ImportListsSimklSettingsUserListTypeCompleted": "Tamamlanmış", + "FailedToLoadTranslationsFromApi": "API'den çeviriler alınamadı", + "ImportListsSonarrSettingsFullUrlHelpText": "İçe aktarılacak {appName} örneğinin bağlantı noktası da dahil olmak üzere URL'si", + "ImportListsTraktSettingsUserListTypeCollection": "Kullanıcı Koleksiyon Listesi", + "ImportListsTraktSettingsUserListTypeWatch": "Kullanıcı İzleme Listesi", + "ImportListsTraktSettingsUserListTypeWatched": "Kullanıcının İzlediği Liste", + "ImportListsTraktSettingsWatchedListFilter": "İzlenenler Listesi Filtresi", + "EpisodesLoadError": "Bölümler yüklenemiyor", + "ErrorLoadingItem": "Bu öğe yüklenirken bir hata oluştu", + "FailedToLoadCustomFiltersFromApi": "API'den özel filtreler alınamadı", + "FilterGreaterThanOrEqual": "büyük veya eşit", + "FilterInNext": "sonraki", + "FilterIsAfter": "sonrası", + "FilterStartsWith": "ile başlar", + "ICalSeasonPremieresOnlyHelpText": "Bir sezonun yalnızca ilk bölümü akışta yer alacak", + "ImportCountSeries": "{selectedCount} Dizisini İçe Aktar", + "ImportListSearchForMissingEpisodesHelpText": "Dizi {appName} uygulamasına eklendikten sonra eksik bölümleri otomatik olarak arayın", + "ImportListStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle kullanılamayan listeler: {importListNames}", + "ImportListsAniListSettingsImportRepeatingHelpText": "Liste: Şu anda tekrar izleniyor", + "ImportListsAniListSettingsImportCancelled": "İçe Aktarma İptal Edildi", + "ImportListsAniListSettingsImportFinished": "İçe Aktarma Tamamlandı", + "ImportListsAniListSettingsImportRepeating": "Tekrarlayan İçe Aktarma", + "ImportListsPlexSettingsAuthenticateWithPlex": "Plex.tv ile kimlik doğrulaması yapın", + "ImportListsSimklSettingsListType": "Liste Türü", + "ImportListsSimklSettingsListTypeHelpText": "İçe aktarmak istediğiniz listenin türü", + "ImportListsSonarrSettingsApiKeyHelpText": "İçe aktarılacak {appName} örneğinin API Anahtarı", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Aya Göre Önerilen Gösteriler", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Haftaya Göre Önerilen Gösteriler", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Haftaya Göre En Çok İzlenen Programlar", + "ImportListsValidationTestFailed": "Test bir hata nedeniyle iptal edildi: {exceptionMessage}", + "IndexerSettingsAnimeCategories": "Anime Kategorileri", + "Mapping": "Haritalama", + "FilterNotInLast": "en son olmayan", + "FilterNotInNext": "bir sonraki olmayan", + "HasMissingSeason": "Eksik Sezon Olan", + "IndexerSettingsAllowZeroSize": "Sıfır Boyutuna İzin Ver", + "IndexerSettingsPasskey": "Geçiş anahtarı", + "ManageEpisodesSeason": "Bu sezondaki Bölüm dosyalarını yönetin", + "InteractiveSearchSeason": "Bu sezondaki tüm bölümler için etkileşimli arama", + "Forums": "Forumlar", + "IconForSpecials": "Özel Bölümler için Simge", + "ImportListsImdbSettingsListId": "Liste Kimliği", + "ImportListsPlexSettingsWatchlistRSSName": "Plex İzleme Listesi RSS", + "ImportListsSettingsAccessToken": "Erişim Token'ı", + "ImportListsTraktSettingsAdditionalParameters": "Ek Parametreler", + "ImportListsSonarrSettingsTagsHelpText": "İçe aktarılacak kaynak örneğinden etiketler", + "ImportListsSonarrValidationInvalidUrl": "{appName} URL'si geçersiz, bir URL tabanı mı eksik?", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Ek Trakt API parametreleri", + "ImportListsTraktSettingsGenres": "Türler", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Tüm Zamanların Önerilen Gösterileri", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Liste Türü İzlenen ise, içe aktarmak istediğiniz dizi türünü seçin", + "ImportListsTraktSettingsWatchedListSorting": "İzleme Listesi Sıralaması", + "ImportListsTraktSettingsWatchedListTypeAll": "Hepsi", + "ImportListsTraktSettingsWatchedListTypeInProgress": "Devam Etmekte", + "ImportListsTraktSettingsYears": "Yıl", + "ImportListsValidationInvalidApiKey": "API Anahtarı geçersiz", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Mümkünse Tamamlanmış İndirme İşlemini Etkinleştirin", + "IndexerHDBitsSettingsCategories": "Kategoriler", + "IndexerHDBitsSettingsCodecs": "Kodekler", + "IndexerHDBitsSettingsCodecsHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "IndexerHDBitsSettingsMediumsHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "IndexerIPTorrentsSettingsFeedUrl": "Besleme URL'si", + "IndexerSettingsApiPath": "API Yolu", + "IndexerSettingsCategories": "Kategoriler", + "IndexerSettingsCookie": "Çerez", + "IndexerSettingsMinimumSeedersHelpText": "Minimum seeder(kaynak) sayısı gereklidir.", + "LocalAirDate": "Yerel Yayın Tarihi", + "EpisodeRequested": "Bölüm Talep Edildi", + "ImportListsAniListSettingsImportCancelledHelpText": "Medya: Dizi iptal edildi", + "ImportListsAniListSettingsImportWatchingHelpText": "Liste: Şu anda izleniyor", + "ImportListsCustomListSettingsName": "Özel Liste", + "ImportListsMyAnimeListSettingsListStatus": "Liste Durumu", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sezon Sekronizasyonu Takip Et", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Beklenen Gösteriler", + "ImportListRootFolderMissingRootHealthCheckMessage": "İçe aktarma listeleri için kök klasör eksik: {rootFolderInfo}", + "ImportListsSimklSettingsUserListTypeDropped": "Engellenmiş", + "ImportListsSimklSettingsUserListTypeHold": "Tut", + "ImportListsSimklSettingsUserListTypeWatching": "İzlenen", + "ImportListsSonarrSettingsFullUrl": "Tam URL", + "IndexerJackettAllHealthCheckMessage": "Desteklenmeyen Jackett 'tümü' uç noktasını kullanan dizinleyiciler: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle tüm dizinleyiciler kullanılamıyor", + "IndexerSearchNoInteractiveHealthCheckMessage": "Etkileşimli Arama etkinleştirildiğinde hiçbir dizinleyici kullanılamaz, {appName} herhangi bir etkileşimli arama sonucu sağlamayacaktır", + "IndexerSettingsAllowZeroSizeHelpText": "Bunu etkinleştirmek, sürüm boyutunu belirtmeyen beslemeleri kullanmanıza olanak tanır; ancak dikkatli olun, boyutla ilgili kontroller gerçekleştirilmeyecektir.", + "IndexerSettingsAnimeCategoriesHelpText": "Açılır listeyi boş bırakın, animeyi devre dışı bırakın", + "IndexerSettingsAnimeStandardFormatSearch": "Anime Standart Format Arama", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Ayrıca standart numaralandırmayı kullanarak anime arayın", + "IndexerSettingsApiPathHelpText": "API'ye giden yol, genellikle {url}", + "KeepAndTagSeries": "Diziyi Sakla ve Etiketle", + "ImportListsAniListSettingsImportReleasing": "Yayınlanan İçe Aktarma", + "ImportListsAniListSettingsImportCompleted": "İöe Aktarma Tamamlandı", + "ImportListsAniListSettingsImportDroppedHelpText": "Liste: Engellenen", + "ImportListsAniListSettingsImportPausedHelpText": "Liste: Beklemede", + "ImportListsTraktSettingsListNameHelpText": "İçe aktarma için liste adı, liste herkese açık olmalı veya listeye erişiminiz olmalı", + "ImportListsTraktSettingsListType": "Liste Türü", + "ImportListsTraktSettingsListTypeHelpText": "İçe aktarmak istediğiniz listenin türü", + "FailedToLoadUiSettingsFromApi": "API'den kullanıcı arayüzü ayarları alınamadı", + "FilterEqual": "eşit", + "IRCLinkText": "#sonarr Daima Özgür", + "EpisodeTitleRequiredHelpText": "Bölüm başlığı adlandırma biçimindeyse ve bölüm başlığı TBA ise 48 saate kadar içe aktarmayı önleyin", + "FullSeason": "Tam Sezon", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle kullanılamayan dizinleyiciler: {indexerNames}", + "IndexerSettingsAdditionalParametersNyaa": "Ek Parametreler", + "IndexerSettingsApiUrl": "API URL'si", + "IndexerSettingsCookieHelpText": "Sitenizin rss'e erişmek için bir giriş çerezine ihtiyacı varsa, bunu bir tarayıcı aracılığıyla almanız gerekecektir.", + "LibraryImportSeriesHeader": "Sahip olduğunuz mevcut dizileri içe aktarın", + "ErrorLoadingPage": "Bu sayfa yüklenirken bir hata oluştu", + "FilterDoesNotContain": "içermez", + "FilterEpisodesPlaceholder": "Bölümleri başlığa veya numaraya göre filtrele", + "FilterGreaterThan": "daha büyük", + "FilterSeriesPlaceholder": "Dizileri Filtrele", + "HideEpisodes": "Bölümleri gizle", + "ImportExistingSeries": "Mevcut Diziyi İçe Aktar", + "IndexerHDBitsSettingsCategoriesHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "FailedToLoadSonarr": "{appName} yüklenemedi", + "EpisodeNumbers": "Bölüm Numarası(ları)", + "EpisodeProgress": "Bölüm İlerlemesi", + "EpisodeTitle": "Bölüm Başlığı", + "EpisodeTitleRequired": "Bölüm Başlığı Gerekli", + "Episodes": "Bölümler", + "ExpandAll": "Tümünü Genişlet", + "Extend": "Genişlet", + "ImportListsTraktSettingsLimit": "Sınır", + "ImportListsTraktSettingsListName": "Liste Adı", + "FilterDoesNotStartWith": "ile başlamıyor", + "FilterLessThan": "daha az", + "FilterLessThanOrEqual": "daha az veya eşit", + "FilterNotEqual": "eşit değil", + "FinaleTooltip": "Dizi veya sezon finali", + "HasUnmonitoredSeason": "Takip Edilmeyen Sezon Var", + "HistorySeason": "Bu sezonun geçmişini görüntüle", + "HistoryModalHeaderSeason": "Tarih {season}", + "ICalIncludeUnmonitoredEpisodesHelpText": "Takip edilmeyen bölümleri iCal akışına dahil et", + "ICalTagsSeriesHelpText": "Besleme yalnızca en az bir eşleşen etikete sahip dizileri içerecektir", + "ImportListsAniListSettingsImportCompletedHelpText": "Liste: Tamamlanan Takip Edilenler", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "İçe aktarma listeleri için birden fazla kök klasör eksik: {rootFolderInfo}", + "ImportListsAniListSettingsImportPaused": "İçe Aktarma Duraklatıldı", + "ImportListsAniListSettingsImportPlanning": "İçe Aktarma Planlaması", + "ImportListsSimklSettingsUserListTypePlanToWatch": "İzlemeyi Planla", + "ImportListsTraktSettingsRating": "Derecelendirme", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Tüm Zamanların En Çok İzlenen Dizileri", + "ExistingSeries": "Mevcut Dizi", + "FilterIs": "dır", + "FilterEndsWith": "ile biter", + "FilterDoesNotEndWith": "ile bitmiyor", + "FilterInLast": "en son", + "FilterIsBefore": "öncesi", + "FilterIsNot": "değil", + "IconForFinales": "Finaller için Simge", + "IconForFinalesHelpText": "Mevcut bölüm bilgilerine göre dizi/sezon finalleri için simge göster", + "IconForSpecialsHelpText": "Özel bölümler için simge göster (sezon 0)", + "ImdbId": "IMDb Kimliği", + "ImportExtraFilesEpisodeHelpText": "Bir bölüm dosyasını içe aktardıktan sonra eşleşen ekstra dosyaları (altyazılar, nfo, vb.) içe aktarın", + "ImportListSearchForMissingEpisodes": "Eksik Bölümleri Ara", + "ImportListsAniListSettingsAuthenticateWithAniList": "AniList ile kimlik doğrulaması yapın", + "ImportListsAniListSettingsImportHiatus": "İçe Aktarmaya Ara Verildi", + "ImportListsAniListSettingsImportHiatusHelpText": "Medya: Diziye Ara Verildi", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Medya: Yayın henüz başlamadı", + "ImportListsAniListSettingsImportPlanningHelpText": "Liste: İzlemeyi Planlıyorum", + "ImportListsCustomListSettingsUrl": "Liste URL'si", + "ImportListsCustomListSettingsUrlHelpText": "Dizi listesi için URL", + "ImportListsImdbSettingsListIdHelpText": "IMDb liste kimliği (örn. ls12345678)", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Kaynak örneğinden içe aktarılacak Kalite Profilleri", + "ImportListsSonarrSettingsRootFoldersHelpText": "Kaynak örneğinden içe aktarılacak Kök Klasörler", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Yıla Göre Önerilen Gösteriler", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trend Olan Gösteriler", + "ImportListsTraktSettingsPopularName": "Trakt Popüler Listesi", + "ImportSeries": "Dizileri İçe Aktar", + "FailedToFetchSettings": "Ayarlar alınamadı", + "FailedToLoadSeriesFromApi": "API'den dizi alınamadı", + "FailedToLoadTagsFromApi": "API'den etiketler alınamadı", + "Here": "Burada", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Olası kısmi alımlar nedeniyle tüm listeler manuel etkileşim gerektirir", + "ImportListsTraktSettingsGenresHelpText": "Diziyi Trakt Slug Türüne göre filtrele (Virgülle Ayrılmış) Yalnızca Popüler Listeler İçin", + "IndexerSettingsAdditionalParameters": "Ek Parametreler", + "LibraryImportTipsQualityInEpisodeFilename": "Dosyalarınızın dosya adlarında kalite bilgisinin yer aldığından emin olun. Örneğin `dizi.adı.s02e15.bluray.mkv`", + "FailedToLoadQualityProfilesFromApi": "API'den kalite profilleri alınamadı", + "ImportListStatusAllUnavailableHealthCheckMessage": "Tüm listeler hatalar nedeniyle kullanılamıyor", + "ImportListsAniListSettingsImportFinishedHelpText": "Medya: Tüm bölümler yayınlandı", + "ImportListsAniListSettingsImportNotYetReleased": "İçe Aktarma Henüz Yayınlanmadı", + "ImportListsPlexSettingsWatchlistName": "Plex İzleme Listesi", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "{appName} örneğinden Sezon Senkronizasyon Takip Et, etkinleştirilirse yukarıda bulunan 'Takip' ayarları göz ardı edilecektir", + "ImportListsTraktSettingsPopularListTypePopularShows": "Popüler Gösteriler", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Yıla Göre En Çok İzlenen Diziler", + "ImportListsValidationUnableToConnectException": "İçe aktarma listesine bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin.", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Tamamlanmış İndirme İşlemini Etkinleştir", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Son zamanlardaki dizinleyici hataları nedeniyle tüm rss uyumlu dizinleyiciler geçici olarak kullanılamıyor", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS senkronizasyonu etkinleştirildiğinde dizinleyiciler kullanılamaz, {appName} yeni sürümleri otomatik olarak almayacaktır", + "IndexerSettingsCategoriesHelpText": "Açılır listeyi boş bırakın, standart/günlük gösterileri devre dışı bırakın", + "EpisodeTitleFootNote": "İsteğe bağlı olarak kesmeyi üç nokta (`...`) dahil olarak maksimum bayt boyutuna göre kontrol edin. Sondan (örn. `{Episode Title:30}`) veya başlangıçtan (örn. `{Episode Title:-30}`) kesme her ikisi de desteklenmektedir. Bölüm başlıkları, gerekirse dosya sistemi sınırlamalarına göre otomatik olarak kesilecektir.", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} bu sürümün hangi dizi ve bölüm için olduğunu belirleyemedi. {appName} bu sürümü otomatik olarak içe aktaramayabilir. '{title}' öğesini almak ister misiniz?", + "ImportListsCustomListValidationConnectionError": "Bu URL'ye istekte bulunulamıyor. DurumKodu: {exceptionStatusCode}", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Mümkünse Tamamlanmış İndirme İşlemini Etkinleştirin (Çoklu Bilgisayar desteklenmiyor)", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "IPTorrents tarafından yalnızca seçtiğiniz kategorileri (HD, SD, x264, vb.) kullanarak oluşturulan tam RSS besleme URL'si", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Lütfen kategoriyi değiştirmeniz durumunda yabancı dil sürümlerini önlemek için alt gruplar hakkında zorunlu/kısıtlı kurallar eklemeniz gerekeceğini unutmayın.", + "IndexerValidationNoResultsInConfiguredCategories": "Sorgu başarılı, ancak dizinleyicinizden yapılandırılan kategorilerde hiçbir sonuç döndürülmedi. Bu, dizinleyici veya dizinleyici kategori ayarlarınızdaki bir sorun olabilir.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Dizinleyici geçerli sorguyu desteklemiyor. Kategorilerin ve/veya sezon/bölüm aramasının desteklenip desteklenmediğini kontrol edin. Daha fazla ayrıntı için günlüğü kontrol edin.", + "MarkAsFailedConfirmation": "'{sourceTitle}' öğesini başarısız olarak işaretlemek istediğinizden emin misiniz?", + "ErrorLoadingContent": "Bu içerik yüklenirken bir hata oluştu", + "FilterContains": "içerir", + "ImportListsAniListSettingsImportReleasingHelpText": "Medya: Şu anda yeni bölümler yayınlanıyor", + "ImportListsAniListSettingsImportWatching": "İzlenilen İçe Aktarma", + "ImportListsCustomListValidationAuthenticationFailure": "Kimlik Doğrulama Hatası", + "ImportListsTraktSettingsLimitHelpText": "Alınacak dizi sayısını sınırlayın", + "ImportListsTraktSettingsUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Liste Türü İzlenen ise, listeyi sıralamak için sırayı seçin", + "IndexerSearchNoAutomaticHealthCheckMessage": "Otomatik Arama etkinleştirildiğinde hiçbir dizinleyici kullanılamaz, {appName} herhangi bir otomatik arama sonucu sağlamayacaktır", + "IndexerSettingsApiUrlHelpText": "Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. API anahtarınız ana sunucuya gönderilecektir.", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Bir sezon paketi torrentinin durdurulmadan önce başlatılması gereken süre, boş bırakıldığında indirme istemcisinin varsayılanı kullanılır", + "IndexerValidationCloudFlareCaptchaRequired": "Site CloudFlare CAPTCHA tarafından korunmaktadır. Geçerli CAPTCHA belirteci gereklidir.", + "IndexerValidationUnableToConnectServerUnavailable": "Dizinleyiciye bağlanılamıyor, dizinleyicinin sunucusu kullanılamıyor. Daha sonra tekrar deneyin. {exceptionMessage}.", + "ImportListsTraktSettingsUserListUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı (Yetkili Kullanıcı için boş bırakın)", + "ImportListsTraktSettingsYearsHelpText": "Diziyi yıla veya yıl aralığına göre filtreleyin", + "IndexerHDBitsSettingsMediums": "Ortamlar", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Son zamanlardaki dizinleyici hataları nedeniyle tüm arama yeteneğine sahip dizinleyiciler geçici olarak kullanılamıyor", + "IndexerSettingsSeasonPackSeedTime": "Sezon Paketi Seed Süresi", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen dizinleyicileri tek tek ekleyin", + "IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, dizinleyici veya dizinleyici kategori ayarlarınızdaki bir sorun olabilir.", + "IndexerValidationUnableToConnectResolutionFailure": "Dizinleyiciye bağlanılamıyor bağlantı hatası. Dizinleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.", + "IndexerSettingsFailDownloads": "Başarısız İndirmeler", + "IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName}, içe aktarmayı engelleyen seçili hataları başarısız indirmeler olarak ele alacaktır.", + "IndexerSettingsMinimumSeeders": "Minimum Seeder", + "IndexerSettingsRssUrl": "RSS URL", + "IndexerSettingsRssUrlHelpText": "{indexer} uyumlu bir RSS beslemesine URL girin", + "IndexerSettingsWebsiteUrl": "Web site URL'si", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tüm dizinleyiciler hatalar nedeniyle kullanılamıyor", + "IndexerStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle kullanılamayan dizinleyiciler: {indexerNames}", + "IndexerTagSeriesHelpText": "Bu indeksleyiciyi yalnızca en az bir eşleşen etiketi olan seriler için kullanın. Tüm serilerle kullanmak için boş bırakın.", + "IndexerValidationCloudFlareCaptchaExpired": "CloudFlare CAPTCHA token'ınızın süresi doldu, lütfen yenileyin.", + "IndexerValidationFeedNotSupported": "Dizinleyici beslemesi desteklenmiyor: {exceptionMessage}", + "IndexerValidationInvalidApiKey": "Geçersiz API Anahtarı", + "IndexerValidationJackettAllNotSupported": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen dizinleyicileri tek tek ekleyin", + "IndexerValidationRequestLimitReached": "Talep sınırına ulaşıldı: {exceptionMessage}", + "IndexerValidationSearchParametersNotSupported": "Dizinleyici gerekli arama parametrelerini desteklemiyor", + "IndexerValidationTestAbortedDueToError": "Test bir hata nedeniyle iptal edildi: {exceptionMessage}", + "IndexerValidationUnableToConnect": "Dizinleyiciye bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin", + "IndexerValidationUnableToConnectHttpError": "Dizinleyiciye bağlanılamıyor, lütfen DNS ayarlarınızı kontrol edin ve IPv6'nın çalıştığından veya devre dışı olduğundan emin olun. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "Dizinleyiciye bağlanılamıyor, geçersiz kimlik bilgileri. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Dizinleyiciye bağlanılamıyor, muhtemelen zaman aşımı nedeniyle. Tekrar deneyin veya ağ ayarlarınızı kontrol edin. {exceptionMessage}.", + "InteractiveImportNoEpisode": "Her seçili dosya için bir veya daha fazla bölüm seçilmelidir", + "InteractiveImportNoSeason": "Her seçilen dosya için sezon seçilmelidir", + "InteractiveImportNoSeries": "Her seçilen dosya için dizi seçilmelidir", + "KeepAndUnmonitorSeries": "Diziyi Tut ve Takip Etmeyi Bırak", + "InteractiveSearchModalHeaderSeason": "Etkileşimli Arama - {season}", + "InteractiveSearchResultsSeriesFailedErrorMessage": "Arama başarısız oldu çünkü {message}. Dizi bilgilerini yenilemeyi deneyin ve tekrar aramadan önce gerekli bilgilerin mevcut olduğundan emin olun.", + "KeyboardShortcutsCloseModal": "Mevcut Modalı Kapat", + "KeyboardShortcutsConfirmModal": "Onaylama Modalini Kabul Et", + "KeyboardShortcutsFocusSearchBox": "Arama Kutusuna Odaklan", + "KeyboardShortcutsOpenModal": "Bu Modalı Aç", + "KeyboardShortcutsSaveSettings": "Ayarları kaydet", + "LatestSeason": "Son Sezon", + "LiberaWebchat": "Özgür Webchat", + "LibraryImport": "Kütüphane İçe Aktarımı", + "LibraryImportTips": "İçe aktarmanın sorunsuz bir şekilde gerçekleşmesini sağlamak için bazı ipuçları:", + "LibraryImportTipsDontUseDownloadsFolder": "İndirme istemcinizden indirmeleri içe aktarmak için kullanmayın, bu yalnızca mevcut düzenlenmiş kitaplıklar içindir, sıralanmamış dosyalar için değildir.", + "LibraryImportTipsSeriesUseRootFolder": "{appName}'ı belirli bir diziye değil, tüm dizilerinizi içeren klasöre yönlendirin. Örneğin, \"`{goodFolderExample}`\" ve \"`{badFolderExample}`\" değil. Ayrıca, her dizi kök/kütüphane klasöründe kendi klasöründe olmalıdır.", + "ListExclusionsLoadError": "Hariç Tutulanlar Listesi Yüklenemedi", + "ListSyncTag": "Liste Senkronizasyon Etiketi", + "ListSyncTagHelpText": "Bu etiket, bir dizi listenizden düştüğünde veya artık listenizde olmadığında eklenecektir", + "ListsLoadError": "Listeler yüklenemiyor", + "LocalStorageIsNotSupported": "Yerel Depolama desteklenmiyor veya devre dışı. Bir eklenti veya özel tarama bunu devre dışı bırakmış olabilir.", + "ManageEpisodes": "Bölümleri Yönet", + "ManualImportItemsLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", + "MatchedToSeason": "Sezona Uygun", + "MatchedToEpisodes": "Bölümlere Uygun", + "UtcAirDate": "UTC Yayın Tarihi", + "MonitorNewSeasons": "Yeni Sezonları Takip Et", + "MonitorSpecialEpisodesDescription": "Diğer bölümlerin takip edilme durumunu değiştirmeden tüm özel bölümleri takip edin", + "MonitorRecentEpisodes": "Son Bölümler", + "SeriesPremiere": "Dizi Prömiyeri", + "MonitorAllEpisodes": "Tüm Bölümler", + "RestartSonarr": "{appName}'ı Yeniden Başlat", + "RootFolderSelectFreeSpace": "{freeSpace} Alan Boş", + "MetadataPlexSettingsEpisodeMappings": "Bölüm Eşlemeleri", + "NotificationsTagsSeriesHelpText": "Yalnızca en az bir eşleşen etiketi olan diziler için bildirim gönder", + "UpdateMonitoring": "Takip Edilenleri Güncelle", + "OnImportComplete": "İçe Aktarım Tamamlandı", + "OnSeriesDelete": "Diziyi Silme Durumunda", + "SeasonsMonitoredPartial": "Kısmi", + "ShowSeasonCount": "Sezon Sayısını Göster", + "ShowPreviousAiring": "Önceki Yayını Göster", + "UpdateStartupNotWritableHealthCheckMessage": "Başlangıç klasörü '{startupFolder}' '{userName}' kullanıcısı tarafından yazılabilir olmadığından güncelleme yüklenemiyor.", + "MetadataSourceSettingsSeriesSummary": "{appName} uygulamasının dizi ve bölüm bilgilerini nereden aldığına dair bilgiler", + "NextAiringDate": "Sonraki Yayın: {date}", + "SeasonsMonitoredAll": "Tümü", + "Umask750Description": "{octal} - Sahip yazma, Grup okuma yetkisi", + "MetadataPlexSettingsEpisodeMappingsHelpText": ".plexmatch dosyasındaki tüm dosyalar için bölüm eşlemelerini ekleyin", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Dizi klasöründe bir .plexmatch dosyası oluşturur", + "MetadataSettingsEpisodeImages": "Bölüm Görüntüleri", + "MetadataSettingsEpisodeMetadata": "Bölüm Meta Verisi", + "MetadataSettingsEpisodeMetadataImageThumbs": "Bölüm Meta Verisi Görüntü Küçük Resimleri", + "MetadataSettingsSeasonImages": "Sezon Görüntüleri", + "MetadataSettingsSeriesImages": "Dizi Görüntüleri", + "MetadataSettingsSeriesMetadata": "Dizi Meta Verisi", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Dizi Meta Verisi Bölüm Rehberi", + "MetadataSettingsSeriesMetadataUrl": "Dizi Meta Veri URL'si", + "MetadataSource": "Meta Veri Kaynağı", + "MetadataSourceSettings": "Meta Veri Kaynak Ayarları", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "<dosyadı>.nfo dosyasına resim küçük resim etiketlerini ekleyin ('Bölüm Meta Verisi' gerektirir)", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo tam dizi meta verileriyle", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "tvshow.nfo'ya TheTVDB şov URL'sini ekleyin ('Dizi Meta Verileri' ile birleştirilebilir)", + "MidseasonFinale": "Sezon Ortası Finali", + "MinutesThirty": "30 Dakika: {thirty}", + "MissingEpisodes": "Eksik Bölümler", + "Mixed": "Karışık", + "MonitorAllEpisodesDescription": "Özel bölümler hariç tüm bölümleri takip edin", + "MonitorAllSeasons": "Tüm Sezonlar", + "MonitorAllSeasonsDescription": "Tüm yeni sezonları otomatik olarak takip edin", + "MonitorExistingEpisodes": "Mevcut Bölümler", + "MonitorExistingEpisodesDescription": "Dosyaları olan veya henüz yayınlanmamış bölümleri takip edin", + "MonitorFirstSeason": "Birinci Sezon", + "MonitorFirstSeasonDescription": "İlk sezonun tüm bölümlerini takip edin. Diğer tüm sezonlar göz ardı edilecektir", + "MonitorFutureEpisodes": "Gelecek Bölümler", + "MonitorFutureEpisodesDescription": "Henüz yayınlanmamış bölümleri takip edin", + "MonitorLastSeason": "Geçen Sezon", + "MonitorLastSeasonDescription": "Geçtiğimiz sezonun tüm bölümlerini takip edin", + "MonitorNewItems": "Yeni Öğeleri Takip Et", + "MonitorNoEpisodes": "Hiçbiri", + "MonitorNoEpisodesDescription": "Hiçbir bölüm takip edilmeyecek", + "MonitorNoNewSeasons": "Yeni Sezon Yok", + "MonitorPilotEpisode": "Pilot Bölüm", + "MonitorNoNewSeasonsDescription": "Hiçbir yeni sezonu otomatik olarak takip etmeyin", + "MonitorPilotEpisodeDescription": "Sadece ilk sezonun ilk bölümünü takip edin", + "MonitorSeries": "Dizileri Takip Et", + "MonitorSpecialEpisodes": "Özel Bölümleri Takip Et", + "MonitoredEpisodesHelpText": "Bu dizide takip edilen bölümleri indirin", + "MonitoringOptions": "Takip Etme Seçenekleri", + "Monitoring": "Takip Durumu", + "MoveSeriesFoldersDontMoveFiles": "Hayır, Dosyaları Kendim Taşıyacağım", + "MoveSeriesFoldersMoveFiles": "Evet, Dosyaları Taşı", + "MoveSeriesFoldersToNewPath": "Dizi dosyalarını '{originalPath}' konumundan '{destinationPath}' konumuna taşımak ister misiniz?", + "MoveSeriesFoldersToRootFolder": "Dizi klasörlerini '{destinationRootFolder}' dizinine taşımak ister misiniz?", + "MountSeriesHealthCheckMessage": "Dizilerin içerdiği arabirim dizini salt okunur olarak bağlanır: ", + "MultiEpisode": "Çok Bölümlü", + "MultiEpisodeInvalidFormat": "Çoklu Bölüm: Geçersiz Format", + "MultiEpisodeStyle": "Çok Bölümlü Stil", + "MultiLanguages": "Çok Dilli", + "MultiSeason": "Çok Sezonlu", + "MyComputer": "Bilgisayarım", + "Network": "Ağ", + "NextAiring": "Sonraki Yayın", + "NoEpisodeHistory": "Bölüm geçmişi yok", + "NoEpisodeInformation": "Bölüm bilgisi mevcut değil.", + "NoEpisodeOverview": "Bölüm özeti yok", + "NoEpisodesFoundForSelectedSeason": "Seçilen sezon için bölüm bulunamadı", + "NoEpisodesInThisSeason": "Bu sezonda bölüm yok", + "NoMonitoredEpisodes": "Bu dizide takio edilen bölüm yok", + "NoMonitoredEpisodesSeason": "Bu sezonda takip edilen bölüm yok", + "NoSeasons": "Sezon yok", + "NoSeriesFoundImportOrAdd": "Dizi bulunamadı. Başlamak için mevcut dizilerinizi içe aktarmanız veya yeni bir dizi eklemeniz gerekir.", + "NoSeriesHaveBeenAdded": "Henüz dizi eklemediniz, öncelikle dizilerinizin bir kısmını veya tamamını içe aktarmak ister misiniz?", + "NotSeasonPack": "Sezon Paketi Olmayan", + "NotificationsGotifySettingIncludeSeriesPoster": "Dizi Posterini Dahil Et", + "NotificationsGotifySettingsMetadataLinksHelpText": "Bildirim gönderirken dizi meta verilerine bağlantılar ekleyin", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Bildirim gönderirken seri meta verilerine bağlantılar ekleyin", + "OnEpisodeFileDeleteForUpgrade": "Yükseltme İçin Bölüm Dosyası Silindiğinde", + "OnSeriesAdd": "Diziye Eklendiğinde", + "OnlyForBulkSeasonReleases": "Sadece Toplu Sezon Sürümleri İçin", + "OpenBrowserOnStartHelpText": " Bir web tarayıcısı açın ve uygulama başlangıcında {appName} ana sayfasına gidin.", + "OpenSeries": "Dizileri Aç", + "OrganizeModalHeaderSeason": "Düzenle ve Yeniden Adlandır - {season}", + "OrganizeSelectedSeriesModalAlert": "İpucu: Yeniden adlandırmayı önizlemek için \"İptal\"i seçin, ardından herhangi bir dizi başlığını seçin ve bu simgeyi kullanın:", + "OrganizeSelectedSeriesModalConfirmation": "Seçili {count} dizideki tüm dosyaları düzenlemek istediğinizden emin misiniz?", + "OrganizeSelectedSeriesModalHeader": "Seçili Dizileri Düzenle", + "Other": "Diğer", + "OverrideGrabNoEpisode": "En az bir bölüm seçilmelidir", + "PartialSeason": "Kısmi Sezon", + "PrefixedRange": "Ön Ek Aralığı", + "Premiere": "Prömiyer", + "PreviewRenameSeason": "Bu sezon için yeniden adlandırma önizlemesi", + "PreviousAiring": "Önceki Yayın", + "ProxyBadRequestHealthCheckMessage": "Proxy test edilemedi. Durum Kodu: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Proxy test edilemedi: {url}", + "ProxyResolveIpHealthCheckMessage": "Yapılandırılmış Proxy Ana Bilgisayarı {proxyHostName} için IP Adresi çözümlenemedi", + "QualityLimitsSeriesRuntimeHelpText": "Limitler, dizi çalışma süresine ve dosyadaki bölüm sayısına göre otomatik olarak ayarlanır.", + "QualityProfileInUseSeriesListCollection": "Bir diziye, listeye veya koleksiyona bağlı olan bir kalite profili silinemez", + "QuickSearch": "Hızlı Arama", + "Range": "Kapsam", + "RecycleBinUnableToWriteHealthCheckMessage": "Yapılandırılmış geri dönüşüm kutusu klasörüne yazılamıyor: {path}. Bu yolun var olduğundan ve {appName} uygulamasını çalıştıran kullanıcı tarafından yazılabilir olduğundan emin olun", + "RefreshAndScanTooltip": "Bilgileri yenile ve diski tara", + "RefreshSeries": "Dizileri Yenile", + "RegularExpression": "Düzenli İfade", + "ReleaseProfile": "Sürüm Profili", + "ReleaseProfileTagSeriesHelpText": "Sürüm profilleri en az bir eşleşen etikete sahip serilere uygulanacaktır. Tüm serilere uygulamak için boş bırakın", + "ReleaseSceneIndicatorAssumingTvdb": "Varsayılan olarak TheTVDB numaralandırması kullanılır.", + "ReleaseSceneIndicatorMappedNotRequested": "Seçilen bölüm bu aramaya dahil edilmedi.", + "ReleaseSceneIndicatorSourceMessage": "{message} sürümleri belirsiz numaralandırmayla mevcuttur ve bölümü güvenilir bir şekilde tanımlayamaz.", + "ReleaseSceneIndicatorUnknownMessage": "Bu bölüm için numaralandırma değişiklik göstermektedir ve sürüm bilinen hiçbir eşleştirmeyle uyuşmamaktadır.", + "ReleaseSceneIndicatorUnknownSeries": "Bilinmeyen bölüm veya dizi.", + "ReleaseType": "Sürüm Türü", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker kullanıyorsunuz; indirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak bu dizin konteynerin içinde görünmüyor. Uzak yol eşlemelerinizi ve konteyner hacmi ayarlarınızı inceleyin.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} indirilen bölümü {path} görebiliyor ancak erişemiyor. Muhtemelen izin hatası.", + "RemotePathMappingFileRemovedHealthCheckMessage": "{path} dosyası işleme sırasında kaldırıldı.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "{downloadClientName} indirme istemcisi {path} dizinindeki dosyaları raporladı ancak {appName} bu dizini göremiyor. Klasörün izinlerini ayarlamanız gerekebilir.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Yerel indirme istemcisi {downloadClientName}, {path} yolunda dosyalar raporladı ancak bu geçerli bir {osName} yolu değil. İndirme istemcisi ayarlarınızı inceleyin.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Uzaktan indirme istemcisi {downloadClientName} {path} yolunda dosyalar raporladı ancak bu geçerli bir {osName} yolu değil. Uzak yol eşlemelerinizi ve indirme istemcisi ayarlarınızı inceleyin.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName}, {downloadPath} indirme dizinini görebiliyor ancak erişemiyor. Muhtemelen izin hatası.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} (bir) bölümü içe aktaramadı. Ayrıntılar için günlüklerinizi kontrol edin.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Uzaktan indirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak bu dizin mevcut görünmüyor. Muhtemelen eksik veya yanlış uzak yol eşlemesi.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Uzaktan indirme istemcisi {downloadClientName} {path} dizininde dosyalar raporladı ancak bu dizin mevcut görünmüyor. Muhtemelen uzak yol eşlemesi eksik.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Uzaktan indirme istemcisi {downloadClientName} indirmeleri {path} yoluna yerleştiriyor ancak bu geçerli bir {osName} yolu değil. Uzak yol eşlemelerinizi ve indirme istemcisi ayarlarınızı inceleyin.", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "{series} dizisi TheTVDB'den kaldırıldı", + "RenameEpisodes": "Bölümleri Yeniden Adlandır", + "RenameEpisodesHelpText": "Yeniden adlandırma devre dışı bırakılırsa {appName} mevcut dosya adını kullanacaktır", + "Repeat": "Tekrarla", + "RescanAfterRefreshSeriesHelpText": "Diziyi yeniledikten sonra dizi klasörünü yeniden tarayın", + "RescanSeriesFolderAfterRefresh": "Yenilemeden Sonra Dizi Klasörünü Yeniden Tara", + "RestrictionsLoadError": "Kısıtlamalar yüklenemedi", + "RootFolderMissingHealthCheckMessage": "Kök klasör eksik: {rootFolderPath}", + "RootFolderMultipleMissingHealthCheckMessage": "Birden fazla kök klasör eksik: {rootFolderPaths}", + "RootFoldersLoadError": "Kök klasörler yüklenemiyor", + "Scene": "Sahne", + "SceneInfo": "Sahne Bilgisi", + "SceneInformation": "Sahne Bilgileri", + "SceneNumberNotVerified": "Sahne numarası henüz doğrulanmadı", + "SceneNumbering": "Sahne Numaralandırması", + "SearchByTvdbId": "Ayrıca bir tv gösterisinin TVDB ID'sini kullanarak da arama yapabilirsiniz. Örn. tvdb:71663", + "SearchFailedError": "Arama başarısız oldu, lütfen daha sonra tekrar deneyin.", + "SearchForAllMissingEpisodes": "Tüm eksik bölümleri arayın", + "SearchForAllMissingEpisodesConfirmationCount": "{totalRecords} eksik bölümün hepsini aramak istediğinizden emin misiniz?", + "SearchForCutoffUnmetEpisodes": "Tüm Kesinti Karşılamayan bölümlerini arayın", + "SearchForCutoffUnmetEpisodesConfirmationCount": "{totalRecords} kesinti karşılamamış bölümünün tümünü aramak istediğinizden emin misiniz?", + "SearchForMonitoredEpisodes": "Takip edilen bölümleri arayın", + "SearchForMonitoredEpisodesSeason": "Bu sezonda takip edilen bölümleri arayın", + "SearchForQuery": "{query} için arama yapın", + "SearchMonitored": "Takip Edilenleri Ara", + "Season": "Sezon", + "SeasonFolder": "Sezon Klasörü", + "SeasonInformation": "Sezon Bilgileri", + "SeasonNumber": "Sezon Numarası", + "SeasonPack": "Sezon Paketi", + "SeasonNumberToken": "{seasonNumber} Sezon", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} bölüm indirildi", + "SeasonPassTruncated": "Sadece son 25 sezon gösteriliyor, tüm sezonları görmek için ayrıntılara gidin", + "SeasonPremiere": "Sezon Prömiyeri", + "SeasonPremieresOnly": "Sadece Sezon Prömiyerleri", + "Seasons": "Sezonlar", + "SeasonsMonitoredNone": "Hiçbiri", + "SelectEpisodes": "Bölüm(ler)i seçin", + "SelectEpisodesModalTitle": "{modalTitle} - Bölüm(ler)i Seçin", + "SelectReleaseType": "Sürüm Türünü Seçin", + "SelectSeason": "Sezon Seçin", + "SelectSeasonModalTitle": "{modalTitle} - Sezon Seçin", + "SelectSeries": "Dizi Seçin", + "SeriesCannotBeFound": "Üzgünüz, Dizi bulunamadı.", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} bölüm dosyası", + "SeriesDetailsGoTo": "{title}'a git", + "SeriesDetailsNoEpisodeFiles": "Bölüm dosyası yok", + "SeriesDetailsOneEpisodeFile": "1 bölüm dosyası", + "SeriesEditor": "Dizi Editörü", + "SeriesFinale": "Dizi Finali", + "SeriesFolderFormat": "Dizi Klasör Formatı", + "SeriesFolderImportedTooltip": "Bölüm dizi klasöründen içe aktarıldı", + "SeriesID": "Dizi Kimliği", + "SeriesIndexFooterContinuing": "Devam ediyor (Tüm bölümler indirildi)", + "SeriesIndexFooterDownloading": "İndiriliyor (Bir veya daha fazla bölüm)", + "SeriesIndexFooterEnded": "Bitti (Tüm bölümler indirildi)", + "SeriesIsUnmonitored": "Dizi takip edilmiyor", + "SeriesLoadError": "Dizi yüklenemedi", + "SeriesMonitoring": "Takip Seçenekleri", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Toplam: {totalEpisodeCount}, İndiriliyor: {downloadingCount})", + "SeriesTitle": "Dizi Başlığı", + "SeriesTitleToExcludeHelpText": "Hariç tutulacak dizinin adı", + "SeriesType": "Dizi Türü", + "SeriesTypes": "Dizi Türleri", + "SeriesTypesHelpText": "Seri türü yeniden adlandırma, ayrıştırma ve arama için kullanılır", + "ShowBanners": "Bannerları Göster", + "ShowBannersHelpText": "Başlıklar yerine posterleri göster", + "ShowEpisodeInformation": "Bölüm Bilgilerini Göster", + "ShowEpisodeInformationHelpText": "Bölüm başlığını ve numarasını göster", + "ShowEpisodes": "Bölümleri göster", + "ShowNetwork": "Dijital Platfom'u Göster", + "ShowUnknownSeriesItems": "Bilinmeyen Dizi Öğelerini Göster", + "ShowUnknownSeriesItemsHelpText": "Sırada dizi olmayan öğeleri göster, buna {appName} kategorisindeki kaldırılmış diziler, filmler veya başka herhangi bir şey dahil olabilir", + "SingleEpisode": "Tek Bölüm", + "SingleEpisodeInvalidFormat": "Tek Bölüm: Geçersiz Format", + "SomeResultsAreHiddenByTheAppliedFilter": "Bazı sonuçlar uygulanan filtre tarafından gizlendi", + "SonarrTags": "{appName} Etiketleri", + "Special": "Özel", + "SpecialEpisode": "Özel Bölüm", + "Specials": "Özel Bölümler", + "SpecialsFolderFormat": "Özel Bölümler Klasör Formatı", + "Standard": "Standart", + "StandardEpisodeFormat": "Standart Bölüm Formatı", + "StandardEpisodeTypeDescription": "Bölümler SxxEyy şablonuyla yayınlanan", + "SupportedListsSeries": "{appName}, Dizileri veritabanına aktarmak için birden fazla listeyi destekler.", + "TableColumns": "Sütunlar", + "TheTvdb": "TVDB", + "TimeLeft": "Kalan zaman", + "ToggleMonitoredSeriesUnmonitored": "Dizi takip edilmiyorken takip durumu değiştirilemez", + "Total": "Toplam", + "TotalRecords": "Toplam kayıt: {totalRecords}", + "TvdbId": "TVDB Kimliği", + "Umask777Description": "{octal} - Herkese yazma yetkisi", + "Umask770Description": "{octal} - Sahip ve Grup yazma yetkisi", + "Umask775Description": "{octal} - Sahip ve Grup yazma, Diğerleri okuma yetkisi", + "Umask755Description": "{octal} Sahip yazma , Diğer kullanıcılar okuma yetkisi", + "UnableToLoadAutoTagging": "Otomatik etiketleme yüklenemiyor", + "UnableToLoadBackups": "Yedeklemeler yüklenemiyor", + "UnableToUpdateSonarrDirectly": "{appName} doğrudan güncellenemiyor,", + "UnmonitorDeletedEpisodes": "Silinen Bölümlerin Takibini Bırak", + "UnmonitorDeletedEpisodesHelpText": "Diskten silinen bölümler {appName} uygulamasında otomatik olarak takipten çıkarılır", + "UnmonitorSpecialsEpisodesDescription": "Diğer bölümlerin takip edilme durumunu değiştirmeden tüm özel bölümlerin takip edilmesini durdur", + "UnmonitorSpecialEpisodes": "Özel Bölümleri Takip Etme", + "UnmonitoredOnly": "Sadece Takip Edilmeyen", + "Upcoming": "Yaklaşan", + "UpcomingSeriesDescription": "Dizi duyuruldu ancak henüz kesin yayın tarihi belirsiz", + "UpdateStartupTranslocationHealthCheckMessage": "Başlangıç klasörü '{startupFolder}' bir Uygulama Taşıma klasöründe olduğundan güncelleme yüklenemiyor.", + "UpdateUiNotWritableHealthCheckMessage": "UI klasörü '{uiFolder}' '{userName}' kullanıcısı tarafından yazılabilir olmadığından güncelleme yüklenemiyor.", + "UpdatePath": "Güncelleme Yolu", + "UpdateSeriesPath": "Dizi Yolunu Güncelle", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Bu özel format puanına ulaşıldığında {appName} artık bölüm yayınlarını almayacak", + "UpgradeUntilEpisodeHelpText": "Bu kaliteye ulaşıldığında {appName} artık bölümleri indirmeyecek", + "UseSeasonFolderHelpText": "Bölümleri sezon klasörlerine ayırın", + "VersionNumber": "Sürüm {version}", + "WhyCantIFindMyShow": "Neden programı bulamıyorum?", + "MonitorMissingEpisodesDescription": "Dosyası olmayan veya henüz yayınlanmamış bölümleri takip edin", + "MetadataPlexSettingsSeriesPlexMatchFile": "Dizi İçin Plex Eşleşme Dosyası", + "OneSeason": "1 Sezon", + "RatingVotes": "Derecelendirme Oyları", + "PreviousAiringDate": "Önceki Yayın: {date}", + "RemovedSeriesSingleRemovedHealthCheckMessage": "{series} dizisi TVDB'den kaldırıldı", + "SeasonDetails": "Sezon Detayları", + "WithFiles": "Dosyalarla", + "SeasonCount": "Sezon Sayısı", + "SeasonsMonitoredStatus": "Takip Edilen Sezonlar", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Mesaja Dizi posterini ekle", + "SeasonFinale": "Sezon Finali", + "SeriesMatchType": "Dizi Eşleşme Türü", + "SeriesFolderFormatHelpText": "Yeni bir dizi eklerken veya dizi düzenleyicisi aracılığıyla diziyi taşırken kullanılır", + "OverrideGrabNoSeries": "Dizi seçilmelidir", + "SeriesIndexFooterMissingMonitored": "Eksik Bölümler (Takip edilen dizi)", + "ReleaseSceneIndicatorAssumingScene": "Sahne numaralandırmasını varsayalım.", + "UseSeasonFolder": "Sezon Klasörünü Kullan", + "TvdbIdExcludeHelpText": "Hariç tutulacak dizinin TVDB kimliği", + "SeasonFolderFormat": "Sezon Klasör Formatı", + "TableColumnsHelpText": "Hangi sütunların görünür olacağını ve hangi sırayla görüneceğini seçin", + "Tba": "Yakında duyurulacak", + "MinutesFortyFive": "45 Dakika: {fortyFive}", + "SeriesDetailsRuntime": "{runtime} Dakika", + "Twitter": "Twitter", + "MetadataProvidedBy": "Meta veriler {provider} tarafından sağlanmaktadır", + "StandardEpisodeTypeFormat": "Sezon ve bölüm numaraları ({format})", + "MonitorRecentEpisodesDescription": "Son 90 gün içinde yayınlanan bölümleri ve gelecekteki bölümleri takip edin", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Docker kullanıyorsunuz; indirme istemcisi {downloadClientName} indirmeleri {path} yoluna konumlandırıyor ancak bu geçerli bir {osName} dizini değil. Uzak yol eşlemelerinizi ve indirme istemcisi ayarlarınızı inceleyin.", + "SeriesIndexFooterMissingUnmonitored": "Eksik Bölümler (Takip edilmeyen dizi)", + "MonitorMissingEpisodes": "Eksik Bölümler", + "NotificationsPlexValidationNoTvLibraryFound": "En az bir TV kütüphanesi gereklidir", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Docker'ı kullanıyorsunuz; {downloadClientName} indirme istemcisi, dosyaları {path} yolunda rapor ediyor, ancak bu geçerli bir {osName} yolu değil. Uzak yol eşleşmelerinizi kontrol edin ve istemci ayarlarını indirin.", + "SeriesEditRootFolderHelpText": "Diziyi aynı kök klasöre taşımak, dizi klasörlerinin güncellenen başlık veya adlandırma biçimiyle eşleşecek şekilde yeniden adlandırılmasında kullanılabilir", + "MonitorNewSeasonsHelpText": "Hangi yeni sezonlar otomatik olarak takip edilmeli", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "İndirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak {appName} bu dizine erişemiyor. Klasörün izinlerini ayarlamanız gerekebilir.", + "SeriesIsMonitored": "Dizi takip ediliyor", + "MetadataSettingsSeriesSummary": "Bölümler içe aktarıldığında veya diziler yenilendiğinde meta veri dosyaları oluşturun", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "tvshow.nfo'ya JSON biçimli bölüm rehberi öğesini ekleyin ('Dizi Meta Verisi' gerektirir)", + "OnEpisodeFileDelete": "Bölüm Dosyası Silindiğinde", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Yerel indirme istemcisi {downloadClientName} indirmeleri {path} yoluna yerleştiyor ancak bu geçerli bir {osName} yolu değil. İndirme istemcisi ayarlarınızı inceleyin.", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Dizi ve bölüm bilgileri TheTVDB.com tarafından sağlanmaktadır. [Lütfen hizmetlerini desteklemeyi düşünün]({url}) .", + "SeriesFootNote": "İsteğe bağlı olarak, üç nokta (`...`) dahil olmak üzere maksimum bayt sayısına kadar kesmeyi kontrol edin. Sondan (örn. `{Series Title:30}`) veya başlangıçtan (örn. `{Series Title:-30}`) kesme her ikisi de desteklenir.", + "ShowSeriesTitleHelpText": "Poster altında dizi başlığını göster", + "SupportedImportListsMoreInfo": "Bireysel içe aktarma listeleri hakkında daha fazla bilgi edinmek için daha fazla bilgi butonlarına tıklayın.", + "NotificationsTelegramSettingsMetadataLinks": "Meta Veri Bağlantıları", + "MatchedToSeries": "Diziye Eşleşti", + "MaximumSingleEpisodeAge": "Tek Bir Bölümde Maksimum Geçen Süre", + "MaximumSingleEpisodeAgeHelpText": "Tam sezon araması sırasında, yalnızca sezonun son bölümünün bu ayardan daha eski olması durumunda sezon paketlerine izin verilecektir. Yalnızca standart diziler için. Devre dışı bırakmak için 0'ı kullanın." } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 0d758d187..db1c60668 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -367,5 +367,10 @@ "BypassDelayIfAboveCustomFormatScore": "Пропустити, якщо перевищено оцінку користувацького формату", "Clone": "Клонування", "BlocklistFilterHasNoItems": "Вибраний фільтр чорного списку не містить елементів", - "BypassDelayIfAboveCustomFormatScoreHelpText": "Увімкнути обхід, якщо реліз має оцінку вищу за встановлений мінімальний бал користувацького формату" + "BypassDelayIfAboveCustomFormatScoreHelpText": "Увімкнути обхід, якщо реліз має оцінку вищу за встановлений мінімальний бал користувацького формату", + "AutoTaggingRequiredHelpText": "Ця умова {0} має збігатися, щоб користувацький формат застосовувався. В іншому випадку достатньо одного збігу {1}.", + "CountIndexersSelected": "{count} індексер(-и) обрано", + "CountCustomFormatsSelected": "Користувацькі формати обрано {count}", + "BlocklistReleaseHelpText": "Блокує завантаження цього випуску {appName} через RSS або Автоматичний пошук", + "AutoTaggingNegateHelpText": "Якщо позначено, настроюваний формат не застосовуватиметься, якщо ця умова {0} збігається." } From 8ce688186e348e4f6dc7990a1b898cce2aff4dfb Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:54:14 +0200 Subject: [PATCH 684/762] Cleanup unused metadatas connector --- .../Metadata/Metadata/MetadatasConnector.js | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js diff --git a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js deleted file mode 100644 index 8675f4742..000000000 --- a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchMetadata } from 'Store/Actions/settingsActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import Metadatas from './Metadatas'; - -function createMapStateToProps() { - return createSelector( - createSortedSectionSelector('settings.metadata', sortByProp('name')), - (metadata) => metadata - ); -} - -const mapDispatchToProps = { - fetchMetadata -}; - -class MetadatasConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchMetadata(); - } - - // - // Render - - render() { - return ( - <Metadatas - {...this.props} - onConfirmDeleteMetadata={this.onConfirmDeleteMetadata} - /> - ); - } -} - -MetadatasConnector.propTypes = { - fetchMetadata: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); From 6c231cbe6aed73801c0c01ed987f2e2d16aa564f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:54:24 +0200 Subject: [PATCH 685/762] Increase input sizes in edit series modal --- .../Series/Edit/EditSeriesModalContent.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Edit/EditSeriesModalContent.tsx index 2362000a9..9a25c0fe5 100644 --- a/frontend/src/Series/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Edit/EditSeriesModalContent.tsx @@ -15,7 +15,13 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Popover from 'Components/Tooltip/Popover'; -import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; +import { + icons, + inputTypes, + kinds, + sizes, + tooltipPositions, +} from 'Helpers/Props'; import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal'; import useSeries from 'Series/useSeries'; import { saveSeries, setSeriesValue } from 'Store/Actions/seriesActions'; @@ -151,7 +157,7 @@ function EditSeriesModalContent({ <ModalBody> <Form {...otherSettings}> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('Monitored')}</FormLabel> <FormInputGroup @@ -163,7 +169,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel> {translate('MonitorNewSeasons')} <Popover @@ -183,7 +189,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('UseSeasonFolder')}</FormLabel> <FormInputGroup @@ -195,7 +201,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('QualityProfile')}</FormLabel> <FormInputGroup @@ -206,7 +212,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('SeriesType')}</FormLabel> <FormInputGroup @@ -218,7 +224,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('Path')}</FormLabel> <FormInputGroup @@ -229,7 +235,7 @@ function EditSeriesModalContent({ <FormInputButton key="fileBrowser" kind={kinds.DEFAULT} - title="Root Folder" + title={translate('RootFolder')} onPress={handleRootFolderPress} > <Icon name={icons.ROOT_FOLDER} /> @@ -239,7 +245,7 @@ function EditSeriesModalContent({ /> </FormGroup> - <FormGroup> + <FormGroup size={sizes.MEDIUM}> <FormLabel>{translate('Tags')}</FormLabel> <FormInputGroup From e8c3aa20bd92701a16dcd97c5e103b79b3683105 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Mon, 9 Dec 2024 04:36:10 +0100 Subject: [PATCH 686/762] New: Reactive search button on Wanted pages Closes #7449 --- .../src/Wanted/CutoffUnmet/CutoffUnmet.js | 20 ++++++------------- .../CutoffUnmet/CutoffUnmetConnector.js | 5 +++-- frontend/src/Wanted/Missing/Missing.js | 19 ++++++------------ .../src/Wanted/Missing/MissingConnector.js | 5 +++-- 4 files changed, 18 insertions(+), 31 deletions(-) diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 3b2703de1..57a0242c6 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -153,12 +153,15 @@ class CutoffUnmet extends Component { <PageToolbar> <PageToolbarSection> <PageToolbarButton - label={translate('SearchSelected')} + label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')} iconName={icons.SEARCH} - isDisabled={!itemsSelected || isSearchingForCutoffUnmetEpisodes} - onPress={this.onSearchSelectedPress} + isDisabled={isSearchingForCutoffUnmetEpisodes} + isSpinning={isSearchingForCutoffUnmetEpisodes} + onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress} /> + <PageToolbarSeparator /> + <PageToolbarButton label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')} iconName={icons.MONITORED} @@ -167,17 +170,6 @@ class CutoffUnmet extends Component { onPress={this.onToggleSelectedPress} /> - <PageToolbarSeparator /> - - <PageToolbarButton - label={translate('SearchAll')} - iconName={icons.SEARCH} - isDisabled={!items.length} - isSpinning={isSearchingForCutoffUnmetEpisodes} - onPress={this.onSearchAllCutoffUnmetPress} - /> - - <PageToolbarSeparator /> </PageToolbarSection> <PageToolbarSection alignContent={align.RIGHT}> diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index 6b52df496..6c4c13a74 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -18,9 +18,10 @@ function createMapStateToProps() { return createSelector( (state) => state.wanted.cutoffUnmet, createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH), - (cutoffUnmet, isSearchingForCutoffUnmetEpisodes) => { + createCommandExecutingSelector(commandNames.EPISODE_SEARCH), + (cutoffUnmet, isSearchingForAllCutoffUnmetEpisodes, isSearchingForSelectedCutoffUnmetEpisodes) => { return { - isSearchingForCutoffUnmetEpisodes, + isSearchingForCutoffUnmetEpisodes: isSearchingForAllCutoffUnmetEpisodes || isSearchingForSelectedCutoffUnmetEpisodes, isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1, ...cutoffUnmet }; diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js index 734b7e6c5..2783693a1 100644 --- a/frontend/src/Wanted/Missing/Missing.js +++ b/frontend/src/Wanted/Missing/Missing.js @@ -159,12 +159,15 @@ class Missing extends Component { <PageToolbar> <PageToolbarSection> <PageToolbarButton - label={translate('SearchSelected')} + label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')} iconName={icons.SEARCH} - isDisabled={!itemsSelected || isSearchingForMissingEpisodes} - onPress={this.onSearchSelectedPress} + isSpinning={isSearchingForMissingEpisodes} + isDisabled={isSearchingForMissingEpisodes} + onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress} /> + <PageToolbarSeparator /> + <PageToolbarButton label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')} iconName={icons.MONITORED} @@ -175,16 +178,6 @@ class Missing extends Component { <PageToolbarSeparator /> - <PageToolbarButton - label={translate('SearchAll')} - iconName={icons.SEARCH} - isDisabled={!items.length} - isSpinning={isSearchingForMissingEpisodes} - onPress={this.onSearchAllMissingPress} - /> - - <PageToolbarSeparator /> - <PageToolbarButton label={translate('ManualImport')} iconName={icons.INTERACTIVE} diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js index d6035ab11..576e4362c 100644 --- a/frontend/src/Wanted/Missing/MissingConnector.js +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -17,9 +17,10 @@ function createMapStateToProps() { return createSelector( (state) => state.wanted.missing, createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH), - (missing, isSearchingForMissingEpisodes) => { + createCommandExecutingSelector(commandNames.EPISODE_SEARCH), + (missing, isSearchingForAllMissingEpisodes, isSearchingForSelectedMissingEpisodes) => { return { - isSearchingForMissingEpisodes, + isSearchingForMissingEpisodes: isSearchingForAllMissingEpisodes || isSearchingForSelectedMissingEpisodes, isSaving: missing.items.filter((m) => m.isSaving).length > 1, ...missing }; From ebe23104d4b29a3c900a982fb84e75c27ed531ab Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 6 Dec 2024 20:27:11 -0800 Subject: [PATCH 687/762] Fixed: Custom Format score bypassing upgrades not being allowed --- .../UpgradeDiskSpecificationFixture.cs | 38 +++++++++++++++++++ .../UpgradeSpecificationFixture.cs | 4 +- .../DecisionEngine/DownloadRejectionReason.cs | 4 +- .../Specifications/QueueSpecification.cs | 12 +----- .../RssSync/HistorySpecification.cs | 3 ++ .../Specifications/UpgradableSpecification.cs | 9 ++++- .../UpgradeDiskSpecification.cs | 3 ++ .../DecisionEngine/UpgradeableRejectReason.cs | 3 +- 8 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 658810bbc..c560368dc 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -5,6 +5,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; @@ -399,5 +400,42 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } + + [Test] + public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_format_cutoff_is_above_current_score_and_is_revision_upgrade() + { + var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 }; + + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); + + GivenProfile(new QualityProfile + { + Cutoff = Quality.SDTV.Id, + MinFormatScore = 0, + CutoffFormatScore = 10000, + Items = Qualities.QualityFixture.GetDefaultQualities(), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"), + UpgradeAllowed = false + }); + + _parseResultSingle.Series.QualityProfile.Value.FormatItems = new List<ProfileFormatItem> + { + new ProfileFormatItem + { + Format = customFormat, + Score = 50 + } + }; + + GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1))); + GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2))); + + GivenOldCustomFormats(new List<CustomFormat>()); + GivenNewCustomFormats(new List<CustomFormat> { customFormat }); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index e3a7a71ee..e04d603da 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List<CustomFormat>(), new QualityModel(Quality.DVD, new Revision(version: 2)), new List<CustomFormat>()) - .Should().Be(UpgradeableRejectReason.CustomFormatScore); + .Should().Be(UpgradeableRejectReason.UpgradesNotAllowed); } [Test] @@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List<CustomFormat>(), new QualityModel(Quality.HDTV720p, new Revision(version: 1)), new List<CustomFormat>()) - .Should().Be(UpgradeableRejectReason.CustomFormatScore); + .Should().Be(UpgradeableRejectReason.UpgradesNotAllowed); } [Test] diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs index 72e83b93f..09b21dcd2 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs @@ -20,6 +20,7 @@ public enum DownloadRejectionReason HistoryCustomFormatCutoffMet, HistoryCustomFormatScore, HistoryCustomFormatScoreIncrement, + HistoryUpgradesNotAllowed, NoMatchingTag, PropersDisabled, ProperForOldFile, @@ -53,7 +54,7 @@ public enum DownloadRejectionReason QueueCustomFormatCutoffMet, QueueCustomFormatScore, QueueCustomFormatScoreIncrement, - QueueNoUpgrades, + QueueUpgradesNotAllowed, QueuePropersDisabled, Raw, MustContainMissing, @@ -72,4 +73,5 @@ public enum DownloadRejectionReason DiskCustomFormatCutoffMet, DiskCustomFormatScore, DiskCustomFormatScoreIncrement, + DiskUpgradesNotAllowed } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 66160260e..a51cfe3bf 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -95,17 +95,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications case UpgradeableRejectReason.MinCustomFormatScore: return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCustomFormatScoreIncrement, "Release in queue has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); - } - _logger.Debug("Checking if profiles allow upgrading. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); - - if (!_upgradableSpecification.IsUpgradeAllowed(subject.Series.QualityProfile, - remoteEpisode.ParsedEpisodeInfo.Quality, - queuedItemCustomFormats, - subject.ParsedEpisodeInfo.Quality, - subject.CustomFormats)) - { - return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueNoUpgrades, "Another release is queued and the Quality profile does not allow upgrades"); + case UpgradeableRejectReason.UpgradesNotAllowed: + return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueUpgradesNotAllowed, "Release in queue and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name); } if (_upgradableSpecification.IsRevisionUpgrade(remoteEpisode.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Quality)) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index f063c5e28..a8ce354f7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -111,6 +111,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync case UpgradeableRejectReason.MinCustomFormatScore: return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCustomFormatScoreIncrement, "{0} grab event in history has Custom Format score within Custom Format score increment: {1}", rejectionSubject, qualityProfile.MinUpgradeFormatScore); + + case UpgradeableRejectReason.UpgradesNotAllowed: + return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryUpgradesNotAllowed, "{0} grab event in history and Quality Profile '{1}' does not allow upgrades", rejectionSubject, qualityProfile.Name); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index ceaf3815c..f2b1028c2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -57,6 +57,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return UpgradeableRejectReason.None; } + if (!qualityProfile.UpgradeAllowed) + { + _logger.Debug("Quality profile '{0}' does not allow upgrading. Skipping.", qualityProfile.Name); + + return UpgradeableRejectReason.UpgradesNotAllowed; + } + // Reject unless the user does not prefer propers/repacks and it's a revision downgrade. if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && qualityRevisionCompare < 0) @@ -86,7 +93,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return UpgradeableRejectReason.CustomFormatScore; } - if (qualityProfile.UpgradeAllowed && currentFormatScore >= qualityProfile.CutoffFormatScore) + if (currentFormatScore >= qualityProfile.CutoffFormatScore) { _logger.Debug("Existing item meets cut-off for custom formats, skipping. Existing: [{0}] ({1}). Cutoff score: {2}", currentCustomFormats.ConcatToString(), diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 0185d4d87..87937b762 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -81,6 +81,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications case UpgradeableRejectReason.MinCustomFormatScore: return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScoreIncrement, "Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore); + + case UpgradeableRejectReason.UpgradesNotAllowed: + return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskUpgradesNotAllowed, "Existing file on disk and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name); } } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs index 2b1b1cfe9..ccf696d82 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.DecisionEngine QualityCutoff, CustomFormatScore, CustomFormatCutoff, - MinCustomFormatScore + MinCustomFormatScore, + UpgradesNotAllowed } } From 34ae65c087f52a65533d301b88ae9f50a564f6b8 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Mon, 9 Dec 2024 04:36:42 +0100 Subject: [PATCH 688/762] Refine localization string for IndexerSettingsFailDownloadsHelpText --- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 823e8356f..f2cc23a11 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -999,7 +999,7 @@ "IndexerSettingsCookie": "Cookie", "IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.", "IndexerSettingsFailDownloads": "Fail Downloads", - "IndexerSettingsFailDownloadsHelpText": "While processing completed downloads {appName} will treat selected errors preventing importing as failed downloads.", + "IndexerSettingsFailDownloadsHelpText": "While processing completed downloads {appName} will treat these selected filetypes as failed downloads.", "IndexerSettingsMinimumSeeders": "Minimum Seeders", "IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.", "IndexerSettingsMultiLanguageRelease": "Multi Languages", From 4e4bf3507f20c0f8581c66804f8ef406c41952d8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 7 Dec 2024 19:04:18 -0800 Subject: [PATCH 689/762] Convert MediaInfo to TypeScript --- .../src/Episode/Summary/EpisodeFileRow.tsx | 25 +++-- frontend/src/Episode/Summary/MediaInfo.js | 33 ------ frontend/src/Episode/Summary/MediaInfo.tsx | 27 +++++ .../EpisodeFileLanguageConnector.js | 17 --- .../src/EpisodeFile/EpisodeFileLanguages.tsx | 15 +++ frontend/src/EpisodeFile/MediaInfo.js | 105 ------------------ frontend/src/EpisodeFile/MediaInfo.tsx | 92 +++++++++++++++ .../src/EpisodeFile/MediaInfoConnector.js | 21 ---- frontend/src/Series/Details/EpisodeRow.js | 16 +-- frontend/src/Utilities/Object/getEntries.ts | 9 ++ .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 4 +- 11 files changed, 166 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/Episode/Summary/MediaInfo.js create mode 100644 frontend/src/Episode/Summary/MediaInfo.tsx delete mode 100644 frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js create mode 100644 frontend/src/EpisodeFile/EpisodeFileLanguages.tsx delete mode 100644 frontend/src/EpisodeFile/MediaInfo.js create mode 100644 frontend/src/EpisodeFile/MediaInfo.tsx delete mode 100644 frontend/src/EpisodeFile/MediaInfoConnector.js create mode 100644 frontend/src/Utilities/Object/getEntries.ts diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.tsx b/frontend/src/Episode/Summary/EpisodeFileRow.tsx index a6b084f78..d2bf5f4ba 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.tsx +++ b/frontend/src/Episode/Summary/EpisodeFileRow.tsx @@ -9,26 +9,27 @@ import Popover from 'Components/Tooltip/Popover'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import Language from 'Language/Language'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; import formatBytes from 'Utilities/Number/formatBytes'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; import MediaInfo from './MediaInfo'; import styles from './EpisodeFileRow.css'; -interface EpisodeFileRowProps { - path: string; - size: number; - languages: Language[]; - quality: QualityModel; - qualityCutoffNotMet: boolean; - customFormats: CustomFormat[]; - customFormatScore: number; - mediaInfo: object; +interface EpisodeFileRowProps + extends Pick< + EpisodeFile, + | 'path' + | 'size' + | 'languages' + | 'quality' + | 'customFormats' + | 'customFormatScore' + | 'qualityCutoffNotMet' + | 'mediaInfo' + > { columns: Column[]; onDeleteEpisodeFile(): void; } diff --git a/frontend/src/Episode/Summary/MediaInfo.js b/frontend/src/Episode/Summary/MediaInfo.js deleted file mode 100644 index af023266b..000000000 --- a/frontend/src/Episode/Summary/MediaInfo.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; - -function MediaInfo(props) { - return ( - <DescriptionList> - { - Object.keys(props).map((key) => { - const title = key - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()); - - const value = props[key]; - - if (!value) { - return null; - } - - return ( - <DescriptionListItem - key={key} - title={title} - data={props[key]} - /> - ); - }) - } - </DescriptionList> - ); -} - -export default MediaInfo; diff --git a/frontend/src/Episode/Summary/MediaInfo.tsx b/frontend/src/Episode/Summary/MediaInfo.tsx new file mode 100644 index 000000000..d0a895175 --- /dev/null +++ b/frontend/src/Episode/Summary/MediaInfo.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import MediaInfoProps from 'typings/MediaInfo'; +import getEntries from 'Utilities/Object/getEntries'; + +function MediaInfo(props: MediaInfoProps) { + return ( + <DescriptionList> + {getEntries(props).map(([key, value]) => { + const title = key + .replace(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase()); + + if (!value) { + return null; + } + + return ( + <DescriptionListItem key={key} title={title} data={props[key]} /> + ); + })} + </DescriptionList> + ); +} + +export default MediaInfo; diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js b/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js deleted file mode 100644 index 9178f37c0..000000000 --- a/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; - -function createMapStateToProps() { - return createSelector( - createEpisodeFileSelector(), - (episodeFile) => { - return { - languages: episodeFile ? episodeFile.languages : undefined - }; - } - ); -} - -export default connect(createMapStateToProps)(EpisodeLanguages); diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx new file mode 100644 index 000000000..c3ab2bbe1 --- /dev/null +++ b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import useEpisodeFile from './useEpisodeFile'; + +interface EpisodeFileLanguagesProps { + episodeFileId: number; +} + +function EpisodeFileLanguages({ episodeFileId }: EpisodeFileLanguagesProps) { + const episodeFile = useEpisodeFile(episodeFileId); + + return <EpisodeLanguages languages={episodeFile?.languages ?? []} />; +} + +export default EpisodeFileLanguages; diff --git a/frontend/src/EpisodeFile/MediaInfo.js b/frontend/src/EpisodeFile/MediaInfo.js deleted file mode 100644 index bcf196469..000000000 --- a/frontend/src/EpisodeFile/MediaInfo.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import getLanguageName from 'Utilities/String/getLanguageName'; -import translate from 'Utilities/String/translate'; -import * as mediaInfoTypes from './mediaInfoTypes'; - -function formatLanguages(languages) { - if (!languages) { - return null; - } - - const splitLanguages = _.uniq(languages.split('/')).map((l) => { - const simpleLanguage = l.split('_')[0]; - - if (simpleLanguage === 'und') { - return translate('Unknown'); - } - - return getLanguageName(simpleLanguage); - } - ); - - if (splitLanguages.length > 3) { - return ( - <span title={splitLanguages.join(', ')}> - {splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more - </span> - ); - } - - return ( - <span> - {splitLanguages.join(', ')} - </span> - ); -} - -function MediaInfo(props) { - const { - type, - audioChannels, - audioCodec, - audioLanguages, - subtitles, - videoCodec, - videoDynamicRangeType - } = props; - - if (type === mediaInfoTypes.AUDIO) { - return ( - <span> - { - audioCodec ? audioCodec : '' - } - - { - audioCodec && audioChannels ? ' - ' : '' - } - - { - audioChannels ? audioChannels.toFixed(1) : '' - } - </span> - ); - } - - if (type === mediaInfoTypes.AUDIO_LANGUAGES) { - return formatLanguages(audioLanguages); - } - - if (type === mediaInfoTypes.SUBTITLES) { - return formatLanguages(subtitles); - } - - if (type === mediaInfoTypes.VIDEO) { - return ( - <span> - {videoCodec} - </span> - ); - } - - if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) { - return ( - <span> - {videoDynamicRangeType} - </span> - ); - } - - return null; -} - -MediaInfo.propTypes = { - type: PropTypes.string.isRequired, - audioChannels: PropTypes.number, - audioCodec: PropTypes.string, - audioLanguages: PropTypes.string, - subtitles: PropTypes.string, - videoCodec: PropTypes.string, - videoDynamicRangeType: PropTypes.string -}; - -export default MediaInfo; diff --git a/frontend/src/EpisodeFile/MediaInfo.tsx b/frontend/src/EpisodeFile/MediaInfo.tsx new file mode 100644 index 000000000..2a72ee5bb --- /dev/null +++ b/frontend/src/EpisodeFile/MediaInfo.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import getLanguageName from 'Utilities/String/getLanguageName'; +import translate from 'Utilities/String/translate'; +import useEpisodeFile from './useEpisodeFile'; + +function formatLanguages(languages: string | undefined) { + if (!languages) { + return null; + } + + const splitLanguages = [...new Set(languages.split('/'))].map((l) => { + const simpleLanguage = l.split('_')[0]; + + if (simpleLanguage === 'und') { + return translate('Unknown'); + } + + return getLanguageName(simpleLanguage); + }); + + if (splitLanguages.length > 3) { + return ( + <span title={splitLanguages.join(', ')}> + {splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '} + more + </span> + ); + } + + return <span>{splitLanguages.join(', ')}</span>; +} + +export type MediaInfoType = + | 'audio' + | 'audioLanguages' + | 'subtitles' + | 'video' + | 'videoDynamicRangeType'; + +interface MediaInfoProps { + episodeFileId?: number; + type: MediaInfoType; +} + +function MediaInfo({ episodeFileId, type }: MediaInfoProps) { + const episodeFile = useEpisodeFile(episodeFileId); + + if (!episodeFile?.mediaInfo) { + return null; + } + + const { + audioChannels, + audioCodec, + audioLanguages, + subtitles, + videoCodec, + videoDynamicRangeType, + } = episodeFile.mediaInfo; + + if (type === 'audio') { + return ( + <span> + {audioCodec ? audioCodec : ''} + + {audioCodec && audioChannels ? ' - ' : ''} + + {audioChannels ? audioChannels.toFixed(1) : ''} + </span> + ); + } + + if (type === 'audioLanguages') { + return formatLanguages(audioLanguages); + } + + if (type === 'subtitles') { + return formatLanguages(subtitles); + } + + if (type === 'video') { + return <span>{videoCodec}</span>; + } + + if (type === 'videoDynamicRangeType') { + return <span>{videoDynamicRangeType}</span>; + } + + return null; +} + +export default MediaInfo; diff --git a/frontend/src/EpisodeFile/MediaInfoConnector.js b/frontend/src/EpisodeFile/MediaInfoConnector.js deleted file mode 100644 index bbb963cf4..000000000 --- a/frontend/src/EpisodeFile/MediaInfoConnector.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import MediaInfo from './MediaInfo'; - -function createMapStateToProps() { - return createSelector( - createEpisodeFileSelector(), - (episodeFile) => { - if (episodeFile) { - return { - ...episodeFile.mediaInfo - }; - } - - return {}; - } - ); -} - -export default connect(createMapStateToProps)(MediaInfo); diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index 85243b6bb..a1dc3e21a 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -13,8 +13,8 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import IndexerFlags from 'Episode/IndexerFlags'; -import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; -import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; +import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; +import MediaInfo from 'EpisodeFile/MediaInfo'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -229,7 +229,7 @@ class EpisodeRow extends Component { key={name} className={styles.languages} > - <EpisodeFileLanguageConnector + <EpisodeFileLanguages episodeFileId={episodeFileId} /> </TableRowCell> @@ -242,7 +242,7 @@ class EpisodeRow extends Component { key={name} className={styles.audio} > - <MediaInfoConnector + <MediaInfo type={mediaInfoTypes.AUDIO} episodeFileId={episodeFileId} /> @@ -256,7 +256,7 @@ class EpisodeRow extends Component { key={name} className={styles.audioLanguages} > - <MediaInfoConnector + <MediaInfo type={mediaInfoTypes.AUDIO_LANGUAGES} episodeFileId={episodeFileId} /> @@ -270,7 +270,7 @@ class EpisodeRow extends Component { key={name} className={styles.subtitles} > - <MediaInfoConnector + <MediaInfo type={mediaInfoTypes.SUBTITLES} episodeFileId={episodeFileId} /> @@ -284,7 +284,7 @@ class EpisodeRow extends Component { key={name} className={styles.video} > - <MediaInfoConnector + <MediaInfo type={mediaInfoTypes.VIDEO} episodeFileId={episodeFileId} /> @@ -298,7 +298,7 @@ class EpisodeRow extends Component { key={name} className={styles.videoDynamicRangeType} > - <MediaInfoConnector + <MediaInfo type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE} episodeFileId={episodeFileId} /> diff --git a/frontend/src/Utilities/Object/getEntries.ts b/frontend/src/Utilities/Object/getEntries.ts new file mode 100644 index 000000000..ca540c5da --- /dev/null +++ b/frontend/src/Utilities/Object/getEntries.ts @@ -0,0 +1,9 @@ +export type Entries<T> = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +function getEntries<T extends object>(obj: T): Entries<T> { + return Object.entries(obj) as Entries<T>; +} + +export default getEntries; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 05fed682c..6915f7b80 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -9,7 +9,7 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; +import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import styles from './CutoffUnmetRow.css'; @@ -123,7 +123,7 @@ function CutoffUnmetRow(props) { key={name} className={styles.languages} > - <EpisodeFileLanguageConnector + <EpisodeFileLanguages episodeFileId={episodeFileId} /> </TableRowCell> From 03b8c4c28e1d0a5d5caa4c6f4dd04a7edf5c4a17 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 7 Dec 2024 19:28:58 -0800 Subject: [PATCH 690/762] Convert EpisodeSearch to TypeScript --- .../Episode/EpisodeDetailsModalContent.tsx | 4 +- frontend/src/Episode/Search/EpisodeSearch.js | 56 ----------- frontend/src/Episode/Search/EpisodeSearch.tsx | 80 ++++++++++++++++ .../Episode/Search/EpisodeSearchConnector.js | 93 ------------------- 4 files changed, 82 insertions(+), 151 deletions(-) delete mode 100644 frontend/src/Episode/Search/EpisodeSearch.js create mode 100644 frontend/src/Episode/Search/EpisodeSearch.tsx delete mode 100644 frontend/src/Episode/Search/EpisodeSearchConnector.js diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.tsx b/frontend/src/Episode/EpisodeDetailsModalContent.tsx index 05a08f16f..75c8bef73 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContent.tsx +++ b/frontend/src/Episode/EpisodeDetailsModalContent.tsx @@ -20,7 +20,7 @@ import { } from 'Store/Actions/releaseActions'; import translate from 'Utilities/String/translate'; import EpisodeHistoryConnector from './History/EpisodeHistoryConnector'; -import EpisodeSearchConnector from './Search/EpisodeSearchConnector'; +import EpisodeSearch from './Search/EpisodeSearch'; import SeasonEpisodeNumber from './SeasonEpisodeNumber'; import EpisodeSummary from './Summary/EpisodeSummary'; import styles from './EpisodeDetailsModalContent.css'; @@ -174,7 +174,7 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) { <TabPanel> {/* Don't wrap in tabContent so we not have a top margin */} - <EpisodeSearchConnector + <EpisodeSearch episodeId={episodeId} startInteractiveSearch={startInteractiveSearch} onModalClose={onModalClose} diff --git a/frontend/src/Episode/Search/EpisodeSearch.js b/frontend/src/Episode/Search/EpisodeSearch.js deleted file mode 100644 index 87451e63e..000000000 --- a/frontend/src/Episode/Search/EpisodeSearch.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import { icons, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './EpisodeSearch.css'; - -function EpisodeSearch(props) { - const { - onQuickSearchPress, - onInteractiveSearchPress - } = props; - - return ( - <div> - <div className={styles.buttonContainer}> - <Button - className={styles.button} - size={sizes.LARGE} - onPress={onQuickSearchPress} - > - <Icon - className={styles.buttonIcon} - name={icons.QUICK} - /> - - {translate('QuickSearch')} - </Button> - </div> - - <div className={styles.buttonContainer}> - <Button - className={styles.button} - kind={kinds.PRIMARY} - size={sizes.LARGE} - onPress={onInteractiveSearchPress} - > - <Icon - className={styles.buttonIcon} - name={icons.INTERACTIVE} - /> - - {translate('InteractiveSearch')} - </Button> - </div> - </div> - ); -} - -EpisodeSearch.propTypes = { - onQuickSearchPress: PropTypes.func.isRequired, - onInteractiveSearchPress: PropTypes.func.isRequired -}; - -export default EpisodeSearch; diff --git a/frontend/src/Episode/Search/EpisodeSearch.tsx b/frontend/src/Episode/Search/EpisodeSearch.tsx new file mode 100644 index 000000000..818bb5d54 --- /dev/null +++ b/frontend/src/Episode/Search/EpisodeSearch.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import InteractiveSearch from 'InteractiveSearch/InteractiveSearch'; +import { executeCommand } from 'Store/Actions/commandActions'; +import translate from 'Utilities/String/translate'; +import styles from './EpisodeSearch.css'; + +interface EpisodeSearchProps { + episodeId: number; + startInteractiveSearch: boolean; + onModalClose: () => void; +} + +function EpisodeSearch({ + episodeId, + startInteractiveSearch, + onModalClose, +}: EpisodeSearchProps) { + const dispatch = useDispatch(); + const { isPopulated } = useSelector((state: AppState) => state.releases); + + const [isInteractiveSearchOpen, setIsInteractiveSearchOpen] = useState( + startInteractiveSearch || isPopulated + ); + + const handleQuickSearchPress = useCallback(() => { + dispatch( + executeCommand({ + name: commandNames.EPISODE_SEARCH, + episodeIds: [episodeId], + }) + ); + + onModalClose(); + }, [episodeId, dispatch, onModalClose]); + + const handleInteractiveSearchPress = useCallback(() => { + setIsInteractiveSearchOpen(true); + }, []); + + if (isInteractiveSearchOpen) { + return <InteractiveSearch type="episode" searchPayload={{ episodeId }} />; + } + + return ( + <div> + <div className={styles.buttonContainer}> + <Button + className={styles.button} + size={sizes.LARGE} + onPress={handleQuickSearchPress} + > + <Icon className={styles.buttonIcon} name={icons.QUICK} /> + + {translate('QuickSearch')} + </Button> + </div> + + <div className={styles.buttonContainer}> + <Button + className={styles.button} + kind={kinds.PRIMARY} + size={sizes.LARGE} + onPress={handleInteractiveSearchPress} + > + <Icon className={styles.buttonIcon} name={icons.INTERACTIVE} /> + + {translate('InteractiveSearch')} + </Button> + </div> + </div> + ); +} + +export default EpisodeSearch; diff --git a/frontend/src/Episode/Search/EpisodeSearchConnector.js b/frontend/src/Episode/Search/EpisodeSearchConnector.js deleted file mode 100644 index 9b41dd9c4..000000000 --- a/frontend/src/Episode/Search/EpisodeSearchConnector.js +++ /dev/null @@ -1,93 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import InteractiveSearch from 'InteractiveSearch/InteractiveSearch'; -import { executeCommand } from 'Store/Actions/commandActions'; -import EpisodeSearch from './EpisodeSearch'; - -function createMapStateToProps() { - return createSelector( - (state) => state.releases, - (releases) => { - return { - isPopulated: releases.isPopulated - }; - } - ); -} - -const mapDispatchToProps = { - executeCommand -}; - -class EpisodeSearchConnector extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isInteractiveSearchOpen: props.startInteractiveSearch - }; - } - - componentDidMount() { - if (this.props.isPopulated) { - this.setState({ isInteractiveSearchOpen: true }); - } - } - - // - // Listeners - - onQuickSearchPress = () => { - this.props.executeCommand({ - name: commandNames.EPISODE_SEARCH, - episodeIds: [this.props.episodeId] - }); - - this.props.onModalClose(); - }; - - onInteractiveSearchPress = () => { - this.setState({ isInteractiveSearchOpen: true }); - }; - - // - // Render - - render() { - const { episodeId } = this.props; - - if (this.state.isInteractiveSearchOpen) { - return ( - <InteractiveSearch - type="episode" - searchPayload={{ episodeId }} - /> - ); - } - - return ( - <EpisodeSearch - {...this.props} - onQuickSearchPress={this.onQuickSearchPress} - onInteractiveSearchPress={this.onInteractiveSearchPress} - /> - ); - } -} - -EpisodeSearchConnector.propTypes = { - episodeId: PropTypes.number.isRequired, - isPopulated: PropTypes.bool.isRequired, - startInteractiveSearch: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeSearchConnector); From f1d54d2a9a01bbbe4f75cb1d05184e3849d7ed1d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 7 Dec 2024 19:52:26 -0800 Subject: [PATCH 691/762] Convert EpisodeHistory to TypeScript --- frontend/src/App/State/AppState.ts | 1 + .../Episode/EpisodeDetailsModalContent.tsx | 4 +- .../src/Episode/History/EpisodeHistory.js | 130 ------------- .../src/Episode/History/EpisodeHistory.tsx | 129 +++++++++++++ .../History/EpisodeHistoryConnector.js | 63 ------- .../src/Episode/History/EpisodeHistoryRow.js | 177 ------------------ .../src/Episode/History/EpisodeHistoryRow.tsx | 151 +++++++++++++++ .../Episode/SelectEpisodeModalContent.tsx | 3 - 8 files changed, 283 insertions(+), 375 deletions(-) delete mode 100644 frontend/src/Episode/History/EpisodeHistory.js create mode 100644 frontend/src/Episode/History/EpisodeHistory.tsx delete mode 100644 frontend/src/Episode/History/EpisodeHistoryConnector.js delete mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.js create mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.tsx diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 8dfecab9e..36047cc4e 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -70,6 +70,7 @@ interface AppState { captcha: CaptchaAppState; commands: CommandAppState; episodeFiles: EpisodeFilesAppState; + episodeHistory: HistoryAppState; episodes: EpisodesAppState; episodesSelection: EpisodesAppState; history: HistoryAppState; diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.tsx b/frontend/src/Episode/EpisodeDetailsModalContent.tsx index 75c8bef73..ec5a14116 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContent.tsx +++ b/frontend/src/Episode/EpisodeDetailsModalContent.tsx @@ -19,7 +19,7 @@ import { clearReleases, } from 'Store/Actions/releaseActions'; import translate from 'Utilities/String/translate'; -import EpisodeHistoryConnector from './History/EpisodeHistoryConnector'; +import EpisodeHistory from './History/EpisodeHistory'; import EpisodeSearch from './Search/EpisodeSearch'; import SeasonEpisodeNumber from './SeasonEpisodeNumber'; import EpisodeSummary from './Summary/EpisodeSummary'; @@ -168,7 +168,7 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) { <TabPanel> <div className={styles.tabContent}> - <EpisodeHistoryConnector episodeId={episodeId} /> + <EpisodeHistory episodeId={episodeId} /> </div> </TabPanel> diff --git a/frontend/src/Episode/History/EpisodeHistory.js b/frontend/src/Episode/History/EpisodeHistory.js deleted file mode 100644 index 78f05a82d..000000000 --- a/frontend/src/Episode/History/EpisodeHistory.js +++ /dev/null @@ -1,130 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EpisodeHistoryRow from './EpisodeHistoryRow'; - -const columns = [ - { - name: 'eventType', - isVisible: true - }, - { - name: 'sourceTitle', - label: () => translate('SourceTitle'), - isVisible: true - }, - { - name: 'languages', - label: () => translate('Languages'), - isVisible: true - }, - { - name: 'quality', - label: () => translate('Quality'), - isVisible: true - }, - { - name: 'customFormats', - label: () => translate('CustomFormats'), - isSortable: false, - isVisible: true - }, - { - name: 'customFormatScore', - label: React.createElement(Icon, { - name: icons.SCORE, - title: () => translate('CustomFormatScore') - }), - isSortable: true, - isVisible: true - }, - { - name: 'date', - label: () => translate('Date'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -class EpisodeHistory extends Component { - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - items, - onMarkAsFailedPress - } = this.props; - - const hasItems = !!items.length; - - if (isFetching) { - return ( - <LoadingIndicator /> - ); - } - - if (!isFetching && !!error) { - return ( - <Alert kind={kinds.DANGER}>{translate('EpisodeHistoryLoadError')}</Alert> - ); - } - - if (isPopulated && !hasItems && !error) { - return ( - <Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert> - ); - } - - if (isPopulated && hasItems && !error) { - return ( - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - return ( - <EpisodeHistoryRow - key={item.id} - {...item} - onMarkAsFailedPress={onMarkAsFailedPress} - /> - ); - }) - } - </TableBody> - </Table> - ); - } - - return null; - } -} - -EpisodeHistory.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired -}; - -EpisodeHistory.defaultProps = { - selectedTab: 'details' -}; - -export default EpisodeHistory; diff --git a/frontend/src/Episode/History/EpisodeHistory.tsx b/frontend/src/Episode/History/EpisodeHistory.tsx new file mode 100644 index 000000000..ea323ec60 --- /dev/null +++ b/frontend/src/Episode/History/EpisodeHistory.tsx @@ -0,0 +1,129 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { icons, kinds } from 'Helpers/Props'; +import { + clearEpisodeHistory, + episodeHistoryMarkAsFailed, + fetchEpisodeHistory, +} from 'Store/Actions/episodeHistoryActions'; +import translate from 'Utilities/String/translate'; +import EpisodeHistoryRow from './EpisodeHistoryRow'; + +const columns: Column[] = [ + { + name: 'eventType', + label: '', + isVisible: true, + }, + { + name: 'sourceTitle', + label: () => translate('SourceTitle'), + isVisible: true, + }, + { + name: 'languages', + label: () => translate('Languages'), + isVisible: true, + }, + { + name: 'quality', + label: () => translate('Quality'), + isVisible: true, + }, + { + name: 'customFormats', + label: () => translate('CustomFormats'), + isSortable: false, + isVisible: true, + }, + { + name: 'customFormatScore', + label: React.createElement(Icon, { + name: icons.SCORE, + title: () => translate('CustomFormatScore'), + }), + isSortable: true, + isVisible: true, + }, + { + name: 'date', + label: () => translate('Date'), + isVisible: true, + }, + { + name: 'actions', + label: '', + isVisible: true, + }, +]; + +interface EpisodeHistoryProps { + episodeId: number; +} + +function EpisodeHistory({ episodeId }: EpisodeHistoryProps) { + const dispatch = useDispatch(); + const { items, isFetching, isPopulated, error } = useSelector( + (state: AppState) => state.episodeHistory + ); + + const handleMarkAsFailedPress = useCallback( + (historyId: number) => { + dispatch(episodeHistoryMarkAsFailed({ historyId, episodeId })); + }, + [episodeId, dispatch] + ); + + const hasItems = !!items.length; + + useEffect(() => { + dispatch(fetchEpisodeHistory({ episodeId })); + + return () => { + dispatch(clearEpisodeHistory()); + }; + }, [episodeId, dispatch]); + + if (isFetching) { + return <LoadingIndicator />; + } + + if (!isFetching && !!error) { + return ( + <Alert kind={kinds.DANGER}>{translate('EpisodeHistoryLoadError')}</Alert> + ); + } + + if (isPopulated && !hasItems && !error) { + return <Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert>; + } + + if (isPopulated && hasItems && !error) { + return ( + <Table columns={columns}> + <TableBody> + {items.map((item) => { + return ( + <EpisodeHistoryRow + key={item.id} + {...item} + onMarkAsFailedPress={handleMarkAsFailedPress} + /> + ); + })} + </TableBody> + </Table> + ); + } + + return null; +} + +export default EpisodeHistory; diff --git a/frontend/src/Episode/History/EpisodeHistoryConnector.js b/frontend/src/Episode/History/EpisodeHistoryConnector.js deleted file mode 100644 index 1e3414646..000000000 --- a/frontend/src/Episode/History/EpisodeHistoryConnector.js +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearEpisodeHistory, episodeHistoryMarkAsFailed, fetchEpisodeHistory } from 'Store/Actions/episodeHistoryActions'; -import EpisodeHistory from './EpisodeHistory'; - -function createMapStateToProps() { - return createSelector( - (state) => state.episodeHistory, - (episodeHistory) => { - return episodeHistory; - } - ); -} - -const mapDispatchToProps = { - fetchEpisodeHistory, - clearEpisodeHistory, - episodeHistoryMarkAsFailed -}; - -class EpisodeHistoryConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchEpisodeHistory({ episodeId: this.props.episodeId }); - } - - componentWillUnmount() { - this.props.clearEpisodeHistory(); - } - - // - // Listeners - - onMarkAsFailedPress = (historyId) => { - this.props.episodeHistoryMarkAsFailed({ historyId, episodeId: this.props.episodeId }); - }; - - // - // Render - - render() { - return ( - <EpisodeHistory - {...this.props} - onMarkAsFailedPress={this.onMarkAsFailedPress} - /> - ); - } -} - -EpisodeHistoryConnector.propTypes = { - episodeId: PropTypes.number.isRequired, - fetchEpisodeHistory: PropTypes.func.isRequired, - clearEpisodeHistory: PropTypes.func.isRequired, - episodeHistoryMarkAsFailed: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeHistoryConnector); diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js deleted file mode 100644 index fd7fea827..000000000 --- a/frontend/src/Episode/History/EpisodeHistoryRow.js +++ /dev/null @@ -1,177 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import HistoryDetails from 'Activity/History/Details/HistoryDetails'; -import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import Popover from 'Components/Tooltip/Popover'; -import EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; -import styles from './EpisodeHistoryRow.css'; - -function getTitle(eventType) { - switch (eventType) { - case 'grabbed': return 'Grabbed'; - case 'seriesFolderImported': return 'Series Folder Imported'; - case 'downloadFolderImported': return 'Download Folder Imported'; - case 'downloadFailed': return 'Download Failed'; - case 'episodeFileDeleted': return 'Episode File Deleted'; - case 'episodeFileRenamed': return 'Episode File Renamed'; - default: return 'Unknown'; - } -} - -class EpisodeHistoryRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isMarkAsFailedModalOpen: false - }; - } - - // - // Listeners - - onMarkAsFailedPress = () => { - this.setState({ isMarkAsFailedModalOpen: true }); - }; - - onConfirmMarkAsFailed = () => { - this.props.onMarkAsFailedPress(this.props.id); - this.setState({ isMarkAsFailedModalOpen: false }); - }; - - onMarkAsFailedModalClose = () => { - this.setState({ isMarkAsFailedModalOpen: false }); - }; - - // - // Render - - render() { - const { - eventType, - sourceTitle, - languages, - quality, - qualityCutoffNotMet, - customFormats, - customFormatScore, - date, - data, - downloadId - } = this.props; - - const { - isMarkAsFailedModalOpen - } = this.state; - - return ( - <TableRow> - <HistoryEventTypeCell - eventType={eventType} - data={data} - /> - - <TableRowCell> - {sourceTitle} - </TableRowCell> - - <TableRowCell> - <EpisodeLanguages languages={languages} /> - </TableRowCell> - - <TableRowCell> - <EpisodeQuality - quality={quality} - isCutoffNotMet={qualityCutoffNotMet} - /> - </TableRowCell> - - <TableRowCell> - <EpisodeFormats formats={customFormats} /> - </TableRowCell> - - <TableRowCell> - {formatCustomFormatScore(customFormatScore, customFormats.length)} - </TableRowCell> - - <RelativeDateCell - date={date} - includeSeconds={true} - includeTime={true} - /> - - <TableRowCell className={styles.actions}> - <Popover - anchor={ - <Icon - name={icons.INFO} - /> - } - title={getTitle(eventType)} - body={ - <HistoryDetails - eventType={eventType} - sourceTitle={sourceTitle} - data={data} - downloadId={downloadId} - /> - } - position={tooltipPositions.LEFT} - /> - - { - eventType === 'grabbed' && - <IconButton - title={translate('MarkAsFailed')} - name={icons.REMOVE} - size={14} - onPress={this.onMarkAsFailedPress} - /> - } - </TableRowCell> - - <ConfirmModal - isOpen={isMarkAsFailedModalOpen} - kind={kinds.DANGER} - title={translate('MarkAsFailed')} - message={translate('MarkAsFailedConfirmation', { sourceTitle })} - confirmLabel={translate('MarkAsFailed')} - onConfirm={this.onConfirmMarkAsFailed} - onCancel={this.onMarkAsFailedModalClose} - /> - </TableRow> - ); - } -} - -EpisodeHistoryRow.propTypes = { - id: PropTypes.number.isRequired, - eventType: PropTypes.string.isRequired, - sourceTitle: PropTypes.string.isRequired, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - quality: PropTypes.object.isRequired, - qualityCutoffNotMet: PropTypes.bool.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - date: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, - downloadId: PropTypes.string, - onMarkAsFailedPress: PropTypes.func.isRequired -}; - -export default EpisodeHistoryRow; diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.tsx b/frontend/src/Episode/History/EpisodeHistoryRow.tsx new file mode 100644 index 000000000..97b8cb479 --- /dev/null +++ b/frontend/src/Episode/History/EpisodeHistoryRow.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useState } from 'react'; +import HistoryDetails from 'Activity/History/Details/HistoryDetails'; +import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; +import { HistoryData, HistoryEventType } from 'typings/History'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import translate from 'Utilities/String/translate'; +import styles from './EpisodeHistoryRow.css'; + +function getTitle(eventType: HistoryEventType) { + switch (eventType) { + case 'grabbed': + return 'Grabbed'; + case 'seriesFolderImported': + return 'Series Folder Imported'; + case 'downloadFolderImported': + return 'Download Folder Imported'; + case 'downloadFailed': + return 'Download Failed'; + case 'episodeFileDeleted': + return 'Episode File Deleted'; + case 'episodeFileRenamed': + return 'Episode File Renamed'; + default: + return 'Unknown'; + } +} + +interface EpisodeHistoryRowProps { + id: number; + eventType: HistoryEventType; + sourceTitle: string; + languages: Language[]; + quality: QualityModel; + qualityCutoffNotMet: boolean; + customFormats: CustomFormat[]; + customFormatScore: number; + date: string; + data: HistoryData; + downloadId?: string; + onMarkAsFailedPress: (id: number) => void; +} + +function EpisodeHistoryRow({ + id, + eventType, + sourceTitle, + languages, + quality, + qualityCutoffNotMet, + customFormats, + customFormatScore, + date, + data, + downloadId, + onMarkAsFailedPress, +}: EpisodeHistoryRowProps) { + const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false); + + const handleMarkAsFailedPress = useCallback(() => { + setIsMarkAsFailedModalOpen(true); + }, []); + + const handleConfirmMarkAsFailed = useCallback(() => { + onMarkAsFailedPress(id); + setIsMarkAsFailedModalOpen(false); + }, [id, onMarkAsFailedPress]); + + const handleMarkAsFailedModalClose = useCallback(() => { + setIsMarkAsFailedModalOpen(false); + }, []); + + return ( + <TableRow> + <HistoryEventTypeCell eventType={eventType} data={data} /> + + <TableRowCell>{sourceTitle}</TableRowCell> + + <TableRowCell> + <EpisodeLanguages languages={languages} /> + </TableRowCell> + + <TableRowCell> + <EpisodeQuality + quality={quality} + isCutoffNotMet={qualityCutoffNotMet} + /> + </TableRowCell> + + <TableRowCell> + <EpisodeFormats formats={customFormats} /> + </TableRowCell> + + <TableRowCell> + {formatCustomFormatScore(customFormatScore, customFormats.length)} + </TableRowCell> + + <RelativeDateCell date={date} includeSeconds={true} includeTime={true} /> + + <TableRowCell className={styles.actions}> + <Popover + anchor={<Icon name={icons.INFO} />} + title={getTitle(eventType)} + body={ + <HistoryDetails + eventType={eventType} + sourceTitle={sourceTitle} + data={data} + downloadId={downloadId} + /> + } + position={tooltipPositions.LEFT} + /> + + {eventType === 'grabbed' && ( + <IconButton + title={translate('MarkAsFailed')} + name={icons.REMOVE} + size={14} + onPress={handleMarkAsFailedPress} + /> + )} + </TableRowCell> + + <ConfirmModal + isOpen={isMarkAsFailedModalOpen} + kind={kinds.DANGER} + title={translate('MarkAsFailed')} + message={translate('MarkAsFailedConfirmation', { sourceTitle })} + confirmLabel={translate('MarkAsFailed')} + onConfirm={handleConfirmMarkAsFailed} + onCancel={handleMarkAsFailedModalClose} + /> + </TableRow> + ); +} + +export default EpisodeHistoryRow; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index 74473b5ed..1e0143b40 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -74,9 +74,6 @@ interface SelectEpisodeModalContentProps { onModalClose(): unknown; } -// -// Render - function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { const { selectedIds, From 1374240321f08d1400faf95e84217e4b7a2d116b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 7 Dec 2024 20:45:39 -0800 Subject: [PATCH 692/762] Fixed: Converting TimeSpan from database Closes #7461 Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com> --- .../Converters/TimeSpanConverterFixture.cs | 43 +++++++++++++++++++ .../Datastore/Converters/TimeSpanConverter.cs | 18 ++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 ++ 3 files changed, 64 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs new file mode 100644 index 000000000..79d0adaee --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs @@ -0,0 +1,43 @@ +using System; +using System.Data.SQLite; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Converters; + +[TestFixture] +public class TimeSpanConverterFixture : CoreTest<TimeSpanConverter> +{ + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + + [Test] + public void should_return_string_when_saving_timespan_to_db() + { + var span = TimeSpan.FromMilliseconds(10); + + Subject.SetValue(_param, span); + _param.Value.Should().Be(span.ToString()); + } + + [Test] + public void should_return_timespan_when_getting_string_from_db() + { + var span = TimeSpan.FromMilliseconds(10); + + Subject.Parse(span.ToString()).Should().Be(span); + } + + [Test] + public void should_return_zero_timespan_for_db_null_value_when_getting_from_db() + { + Subject.Parse(null).Should().Be(TimeSpan.Zero); + } +} diff --git a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs new file mode 100644 index 000000000..fdcb227c6 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Data; +using Dapper; + +namespace NzbDrone.Core.Datastore.Converters; + +public class TimeSpanConverter : SqlMapper.TypeHandler<TimeSpan> +{ + public override void SetValue(IDbDataParameter parameter, TimeSpan value) + { + parameter.Value = value.ToString(); + } + + public override TimeSpan Parse(object value) + { + return value is string str ? TimeSpan.Parse(str) : TimeSpan.Zero; + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index b97a66eb8..0704099d7 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -201,6 +201,9 @@ namespace NzbDrone.Core.Datastore SqlMapper.RemoveTypeMap(typeof(Guid)); SqlMapper.RemoveTypeMap(typeof(Guid?)); SqlMapper.AddTypeHandler(new GuidConverter()); + SqlMapper.RemoveTypeMap(typeof(TimeSpan)); + SqlMapper.RemoveTypeMap(typeof(TimeSpan?)); + SqlMapper.AddTypeHandler(new TimeSpanConverter()); SqlMapper.AddTypeHandler(new CommandConverter()); SqlMapper.AddTypeHandler(new SystemVersionConverter()); } From 36633b5d08c19158f185c0fa5faabbaec607fcb5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Mon, 9 Dec 2024 04:37:51 +0100 Subject: [PATCH 693/762] New: Optionally as Instance Name to Telegram notifications Closes #7391 --- src/NzbDrone.Core/Localization/Core/en.json | 10 ++++++---- .../Notifications/Telegram/Telegram.cs | 17 ++++++++++++++++- .../Notifications/Telegram/TelegramProxy.cs | 10 ++++++++-- .../Notifications/Telegram/TelegramSettings.cs | 5 ++++- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f2cc23a11..a01ae4846 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1162,9 +1162,9 @@ "Menu": "Menu", "Message": "Message", "Metadata": "Metadata", - "MetadataLoadError": "Unable to load Metadata", "MetadataKometaDeprecated": "Kometa files will no longer be created, support will be removed completely in v5", "MetadataKometaDeprecatedSetting": "Deprecated", + "MetadataLoadError": "Unable to load Metadata", "MetadataPlexSettingsEpisodeMappings": "Episode Mappings", "MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file", "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File", @@ -1437,10 +1437,10 @@ "NotificationsSettingsUpdateMapPathsTo": "Map Paths To", "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')", "NotificationsSettingsUseSslHelpText": "Connect to {serviceName} over HTTPS instead of HTTP", + "NotificationsSettingsWebhookHeaders": "Headers", "NotificationsSettingsWebhookMethod": "Method", "NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice", "NotificationsSettingsWebhookUrl": "Webhook URL", - "NotificationsSettingsWebhookHeaders": "Headers", "NotificationsSignalSettingsGroupIdPhoneNumber": "Group ID / Phone Number", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Group ID / Phone Number of the receiver", "NotificationsSignalSettingsPasswordHelpText": "Password used to authenticate requests toward signal-api", @@ -1466,6 +1466,8 @@ "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", + "NotificationsTelegramSettingsIncludeInstanceName": "Include Instance Name in Title", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Optionally include Instance name in notification", "NotificationsTelegramSettingsMetadataLinks": "Metadata Links", "NotificationsTelegramSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications", "NotificationsTelegramSettingsSendSilently": "Send Silently", @@ -2083,14 +2085,14 @@ "UpdateFiltered": "Update Filtered", "UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script", "UpdateMonitoring": "Update Monitoring", + "UpdatePath": "Update Path", "UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process", "UpdateSelected": "Update Selected", + "UpdateSeriesPath": "Update Series Path", "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is not writable by the user '{userName}'.", "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.", "UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.", "UpdaterLogFiles": "Updater Log Files", - "UpdatePath": "Update Path", - "UpdateSeriesPath": "Update Series Path", "Updates": "Updates", "UpgradeUntil": "Upgrade Until", "UpgradeUntilCustomFormatScore": "Upgrade Until Custom Format Score", diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 91b37000b..b06328a36 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Telegram @@ -8,18 +9,23 @@ namespace NzbDrone.Core.Notifications.Telegram public class Telegram : NotificationBase<TelegramSettings> { private readonly ITelegramProxy _proxy; + private readonly IConfigFileProvider _configFileProvider; - public Telegram(ITelegramProxy proxy) + public Telegram(ITelegramProxy proxy, IConfigFileProvider configFileProvider) { _proxy = proxy; + _configFileProvider = configFileProvider; } public override string Name => "Telegram"; public override string Link => "https://telegram.org/"; + private string InstanceName => _configFileProvider.InstanceName; + public override void OnGrab(GrabMessage grabMessage) { var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(grabMessage.Series); _proxy.SendNotification(title, grabMessage.Message, links, Settings); @@ -28,6 +34,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnDownload(DownloadMessage message) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(message.Series); _proxy.SendNotification(title, message.Message, links, Settings); @@ -36,6 +43,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnImportComplete(ImportCompleteMessage message) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(message.Series); _proxy.SendNotification(title, message.Message, links, Settings); @@ -44,6 +52,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(deleteMessage.Series); _proxy.SendNotification(title, deleteMessage.Message, links, Settings); @@ -52,6 +61,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnSeriesAdd(SeriesAddMessage message) { var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(message.Series); _proxy.SendNotification(title, message.Message, links, Settings); @@ -60,6 +70,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(deleteMessage.Series); _proxy.SendNotification(title, deleteMessage.Message, links, Settings); @@ -68,6 +79,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; _proxy.SendNotification(title, healthCheck.Message, new List<TelegramLink>(), Settings); } @@ -75,6 +87,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", new List<TelegramLink>(), Settings); } @@ -82,6 +95,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; _proxy.SendNotification(title, updateMessage.Message, new List<TelegramLink>(), Settings); } @@ -89,6 +103,7 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE; + title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title; var links = GetLinks(message.Series); _proxy.SendNotification(title, message.Message, links, Settings); diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs index 48f70761e..bd657e331 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs @@ -8,6 +8,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; namespace NzbDrone.Core.Notifications.Telegram @@ -23,12 +24,14 @@ namespace NzbDrone.Core.Notifications.Telegram private const string URL = "https://api.telegram.org"; private readonly IHttpClient _httpClient; + private readonly IConfigFileProvider _configFileProvider; private readonly ILocalizationService _localizationService; private readonly Logger _logger; - public TelegramProxy(IHttpClient httpClient, ILocalizationService localizationService, Logger logger) + public TelegramProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, ILocalizationService localizationService, Logger logger) { _httpClient = httpClient; + _configFileProvider = configFileProvider; _localizationService = localizationService; _logger = logger; } @@ -70,7 +73,10 @@ namespace NzbDrone.Core.Notifications.Telegram new TelegramLink("Sonarr.tv", "https://sonarr.tv") }; - SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, links, settings); + var testMessageTitle = settings.IncludeAppNameInTitle ? brandedTitle : title; + testMessageTitle = settings.IncludeInstanceNameInTitle ? $"{testMessageTitle} - {_configFileProvider.InstanceName}" : testMessageTitle; + + SendNotification(testMessageTitle, body, links, settings); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index ac39a1b45..91bf68cfc 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -51,7 +51,10 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] public bool IncludeAppNameInTitle { get; set; } - [FieldDefinition(5, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")] + [FieldDefinition(5, Label = "NotificationsTelegramSettingsIncludeInstanceName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeInstanceNameHelpText", Advanced = true)] + public bool IncludeInstanceNameInTitle { get; set; } + + [FieldDefinition(6, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")] public IEnumerable<int> MetadataLinks { get; set; } public override NzbDroneValidationResult Validate() From e70aef96906470c9b591eb6d92a4b7d8c91a8fee Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 14 Dec 2024 00:32:42 +0000 Subject: [PATCH 694/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Tomer Horowitz <tomerh2001@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: hhjuhl <hans@kopula.dk> Co-authored-by: kaisernet <afimark7@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/he/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 70 +++++++++++- src/NzbDrone.Core/Localization/Core/es.json | 7 +- src/NzbDrone.Core/Localization/Core/he.json | 8 +- .../Localization/Core/pt_BR.json | 4 +- src/NzbDrone.Core/Localization/Core/tr.json | 102 +++++++++--------- 5 files changed, 131 insertions(+), 60 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 1824ec487..73780fc52 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -2,13 +2,13 @@ "Absolute": "Absolut", "AbsoluteEpisodeNumber": "Absolut Episode-nummer", "AddConditionError": "Kan ikke tilføje en ny betingelse, prøv igen.", - "AddAutoTagError": "Kan ikke tilføje en ny liste, prøv igen.", + "AddAutoTagError": "Kan ikke tilføje en ny automatisk etiket. Prøv igen.", "AddConnection": "Tilføj forbindelse", "AddCustomFormat": "Tilføj tilpasset format", "AddCustomFormatError": "Kunne ikke tilføje et nyt tilpasset format, prøv igen.", "AddDelayProfile": "Tilføj forsinkelsesprofil", "AddCondition": "Tilføj betingelse", - "AddAutoTag": "Tilføj automatisk Tag", + "AddAutoTag": "Tilføj automatisk etiket", "AbsoluteEpisodeNumbers": "Absolutte Episode-numre", "Add": "Tilføj", "Activity": "Aktivitet", @@ -37,11 +37,71 @@ "AddIndexer": "Tilføj indekser", "AddDownloadClient": "Tilføj downloadklient", "AddImportListExclusion": "Tilføj ekslusion til importeringslisten", - "AddDelayProfileError": "Kan ikke tilføje en ny forsinkelsesprofil. Prøv venligst igen.", + "AddDelayProfileError": "Kan ikke tilføje en ny forsinkelsesprofil. Prøv igen.", "AddDownloadClientError": "Ikke muligt at tilføje en ny downloadklient. Prøv venligst igen.", "AddListError": "Kan ikke tilføje en ny liste, prøv igen.", "AddListExclusion": "Tilføj ekskludering af liste", - "AddIndexerImplementation": "Tilføj betingelse - {implementationName}", + "AddIndexerImplementation": "Tilføj indeksør - {implementationName}", "AddImportListExclusionError": "Kunne ikke tilføje en ny listeekskludering. Prøv igen.", - "AddList": "Tilføj Liste" + "AddList": "Tilføj Liste", + "UpdateMechanismHelpText": "Brug {appName}s indbyggede opdateringsfunktion eller et script", + "DeleteAutoTagHelpText": "Er du sikker på, at du vil slette den automatiske etiket »{name}«?", + "DeleteImportListMessageText": "Er du sikker på, at du vil slette listen »{name}«?", + "MappedNetworkDrivesWindowsService": "Tilsluttede netværksdrev er ikke tilgængelige, når programmet kører som en Windows-tjeneste. Se FAQ'en ({url}) for mere information.", + "GrabId": "Hent ID", + "GrabRelease": "Hent udgivelse", + "ICalLink": "iCal-link", + "DeleteIndexerMessageText": "Er du sikker på, at du vil slette indeksøren »{name}«?", + "Socks5": "Socks5 (Understøtter TOR)", + "ConditionUsingRegularExpressions": "Denne betingelse stemmer overens ved brug af regulære udtryk. Bemærk, at tegnene »\\^$.|?*+()[{« har en særlig betydning og skal indledes med indkodningstegnet »\\«", + "DeleteConditionMessageText": "Er du sikker på, at du vil slette betingelsen »{name}«?", + "ImportListsSettingsSummary": "Importér fra en anden {appName}-instans eller fra Trakt-lister og håndter listeekskluderinger", + "PrioritySettings": "Prioritet: {priority}", + "RemoveSelectedItemQueueMessageText": "Er du sikker på, at du vil fjerne 1 element fra køen?", + "UsenetDelayTime": "Usenet-forsinkelse: {usenetDelay}", + "WouldYouLikeToRestoreBackup": "Vil du gendanne sikkerhedskopien »{name}«?", + "StartupDirectory": "Startmappe", + "DeleteTagMessageText": "Er du sikker på, at du vil slette etiketten »{label}«?", + "RemoveFromDownloadClient": "Fjern fra downloadklient", + "AddToDownloadQueue": "Føj til downloadkø", + "AddedToDownloadQueue": "Føjet til downloadkø", + "DelayingDownloadUntil": "Forsinker download indtil {date} kl. {time}", + "DeleteBackupMessageText": "Er du sikker på, at du vil slette sikkerhedskopien »{name}«?", + "RemoveQueueItemConfirmation": "Er du sikker på, at du vil fjerne »{sourceTitle}« fra køen?", + "ConnectionLost": "Forbindelse mistet", + "ConnectSettingsSummary": "Notifikationer, forbindelser til medieservere/-afspillere og brugerdefinerede scripts", + "CustomFormatScore": "Brugerdefineret formats resultat", + "AppDataLocationHealthCheckMessage": "Opdatering vil ikke være muligt for at undgå at slette AppData under opdatering", + "CertificateValidationHelpText": "Skift, hvor streng HTTPS-certificering er. Ændr kun dette hvis du forstå risiciene.", + "ApiKeyValidationHealthCheckMessage": "Opdater din API-nøgle til at være på mindste {length} karakterer. Dette kan gøres i indstillingerne eller i konfigurationsfilen", + "ConnectionLostReconnect": "{appName} vil prøve at tilslutte automatisk. Ellers du kan klikke genindlæs forneden.", + "CouldNotFindResults": "Kunne ikke finde nogen resultater for »{term}«", + "Discord": "Discord", + "CustomFormatUnknownCondition": "Ukendt betingelse for tilpasset format »{implementation}«", + "CustomFormatUnknownConditionOption": "Ukendt valgmulighed »{key}« for betingelsen »{implementation}«", + "DeleteDownloadClientMessageText": "Er du sikker på, at du vil fjerne downloadklienten »{name}«?", + "TheLogLevelDefault": "Logniveauet er som standard 'Info' og kan ændres under [Generelle indstillinger](/settings/general)", + "DeleteNotificationMessageText": "Er du sikker på, at du vil slette notifikationen »{name}«?", + "Interval": "Interval", + "CalendarOptions": "Kalenderindstillinger", + "TorrentDelayTime": "Torrentforsinkelse: {torrentDelay}", + "Usenet": "Usenet", + "AutoTaggingNegateHelpText": "Hvis dette er markeret, vil reglen for automatisk etiket ikke blive anvendt, hvis denne {implementationName}-betingelse stemmer overens.", + "DeleteSpecificationHelpText": "Er du sikker på, at du vil slette specifikationen »{name}«?", + "TagDetails": "Etiketdetaljer - {label}", + "DeleteDelayProfile": "Slet forsinkelsesprofil", + "RemotePathMappings": "Sammenkædning med fjernsti", + "CutoffUnmet": "Grænse ikke opnået", + "DeleteReleaseProfile": "Slet udgivelsesprofil", + "Docker": "Docker", + "DownloadWarning": "Downloadadvarsel: »{warningMessage}«", + "AppDataDirectory": "AppData-mappe", + "ColonReplacement": "Udskiftning af kolon", + "IndexerPriorityHelpText": "Indeksatorprioritet fra 1 (højest) til 50 (lavest). Standard: 25. Anvendes til at vælge mellem udgivelser med ellers lige mange point. {appName} vil stadig bruge alle aktiverede indeksatorer til RSS-synkronisering og søgning", + "RetryingDownloadOn": "Prøver igen at downloade d. {date} kl. {time}", + "DeleteQualityProfileMessageText": "Er du sikker på, at du vil slette kvalitetsprofilen »{name}«?", + "DeleteReleaseProfileMessageText": "Er du sikker på, at du vil slette udgivelsesprofilen »{name}«?", + "MinutesSixty": "60 minutter: {sixty}", + "NegateHelpText": "Hvis dette er markeret, gælder det tilpassede format ikke, hvis denne {implementationName}-betingelse stemmer overens.", + "RemoveSelectedItemsQueueMessageText": "Er du sikker på, at du vil fjerne {selectedCount} elementer fra køen?" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index a629993ff..c844b3595 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2140,6 +2140,9 @@ "UpdatePath": "Actualizar Ruta", "MetadataKometaDeprecatedSetting": "Obsoleto", "MetadataKometaDeprecated": "Los archivos de Kometa no seguirán siendo creados, se eliminará completamente el soporte en la v5", - "IndexerSettingsFailDownloadsHelpText": "Mientras se procesan las descargas completadas, {appName} tratará los errores seleccionados evitando la importación como descargas fallidas.", - "IndexerSettingsFailDownloads": "Fallo de Descargas" + "IndexerSettingsFailDownloadsHelpText": "Mientras se procesan las descargas completadas, {appName} tratará esos tipos de archivo seleccionados como descargas fallidas.", + "IndexerSettingsFailDownloads": "Fallo de Descargas", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcionalmente incluye el nombre de la instancia en la notificación", + "NotificationsTelegramSettingsIncludeInstanceName": "Incluir el nombre de la instancia en el título", + "Fallback": "Retirada" } diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 5cbee5d60..6ac2c5b3d 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -4,7 +4,11 @@ "Add": "הוסף", "Activity": "פעילות", "Indexer": "אינדקסר", - "AddAutoTag": "הוסף טגית אוטומטית", + "AddAutoTag": "הוסף תג אוטומטית", "About": "אודות", - "Actions": "פעולות" + "Actions": "פעולות", + "AddConditionError": "לא ניתן להוסיף תנאי חדש, נסה שנית.", + "AddAutoTagError": "לא ניתן להוסיף תג חדש, נסה שנית.", + "AddCondition": "הוסף תנאי", + "AddANewPath": "הוסף נתיב חדש" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index d090b4e41..5dc4067f8 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2142,5 +2142,7 @@ "MetadataKometaDeprecated": "Os arquivos Kometa não serão mais criados, o suporte será completamente removido na v5", "MetadataKometaDeprecatedSetting": "Deprecado", "IndexerSettingsFailDownloads": "Downloads com Falhas", - "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará os erros selecionados, impedindo a importação, como downloads com falha." + "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará esses tipos de arquivos selecionados como downloads com falha.", + "NotificationsTelegramSettingsIncludeInstanceName": "Incluir nome da instância no título", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcionalmente, inclua o nome da instância na notificação" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 75ebbb2e3..b7be87d2a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -20,12 +20,12 @@ "AddNewRestriction": "Yeni kısıtlama ekle", "AddedDate": "Eklendi: {date}", "Activity": "Etkinlik", - "Added": "Eklendi", + "Added": "Eklenme", "AirDate": "Yayınlanma Tarihi", "Add": "Ekle", "AddingTag": "Etiket ekleniyor", "Age": "Yıl", - "AgeWhenGrabbed": "Yıl (yakalandığında)", + "AgeWhenGrabbed": "Yıl (alındığında)", "AddDelayProfileError": "Yeni bir gecikme profili eklenemiyor, lütfen tekrar deneyin.", "AddImportList": "İçe Aktarım Listesi Ekle", "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", @@ -218,7 +218,7 @@ "DownloadClientDelugeSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", "ApiKey": "API Anahtarı", - "Analytics": "Analitik", + "Analytics": "Analiz", "All": "Hepsi", "AppDataLocationHealthCheckMessage": "Güncelleme sırasında AppData'nın silinmesini önlemek için güncelleme yapılmayacaktır", "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.", @@ -232,7 +232,7 @@ "Apply": "Uygula", "DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}", "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')", - "DownloadClientFreeboxSettingsHostHelpText": "Freebox'un ana bilgisayar adı veya ana bilgisayar IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağdaysa çalışır)", + "DownloadClientFreeboxSettingsHostHelpText": "Freebox'un istemci adı veya istemci IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağda çalışır)", "DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox API temel URL'sini API sürümüyle tanımlayın, örneğin '{url}', varsayılan olarak '{defaultApiUrl}' olur", "BlocklistReleases": "Kara Liste Sürümü", "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet ayarı KeepHistory 0 olarak ayarlandı. Bu, {appName}'in tamamlanan indirmeleri görmesini engelliyor.", @@ -349,7 +349,7 @@ "FormatShortTimeSpanHours": "{hours} saat", "FormatRuntimeMinutes": "{minutes}dk", "FullColorEventsHelpText": "Etkinliğin tamamını yalnızca sol kenar yerine durum rengiyle renklendirecek şekilde stil değiştirildi. Gündem için geçerli değildir", - "GrabId": "ID'den Yakala", + "GrabId": "ID'den Al", "ImportUsingScriptHelpText": "Bir komut dosyası kullanarak içe aktarmak için dosyaları kopyalayın (ör. kod dönüştürme için)", "InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için", "ManageDownloadClients": "İndirme İstemcilerini Yönet", @@ -358,7 +358,7 @@ "NotificationsAppriseSettingsTags": "Apprise Etiketler", "NotificationsCustomScriptSettingsArgumentsHelpText": "Komut dosyasına aktarılacak argümanlar", "NotificationsCustomScriptValidationFileDoesNotExist": "Dosya bulunmuyor", - "NotificationsDiscordSettingsOnGrabFields": "Yakalamalarda", + "NotificationsDiscordSettingsOnGrabFields": "Alımlarda", "NotificationsDiscordSettingsAvatarHelpText": "Bu entegrasyondaki mesajlar için kullanılan avatarı değiştirin", "NotificationsDiscordSettingsOnImportFieldsHelpText": "'İçe aktarmalarda' bildirimi için iletilen alanları değiştirin", "NotificationsDiscordSettingsOnImportFields": "İçe Aktarmalarda", @@ -386,7 +386,7 @@ "Unmonitored": "Takip Edilmiyor", "FormatAgeHour": "saat", "FormatAgeHours": "saat", - "NoHistory": "Geçmiş yok", + "NoHistory": "Geçmiş bulunamadı", "FailedToFetchUpdates": "Güncellemeler getirilemedi", "InstanceName": "Örnek isim", "MoveAutomatically": "Otomatik Olarak Taşı", @@ -408,7 +408,7 @@ "LanguagesLoadError": "Diller yüklenemiyor", "ListWillRefreshEveryInterval": "Liste yenileme periyodu {refreshInterval}dır", "ManageIndexers": "Dizinleyicileri Yönet", - "ManualGrab": "Manuel Yakalama", + "ManualGrab": "Manuel Alımlarda", "DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri", "DownloadClients": "İndirme İstemcileri", "InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı", @@ -419,7 +419,7 @@ "NotificationsAppriseSettingsStatelessUrls": "Apprise Durum bilgisi olmayan URL'ler", "NotificationsCustomScriptSettingsProviderMessage": "Test, betiği EventType {eventTypeTest} olarak ayarlıyken yürütür; betiğinizin bunu doğru şekilde işlediğinden emin olun", "NotificationsDiscordSettingsAuthorHelpText": "Bu bildirim için gösterilen yerleştirme yazarını geçersiz kılın. Boş örnek adıdır", - "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Bu 'yakalandı' bildirimi için iletilen alanları değiştirin", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Bu 'alındı' bildirimi için iletilen alanları değiştirin", "NotificationsDiscordSettingsUsernameHelpText": "Gönderimin yapılacağı kullanıcı adı varsayılan olarak Discord webhook varsayılanıdır", "NotificationsKodiSettingsGuiNotification": "GUI Bildirimi", "NotificationsJoinValidationInvalidDeviceId": "Cihaz kimlikleri geçersiz görünüyor.", @@ -431,7 +431,7 @@ "FullColorEvents": "Tam Renkli Etkinlikler", "ListRootFolderHelpText": "Kök Klasör listesi öğeleri eklenecek", "HourShorthand": "s", - "LogFilesLocation": "Günlük dosyaları şu konumda bulunur: {location}", + "LogFilesLocation": "Log kayıtlarının bulunduğu konum: {location}", "ImportUsingScript": "Komut Dosyası Kullanarak İçe Aktar", "IncludeHealthWarnings": "Sağlık Uyarılarını Dahil Et", "IndexerSettingsMultiLanguageRelease": "Çoklu Dil", @@ -484,9 +484,9 @@ "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", "Test": "Test Et", - "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", - "IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent yakalandıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", + "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [log kayıtlarınızı]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Alırken Engellenen Torrent Karmalarını Reddet", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent alındıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", "NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı", "NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü", "NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si", @@ -504,7 +504,7 @@ "NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?", "NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın", "InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", - "IndexerDownloadClientHelpText": "Bu dizinleyiciden yakalamak için hangi indirme istemcisinin kullanılacağını belirtin", + "IndexerDownloadClientHelpText": "Bu dizinleyiciden almak için hangi indirme istemcisinin kullanılacağını belirtin", "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", "LastDuration": "Yürütme Süresi", "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", @@ -560,7 +560,7 @@ "SetIndexerFlagsModalTitle": "{modalTitle} - Dizinleyici Bayraklarını Ayarla", "SslCertPassword": "SSL Sertifika Parolası", "Rating": "Puan", - "GrabRelease": "Yayın Yakalama", + "GrabRelease": "Yayın Alma", "NotificationsNtfyValidationAuthorizationRequired": "Yetkilendirme gerekli", "NotificationsPushBulletSettingSenderId": "Gönderen ID", "NotificationsPushBulletSettingsChannelTags": "Kanal Etiketleri", @@ -604,7 +604,7 @@ "SslCertPath": "SSL Sertifika Yolu", "StopSelecting": "Düzenlemeden Çık", "TableOptionsButton": "Tablo Seçenekleri Butonu", - "TheLogLevelDefault": "Günlük düzeyi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/settings/general) bölümünden değiştirilebilir", + "TheLogLevelDefault": "Log seviyesi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/ayarlar/genel) bölümünden değiştirilebilir", "NotificationsTwitterSettingsAccessToken": "Erişim Jetonu", "AutoRedownloadFailedHelpText": "Otomatik olarak farklı bir Yayın arayın ve indirmeye çalışın", "Queue": "Kuyruk", @@ -614,7 +614,7 @@ "ChmodFolderHelpText": "Sekizli, medya klasörlerine ve dosyalara içe aktarma / yeniden adlandırma sırasında uygulanır (yürütme bitleri olmadan)", "DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?", "Or": "veya", - "OverrideGrabModalTitle": "Geçersiz Kıl ve Yakala - {title}", + "OverrideGrabModalTitle": "Geçersiz Kıl ve Al - {title}", "PreferProtocol": "{preferredProtocol}'u tercih edin", "PreferredProtocol": "Tercih Edilen Protokol", "PublishedDate": "Yayınlanma Tarihi", @@ -659,13 +659,13 @@ "ResetDefinitions": "Tanımları Sıfırla", "RestartRequiredToApplyChanges": "{appName}, değişikliklerin uygulanabilmesi için yeniden başlatmayı gerektiriyor. Şimdi yeniden başlatmak istiyor musunuz?", "RetryingDownloadOn": "{date} tarihinde, {time} itibarıyla indirme işlemi yeniden deneniyor", - "RssSyncIntervalHelpText": "Dakika cinsinden aralık. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın yakalamayı durduracaktır)", + "RssSyncIntervalHelpText": "Dakika cinsinden aralık. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın almayı durduracaktır)", "TorrentBlackhole": "Blackhole Torrent", "PrioritySettings": "Öncelik: {priority}", "SubtitleLanguages": "Altyazı Dilleri", "OrganizeNothingToRename": "Başarılı! İşim bitti, yeniden adlandırılacak dosya yok.", "MinimumCustomFormatScore": "Minimum Özel Format Puanı", - "MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin yakalanmadan önceki minimum geçen süre (dakika cinsinden). Bunu, yeni sürümlerin usenet sağlayıcınıza yayılması için zaman vermek amacıyla kullanın.", + "MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin almadan önceki minimum geçen süre (dakika cinsinden). Bunu, yeni sürümlerin usenet sağlayıcınıza yayılması için zaman vermek amacıyla kullanın.", "QueueLoadError": "Kuyruk yüklenemedi", "SelectDropdown": "Seçimler...", "NotificationsSettingsUpdateMapPathsTo": "Harita Yolları", @@ -701,7 +701,7 @@ "OnHealthRestored": "Sağlığın İyileştirilmesi Hakkında", "OverrideGrabNoLanguage": "En az bir dil seçilmelidir", "ParseModalUnableToParse": "Sağlanan başlık ayrıştırılamadı, lütfen tekrar deneyin.", - "PreviouslyInstalled": "Önceden Yüklenmiş", + "PreviouslyInstalled": "Daha Önce Kurulmuş", "QualityCutoffNotMet": "Kalite sınırı karşılanmadı", "QueueIsEmpty": "Kuyruk boş", "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizinleyicinin ayarlanması, bu profilin yalnızca söz konusu dizinleyicinin yayınlarına uygulanmasına neden olur.", @@ -767,7 +767,7 @@ "UpdateFiltered": "Filtrelenenleri Güncelle", "UsenetBlackholeNzbFolder": "Nzb Klasörü", "UsenetDelayHelpText": "Usenet'ten bir yayın almadan önce beklemek için dakika cinsinden gecikme", - "UpdaterLogFiles": "Güncelleme Günlük Dosyaları", + "UpdaterLogFiles": "Log Kayıt Güncelleyici", "Usenet": "Usenet", "Filters": "Filtreler", "ImportListsSettingsSummary": "Başka bir {appName} örneğinden veya Trakt listelerinden içe aktarın ve liste hariç tutma işlemlerini yönetin", @@ -803,8 +803,8 @@ "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor", "ImportListExclusions": "İçe Aktarma Listesinden Hariç Bırakılan(lar)", "UiLanguage": "Arayüz Dili", - "IndexerSettingsSeedTimeHelpText": "Bir torrentin durmadan önce seed edilmesi gereken süre. Boş bırakılırsa indirme istemcisinin varsayılan ayarını kullanır", - "IndexerSettingsSeedRatioHelpText": "Bir torrentin durmadan önce ulaşması gereken oran. Boş bırakılırsa indirme istemcisinin varsayılan değerini kullanır. Oran en az 1,0 olmalı ve indeksleyici kurallarına uygun olmalıdır", + "IndexerSettingsSeedTimeHelpText": "Bir torrentin durdurulmadan önce ulaşması gereken oran, boş bırakıldığında uygulamanın varsayılanı kullanılır", + "IndexerSettingsSeedRatioHelpText": "Bir torrentin durdurulmadan önce ulaşması gereken oran. Boş bırakılırsa indirme istemcisinin varsayılan değerini kullanır. Oran en az 1,0 olmalı ve indeksleyici kurallarına uygun olmalıdır", "EditImportListExclusion": "Hariç Tutulanlar Listesini Düzenle", "UiLanguageHelpText": "{appName}'ın arayüz için kullanacağı dil", "IndexerSettingsSeedTime": "Seed Süresi", @@ -828,9 +828,9 @@ "ShowMonitored": "Takip Edilenleri Göster", "ShowMonitoredHelpText": "Posterin altında takip durumu göster", "Ui": "Arayüz", - "Mechanism": "İşleyiş", + "Mechanism": "Teknik", "UiSettings": "Arayüz Ayarları", - "Script": "Hazır Metin", + "Script": "Komut Dosyası", "UiSettingsLoadError": "Arayüz ayarları yüklenemiyor", "UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz", "Wanted": "Arananlar", @@ -970,7 +970,7 @@ "DeleteImportListExclusion": "İçe Aktarma Listesi Hariç Tutmasını Sil", "DeleteIndexer": "Dizinleyiciyi Sil", "Docker": "Docker", - "DockerUpdater": "güncellemeyi almak için docker konteynerini güncelleyin", + "DockerUpdater": "Güncellemeyi almak için docker konteynerini güncelleyin", "DeleteSelectedSeries": "Seçili Serileri Sil", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "14 günden uzun süre önce yayınlanan bölümleri almaya öncelik verin", "DownloadClientStatusAllClientHealthCheckMessage": "Tüm indirme istemcileri hatalar nedeniyle kullanılamıyor", @@ -993,7 +993,7 @@ "Negated": "Reddedildi", "FileManagement": "Dosya Yönetimi", "FirstDayOfWeek": "Haftanın ilk günü", - "Fixed": "Sabit", + "Fixed": "Düzeltilen", "InstallLatest": "En Sonu Yükle", "RemotePathMappings": "Uzak Yol Eşlemeleri", "Retention": "Saklama", @@ -1040,7 +1040,7 @@ "NotificationsGotifySettingsMetadataLinks": "Meta Veri Bağlantıları", "NotificationsPlexSettingsServer": "Sunucu", "Path": "Yol", - "Peers": "Akranlar", + "Peers": "Eşler", "Pending": "Bekliyor", "PendingChangesDiscardChanges": "Değişiklikleri at ve ayrıl", "PendingChangesStayReview": "Kalın ve değişiklikleri inceleyin", @@ -1098,7 +1098,7 @@ "CalendarLoadError": "Takvim yüklenemiyor", "SetPermissionsLinuxHelpTextWarning": "Bu ayarların ne yaptığından emin değilseniz, değiştirmeyin.", "Clear": "Temizle", - "CurrentlyInstalled": "Şu anda Yüklü", + "CurrentlyInstalled": "Şuan Kurulu", "Custom": "Özel", "CustomFormatScore": "Özel Format Puanı", "DeleteEpisodeFromDisk": "Bölümü diskten sil", @@ -1137,7 +1137,7 @@ "RegularExpressionsCanBeTested": "Düzenli ifadeler [burada]({url}) test edilebilir.", "RemoveFilter": "Filtreyi kaldır", "StartImport": "İçe Aktarmayı Başlat", - "TableOptions": "Masa Seçenekleri", + "TableOptions": "Tablo Seçenekleri", "UpdateAll": "Tümünü Güncelle", "Paused": "Duraklatıldı", "RescanAfterRefreshHelpTextWarning": "{appName}, 'Her Zaman' olarak ayarlanmadığında dosyalardaki değişiklikleri otomatik olarak algılamayacaktır", @@ -1272,7 +1272,7 @@ "DestinationRelativePath": "Hedef Göreli Yol", "DiskSpace": "Disk Alanı", "DoNotUpgradeAutomatically": "Otomatik Olarak Yükseltme", - "Donations": "Bağışlar", + "Donations": "Bağış", "Download": "İndir", "DownloadClientUnavailable": "İndirme istemcisi kullanılamıyor", "DownloadPropersAndRepacksHelpText": "Propers / Repacks'e otomatik olarak yükseltme yapılıp yapılmayacağı", @@ -1305,10 +1305,10 @@ "ExtraFileExtensionsHelpTextsExamples": "Örnekler: \".sub, .nfo\" veya \"sub, nfo\"", "FeatureRequests": "Özellik talepleri", "Folder": "Klasör", - "GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analitikler ve güncellemeler", + "GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analizler ve güncellemeler", "Genres": "Türler", - "GrabSelected": "Seçilenleri Kap", - "Grabbed": "Yakalandı", + "GrabSelected": "Seçilenleri Al", + "Grabbed": "Alındı", "Group": "Grup", "HiddenClickToShow": "Gizli, göstermek için tıklayın", "ICalFeedHelpText": "Bu URL'yi müşterilerinize kopyalayın veya tarayıcınız webcal'i destekliyorsa abone olmak için tıklayın", @@ -1333,11 +1333,11 @@ "Level": "Seviye", "ListOptionsLoadError": "Liste seçenekleri yüklenemiyor", "ListTagsHelpText": "Etiketler listesi öğeleri eklenecek", - "LogFiles": "Log dosyaları", - "LogLevel": "Günlük Düzeyi", - "LogLevelTraceHelpTextWarning": "İzleme günlük kaydı yalnızca geçici olarak etkinleştirilmelidir", + "LogFiles": "Log Kayıtları", + "LogLevel": "Log Seviyesi", + "LogLevelTraceHelpTextWarning": "İzleme kaydı yalnızca geçici olarak etkinleştirilmelidir", "LogOnly": "Sadece hesap aç", - "Logs": "Günlükler", + "Logs": "Kayıtlar", "Lowercase": "Küçük Harf", "MaintenanceRelease": "Bakım Sürümü: hata düzeltmeleri ve diğer iyileştirmeler. Daha fazla ayrıntı için Github İşlem Geçmişine bakın", "MappedNetworkDrivesWindowsService": "Windows Hizmeti olarak çalıştırıldığında eşlenen ağ sürücüleri kullanılamaz, daha fazla bilgi için [SSS]({url}) bölümüne bakın.", @@ -1349,7 +1349,7 @@ "MinimumCustomFormatScoreHelpText": "İndirmeye izin verilen minimum özel format puanı", "Month": "Ay", "MoreDetails": "Daha fazla detay", - "MoreInfo": "Daha fazla bilgi", + "MoreInfo": "Daha Fazla Bilgi", "Name": "İsim", "New": "Yeni", "NextExecution": "Sonraki Yürütme", @@ -1360,7 +1360,7 @@ "NoLeaveIt": "Hayır, Bırak", "NoLimitForAnyRuntime": "Herhangi bir çalışma zamanı için sınır yok", "NoLinks": "Bağlantı Yok", - "NoLogFiles": "Günlük dosyası yok", + "NoLogFiles": "Log kayıt dosyası henüz yok", "NoMatchFound": "Eşleşme bulunamadı!", "NoMinimumForAnyRuntime": "Herhangi bir çalışma süresi için minimum değer yok", "NoResultsFound": "Sonuç bulunamadı", @@ -1370,7 +1370,7 @@ "NotificationsGotifySettingsPreferredMetadataLink": "Tercih Edilen Meta Veri Bağlantısı", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Yalnızca tek bir bağlantıyı destekleyen istemciler için meta veri bağlantısı", "NotificationsPlexSettingsServerHelpText": "Kimlik doğrulamasından sonra plex.tv hesabından sunucuyu seçin", - "OnGrab": "Yakalandığında", + "OnGrab": "Alındığında", "OnHealthIssue": "Sağlık Sorunu Hakkında", "OnLatestVersion": "{appName}'ın en son sürümü kurulu", "OnRename": "Yeniden Adlandırıldığında", @@ -1441,7 +1441,7 @@ "RestartRequiredHelpTextWarning": "Etkili olması için yeniden başlatma gerektirir", "RetentionHelpText": "Yalnızca Usenet: Sınırsız saklamaya ayarlamak için sıfıra ayarlayın", "RootFolder": "Kök Klasör", - "RootFolders": "Kök klasörler", + "RootFolders": "Kök Klasörler", "Rss": "RSS", "RssIsNotSupportedWithThisIndexer": "RSS, bu indeksleyici ile desteklenmiyor", "RssSync": "RSS Senkronizasyonu", @@ -1495,7 +1495,7 @@ "Tasks": "Görevler", "TestAll": "Tümünü Test Et", "TestAllClients": "Tüm İstemcileri Test Et", - "TestAllIndexers": "Tüm Dizinleyicileri Test Et", + "TestAllIndexers": "Dizinleyicileri Test Et", "TestAllLists": "Tüm Listeleri Test Et", "Time": "Zaman", "TimeFormat": "Zaman formatı", @@ -1550,7 +1550,7 @@ "Mode": "Mod", "Monday": "Pazartesi", "ListSyncLevelHelpText": "Kitaplıktaki filmlerin listenizde/listelerinizde görünmemesi durumunda seçiminize göre işlem yapılacaktır", - "Location": "Konum", + "Location": "Klasör Yolu", "MediaManagementSettingsSummary": "Adlandırma ve dosya yönetimi ayarları", "Medium": "Orta", "MegabytesPerMinute": "Dakika Başına Megabayt", @@ -1798,7 +1798,7 @@ "IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, dizinleyici veya dizinleyici kategori ayarlarınızdaki bir sorun olabilir.", "IndexerValidationUnableToConnectResolutionFailure": "Dizinleyiciye bağlanılamıyor bağlantı hatası. Dizinleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.", "IndexerSettingsFailDownloads": "Başarısız İndirmeler", - "IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName}, içe aktarmayı engelleyen seçili hataları başarısız indirmeler olarak ele alacaktır.", + "IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName} bu seçili dosya türlerini başarısız indirmeler olarak değerlendirecektir.", "IndexerSettingsMinimumSeeders": "Minimum Seeder", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "{indexer} uyumlu bir RSS beslemesine URL girin", @@ -1994,7 +1994,7 @@ "SceneInformation": "Sahne Bilgileri", "SceneNumberNotVerified": "Sahne numarası henüz doğrulanmadı", "SceneNumbering": "Sahne Numaralandırması", - "SearchByTvdbId": "Ayrıca bir tv gösterisinin TVDB ID'sini kullanarak da arama yapabilirsiniz. Örn. tvdb:71663", + "SearchByTvdbId": "Ayrıca bir tv dizisinin TVDB ID'sini kullanarak da arama yapabilirsiniz. Örn. tvdb:263771", "SearchFailedError": "Arama başarısız oldu, lütfen daha sonra tekrar deneyin.", "SearchForAllMissingEpisodes": "Tüm eksik bölümleri arayın", "SearchForAllMissingEpisodesConfirmationCount": "{totalRecords} eksik bölümün hepsini aramak istediğinizden emin misiniz?", @@ -2043,7 +2043,7 @@ "SeriesTitleToExcludeHelpText": "Hariç tutulacak dizinin adı", "SeriesType": "Dizi Türü", "SeriesTypes": "Dizi Türleri", - "SeriesTypesHelpText": "Seri türü yeniden adlandırma, ayrıştırma ve arama için kullanılır", + "SeriesTypesHelpText": "Dizi türü yeniden adlandırma, ayrıştırma ve arama için kullanılır", "ShowBanners": "Bannerları Göster", "ShowBannersHelpText": "Başlıklar yerine posterleri göster", "ShowEpisodeInformation": "Bölüm Bilgilerini Göster", @@ -2080,7 +2080,7 @@ "UnableToUpdateSonarrDirectly": "{appName} doğrudan güncellenemiyor,", "UnmonitorDeletedEpisodes": "Silinen Bölümlerin Takibini Bırak", "UnmonitorDeletedEpisodesHelpText": "Diskten silinen bölümler {appName} uygulamasında otomatik olarak takipten çıkarılır", - "UnmonitorSpecialsEpisodesDescription": "Diğer bölümlerin takip edilme durumunu değiştirmeden tüm özel bölümlerin takip edilmesini durdur", + "UnmonitorSpecialsEpisodesDescription": "Diğer bölümlerin takip edilme durumunu değiştirmeden tüm özel bölümlerin takip edilmesini durdurun", "UnmonitorSpecialEpisodes": "Özel Bölümleri Takip Etme", "UnmonitoredOnly": "Sadece Takip Edilmeyen", "Upcoming": "Yaklaşan", @@ -2128,7 +2128,7 @@ "NotificationsPlexValidationNoTvLibraryFound": "En az bir TV kütüphanesi gereklidir", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Docker'ı kullanıyorsunuz; {downloadClientName} indirme istemcisi, dosyaları {path} yolunda rapor ediyor, ancak bu geçerli bir {osName} yolu değil. Uzak yol eşleşmelerinizi kontrol edin ve istemci ayarlarını indirin.", "SeriesEditRootFolderHelpText": "Diziyi aynı kök klasöre taşımak, dizi klasörlerinin güncellenen başlık veya adlandırma biçimiyle eşleşecek şekilde yeniden adlandırılmasında kullanılabilir", - "MonitorNewSeasonsHelpText": "Hangi yeni sezonlar otomatik olarak takip edilmeli", + "MonitorNewSeasonsHelpText": "Yeni sezonların otomatik takip edilmesi veya edilmemesi seçeneği", "RemotePathMappingGenericPermissionsHealthCheckMessage": "İndirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak {appName} bu dizine erişemiyor. Klasörün izinlerini ayarlamanız gerekebilir.", "SeriesIsMonitored": "Dizi takip ediliyor", "MetadataSettingsSeriesSummary": "Bölümler içe aktarıldığında veya diziler yenilendiğinde meta veri dosyaları oluşturun", @@ -2142,5 +2142,7 @@ "NotificationsTelegramSettingsMetadataLinks": "Meta Veri Bağlantıları", "MatchedToSeries": "Diziye Eşleşti", "MaximumSingleEpisodeAge": "Tek Bir Bölümde Maksimum Geçen Süre", - "MaximumSingleEpisodeAgeHelpText": "Tam sezon araması sırasında, yalnızca sezonun son bölümünün bu ayardan daha eski olması durumunda sezon paketlerine izin verilecektir. Yalnızca standart diziler için. Devre dışı bırakmak için 0'ı kullanın." + "MaximumSingleEpisodeAgeHelpText": "Tam sezon araması sırasında, yalnızca sezonun son bölümünün bu ayardan daha eski olması durumunda sezon paketlerine izin verilecektir. Yalnızca standart diziler için. Devre dışı bırakmak için 0'ı kullanın.", + "NotificationsTelegramSettingsIncludeInstanceName": "Başlığa Örnek Adını Dahil Et", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "İsteğe bağlı olarak Örnek adını bildirime ekleyin" } From 024462c52dd23b6fb31e3952192b5a680eeba69b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:35:33 +0200 Subject: [PATCH 695/762] Fixed: Fetching ICS calendar with missing series --- src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs b/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs index 5851c6f5a..e79105f42 100644 --- a/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs +++ b/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs @@ -54,6 +54,11 @@ namespace Sonarr.Api.V3.Calendar { var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId); + if (series == null) + { + continue; + } + if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1)) { continue; From 148480909917f69ff3b2ca547ccb4716dd56606e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 8 Dec 2024 17:24:47 -0800 Subject: [PATCH 696/762] Upgrade TypeScript and core-js --- package.json | 4 +- pnpm-lock.yaml | 8180 ++++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 16 +- 3 files changed, 8190 insertions(+), 10 deletions(-) create mode 100644 pnpm-lock.yaml diff --git a/package.json b/package.json index e619cfeca..db747ef6e 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "redux-thunk": "2.4.2", "reselect": "4.1.8", "stacktrace-js": "2.0.2", - "typescript": "5.1.6" + "typescript": "5.7.2" }, "devDependencies": { "@babel/core": "7.25.8", @@ -108,7 +108,7 @@ "babel-loader": "9.2.1", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.38.1", + "core-js": "3.39.0", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", "eslint": "8.57.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..1126f105c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8180 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@fortawesome/fontawesome-free': + specifier: 6.6.0 + version: 6.6.0 + '@fortawesome/fontawesome-svg-core': + specifier: 6.6.0 + version: 6.6.0 + '@fortawesome/free-regular-svg-icons': + specifier: 6.6.0 + version: 6.6.0 + '@fortawesome/free-solid-svg-icons': + specifier: 6.6.0 + version: 6.6.0 + '@fortawesome/react-fontawesome': + specifier: 0.2.2 + version: 0.2.2(@fortawesome/fontawesome-svg-core@6.6.0)(react@18.3.1) + '@juggle/resize-observer': + specifier: 3.4.0 + version: 3.4.0 + '@microsoft/signalr': + specifier: 6.0.21 + version: 6.0.21 + '@sentry/browser': + specifier: 7.119.1 + version: 7.119.1 + '@tanstack/react-query': + specifier: 5.61.0 + version: 5.61.0(react@18.3.1) + '@types/node': + specifier: 20.16.11 + version: 20.16.11 + '@types/react': + specifier: 18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: 18.3.1 + version: 18.3.1 + classnames: + specifier: 2.5.1 + version: 2.5.1 + connected-react-router: + specifier: 6.9.3 + version: 6.9.3(history@4.10.1)(react-redux@7.2.4)(react-router@5.2.0)(react@18.3.1)(redux@4.2.1) + copy-to-clipboard: + specifier: 3.3.3 + version: 3.3.3 + element-class: + specifier: 0.2.2 + version: 0.2.2 + filesize: + specifier: 10.1.6 + version: 10.1.6 + fuse.js: + specifier: 6.6.2 + version: 6.6.2 + history: + specifier: 4.10.1 + version: 4.10.1 + jdu: + specifier: 1.0.0 + version: 1.0.0 + jquery: + specifier: 3.7.1 + version: 3.7.1 + lodash: + specifier: 4.17.21 + version: 4.17.21 + mobile-detect: + specifier: 1.4.5 + version: 1.4.5 + moment: + specifier: 2.30.1 + version: 2.30.1 + mousetrap: + specifier: 1.6.5 + version: 1.6.5 + normalize.css: + specifier: 8.0.1 + version: 8.0.1 + prop-types: + specifier: 15.8.1 + version: 15.8.1 + qs: + specifier: 6.13.0 + version: 6.13.0 + react: + specifier: 18.3.1 + version: 18.3.1 + react-addons-shallow-compare: + specifier: 15.6.3 + version: 15.6.3 + react-async-script: + specifier: 1.2.0 + version: 1.2.0(react@18.3.1) + react-autosuggest: + specifier: 10.1.0 + version: 10.1.0(react@18.3.1) + react-custom-scrollbars-2: + specifier: 4.5.0 + version: 4.5.0(react-dom@18.3.1)(react@18.3.1) + react-dnd: + specifier: 14.0.4 + version: 14.0.4(@types/node@20.16.11)(@types/react@18.3.12)(react@18.3.1) + react-dnd-html5-backend: + specifier: 14.0.2 + version: 14.0.2 + react-dnd-multi-backend: + specifier: 6.0.2 + version: 6.0.2(react-dnd-html5-backend@14.0.2)(react-dnd-touch-backend@14.1.1)(react-dnd@14.0.4)(react-dom@18.3.1)(react@18.3.1) + react-dnd-touch-backend: + specifier: 14.1.1 + version: 14.1.1 + react-document-title: + specifier: 2.0.3 + version: 2.0.3(react@18.3.1) + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-focus-lock: + specifier: 2.9.4 + version: 2.9.4(@types/react@18.3.12)(react@18.3.1) + react-google-recaptcha: + specifier: 2.1.0 + version: 2.1.0(react@18.3.1) + react-lazyload: + specifier: 3.2.0 + version: 3.2.0(react-dom@18.3.1)(react@18.3.1) + react-measure: + specifier: 1.4.7 + version: 1.4.7(react-dom@18.3.1)(react@18.3.1) + react-popper: + specifier: 1.3.7 + version: 1.3.7(react@18.3.1) + react-redux: + specifier: 7.2.4 + version: 7.2.4(react-dom@18.3.1)(react@18.3.1) + react-router: + specifier: 5.2.0 + version: 5.2.0(react@18.3.1) + react-router-dom: + specifier: 5.2.0 + version: 5.2.0(react@18.3.1) + react-slider: + specifier: 1.1.4 + version: 1.1.4(prop-types@15.8.1)(react@18.3.1) + react-tabs: + specifier: 4.3.0 + version: 4.3.0(react@18.3.1) + react-text-truncate: + specifier: 0.19.0 + version: 0.19.0(react-dom@18.3.1)(react@18.3.1) + react-use-measure: + specifier: 2.1.1 + version: 2.1.1(react-dom@18.3.1)(react@18.3.1) + react-virtualized: + specifier: 9.21.1 + version: 9.21.1(react-dom@18.3.1)(react@18.3.1) + react-window: + specifier: 1.8.10 + version: 1.8.10(react-dom@18.3.1)(react@18.3.1) + redux: + specifier: 4.2.1 + version: 4.2.1 + redux-actions: + specifier: 2.6.5 + version: 2.6.5 + redux-batched-actions: + specifier: 0.5.0 + version: 0.5.0(redux@4.2.1) + redux-localstorage: + specifier: 0.4.1 + version: 0.4.1 + redux-thunk: + specifier: 2.4.2 + version: 2.4.2(redux@4.2.1) + reselect: + specifier: 4.1.8 + version: 4.1.8 + stacktrace-js: + specifier: 2.0.2 + version: 2.0.2 + typescript: + specifier: 5.7.2 + version: 5.7.2 + +devDependencies: + '@babel/core': + specifier: 7.25.8 + version: 7.25.8 + '@babel/eslint-parser': + specifier: 7.25.8 + version: 7.25.8(@babel/core@7.25.8)(eslint@8.57.1) + '@babel/plugin-proposal-export-default-from': + specifier: 7.25.8 + version: 7.25.8(@babel/core@7.25.8) + '@babel/plugin-syntax-dynamic-import': + specifier: 7.8.3 + version: 7.8.3(@babel/core@7.25.8) + '@babel/preset-env': + specifier: 7.25.8 + version: 7.25.8(@babel/core@7.25.8) + '@babel/preset-react': + specifier: 7.25.7 + version: 7.25.7(@babel/core@7.25.8) + '@babel/preset-typescript': + specifier: 7.25.7 + version: 7.25.7(@babel/core@7.25.8) + '@types/lodash': + specifier: 4.14.195 + version: 4.14.195 + '@types/qs': + specifier: 6.9.16 + version: 6.9.16 + '@types/react-autosuggest': + specifier: 10.1.11 + version: 10.1.11 + '@types/react-document-title': + specifier: 2.0.10 + version: 2.0.10 + '@types/react-google-recaptcha': + specifier: 2.1.9 + version: 2.1.9 + '@types/react-lazyload': + specifier: 3.2.3 + version: 3.2.3 + '@types/react-router-dom': + specifier: 5.3.3 + version: 5.3.3 + '@types/react-text-truncate': + specifier: 0.19.0 + version: 0.19.0 + '@types/react-window': + specifier: 1.8.8 + version: 1.8.8 + '@types/redux-actions': + specifier: 2.6.5 + version: 2.6.5 + '@types/webpack-livereload-plugin': + specifier: 2.3.6 + version: 2.3.6 + '@typescript-eslint/eslint-plugin': + specifier: 6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/parser': + specifier: 6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.7.2) + autoprefixer: + specifier: 10.4.20 + version: 10.4.20(postcss@8.4.47) + babel-loader: + specifier: 9.2.1 + version: 9.2.1(@babel/core@7.25.8)(webpack@5.95.0) + babel-plugin-inline-classnames: + specifier: 2.0.1 + version: 2.0.1(@babel/core@7.25.8) + babel-plugin-transform-react-remove-prop-types: + specifier: 0.4.24 + version: 0.4.24 + core-js: + specifier: 3.39.0 + version: 3.39.0 + css-loader: + specifier: 6.7.3 + version: 6.7.3(webpack@5.95.0) + css-modules-typescript-loader: + specifier: 4.0.1 + version: 4.0.1 + eslint: + specifier: 8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: 8.10.0 + version: 8.10.0(eslint@8.57.1) + eslint-plugin-filenames: + specifier: 1.3.2 + version: 1.3.2(eslint@8.57.1) + eslint-plugin-import: + specifier: 2.31.0 + version: 2.31.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1) + eslint-plugin-prettier: + specifier: 4.2.1 + version: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.1)(prettier@2.8.8) + eslint-plugin-react: + specifier: 7.37.1 + version: 7.37.1(eslint@8.57.1) + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-simple-import-sort: + specifier: 12.1.1 + version: 12.1.1(eslint@8.57.1) + file-loader: + specifier: 6.2.0 + version: 6.2.0(webpack@5.95.0) + filemanager-webpack-plugin: + specifier: 8.0.0 + version: 8.0.0(webpack@5.95.0) + fork-ts-checker-webpack-plugin: + specifier: 8.0.0 + version: 8.0.0(typescript@5.7.2)(webpack@5.95.0) + html-webpack-plugin: + specifier: 5.6.0 + version: 5.6.0(webpack@5.95.0) + loader-utils: + specifier: ^3.2.1 + version: 3.3.1 + mini-css-extract-plugin: + specifier: 2.9.1 + version: 2.9.1(webpack@5.95.0) + postcss: + specifier: 8.4.47 + version: 8.4.47 + postcss-color-function: + specifier: 4.1.0 + version: 4.1.0 + postcss-loader: + specifier: 7.3.0 + version: 7.3.0(postcss@8.4.47)(typescript@5.7.2)(webpack@5.95.0) + postcss-mixins: + specifier: 9.0.4 + version: 9.0.4(postcss@8.4.47) + postcss-nested: + specifier: 6.2.0 + version: 6.2.0(postcss@8.4.47) + postcss-simple-vars: + specifier: 7.0.1 + version: 7.0.1(postcss@8.4.47) + postcss-url: + specifier: 10.1.3 + version: 10.1.3(postcss@8.4.47) + prettier: + specifier: 2.8.8 + version: 2.8.8 + require-nocache: + specifier: 1.0.0 + version: 1.0.0 + rimraf: + specifier: 6.0.1 + version: 6.0.1 + style-loader: + specifier: 3.3.2 + version: 3.3.2(webpack@5.95.0) + stylelint: + specifier: 15.6.1 + version: 15.6.1(typescript@5.7.2) + stylelint-order: + specifier: 6.0.4 + version: 6.0.4(stylelint@15.6.1) + terser-webpack-plugin: + specifier: 5.3.10 + version: 5.3.10(webpack@5.95.0) + ts-loader: + specifier: 9.5.1 + version: 9.5.1(typescript@5.7.2)(webpack@5.95.0) + typescript-plugin-css-modules: + specifier: 5.0.1 + version: 5.0.1(typescript@5.7.2) + url-loader: + specifier: 4.1.1 + version: 4.1.1(file-loader@6.2.0)(webpack@5.95.0) + webpack: + specifier: 5.95.0 + version: 5.95.0(webpack-cli@5.1.4) + webpack-cli: + specifier: 5.1.4 + version: 5.1.4(webpack@5.95.0) + webpack-livereload-plugin: + specifier: 3.0.2 + version: 3.0.2(webpack@5.95.0) + worker-loader: + specifier: 3.0.8 + version: 3.0.8(webpack@5.95.0) + +packages: + + /@adobe/css-tools@4.4.1: + resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==} + dev: true + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@babel/code-frame@7.26.2: + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/compat-data@7.26.3: + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.25.8: + resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.25.8) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/eslint-parser@7.25.8(@babel/core@7.25.8)(eslint@8.57.1): + resolution: {integrity: sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + dependencies: + '@babel/core': 7.25.8 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + dev: true + + /@babel/generator@7.26.3: + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + dev: true + + /@babel/helper-annotate-as-pure@7.25.9: + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.3 + dev: true + + /@babel/helper-compilation-targets@7.25.9: + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.25.8) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.25.8): + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.25.8): + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-member-expression-to-functions@7.25.9: + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-imports@7.25.9: + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.26.0(@babel/core@7.25.8): + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression@7.25.9: + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.3 + dev: true + + /@babel/helper-plugin-utils@7.25.9: + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.25.9: + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.25.9: + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.25.9: + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers@7.26.0: + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + dev: true + + /@babel/parser@7.26.3: + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.3 + dev: true + + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-export-default-from@7.25.8(@babel/core@7.25.8): + resolution: {integrity: sha512-5SLPHA/Gk7lNdaymtSVS9jH77Cs7yuHTR3dYj+9q+M7R7tNLXhNuvnmOfafRIzpWL+dtMibuu1I4ofrc768Gkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.8): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.25.8): + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.25.8): + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.8): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.25.8) + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.25.8): + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.25.8) + '@babel/traverse': 7.26.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + dev: true + + /@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.25.8): + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-for-of@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-function-name@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-literals@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.25.8): + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-new-target@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.25.8) + dev: true + + /@babel/plugin-transform-object-super@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-react-jsx-development@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.25.8) + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-react-pure-annotations@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-spread@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-typescript@7.26.3(@babel/core@7.25.8): + resolution: {integrity: sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.25.8): + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/preset-env@7.25.8(@babel/core@7.25.8): + resolution: {integrity: sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.25.8) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.25.8) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.8) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.25.8) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.25.8) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.25.8) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.25.8) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.8) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.25.8) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.8) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.25.8) + core-js-compat: 3.39.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.8): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.3 + esutils: 2.0.3 + dev: true + + /@babel/preset-react@7.25.7(@babel/core@7.25.8): + resolution: {integrity: sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-react-jsx-development': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-react-pure-annotations': 7.25.9(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-typescript@7.25.7(@babel/core@7.25.8): + resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.25.8) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.25.8) + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/runtime@7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + + /@babel/template@7.25.9: + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + dev: true + + /@babel/traverse@7.26.4: + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.26.3: + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + dev: true + + /@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1): + resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.4.1 + dependencies: + '@csstools/css-tokenizer': 2.4.1 + dev: true + + /@csstools/css-tokenizer@2.4.1: + resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} + engines: {node: ^14 || ^16 || >=18} + dev: true + + /@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1)(@csstools/css-tokenizer@2.4.1): + resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + dev: true + + /@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2): + resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.10 + dependencies: + postcss-selector-parser: 6.1.2 + dev: true + + /@discoveryjs/json-ext@0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + dev: true + + /@eslint-community/eslint-utils@4.4.1(eslint@8.57.1): + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.12.1: + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.1: + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@fortawesome/fontawesome-common-types@6.6.0: + resolution: {integrity: sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==} + engines: {node: '>=6'} + dev: false + + /@fortawesome/fontawesome-free@6.6.0: + resolution: {integrity: sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==} + engines: {node: '>=6'} + dev: false + + /@fortawesome/fontawesome-svg-core@6.6.0: + resolution: {integrity: sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==} + engines: {node: '>=6'} + dependencies: + '@fortawesome/fontawesome-common-types': 6.6.0 + dev: false + + /@fortawesome/free-regular-svg-icons@6.6.0: + resolution: {integrity: sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==} + engines: {node: '>=6'} + dependencies: + '@fortawesome/fontawesome-common-types': 6.6.0 + dev: false + + /@fortawesome/free-solid-svg-icons@6.6.0: + resolution: {integrity: sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==} + engines: {node: '>=6'} + dependencies: + '@fortawesome/fontawesome-common-types': 6.6.0 + dev: false + + /@fortawesome/react-fontawesome@0.2.2(@fortawesome/fontawesome-svg-core@6.6.0)(react@18.3.1): + resolution: {integrity: sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 + react: '>=16.3' + dependencies: + '@fortawesome/fontawesome-svg-core': 6.6.0 + prop-types: 15.8.1 + react: 18.3.1 + dev: false + + /@humanwhocodes/config-array@0.13.0: + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: true + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + + /@microsoft/signalr@6.0.21: + resolution: {integrity: sha512-3MWhSUE7AxkQs3QBuJ/spJJpg1mAHo0/6yRGhs5+Hew3Z+iqYrHVfo0yTElC7W2bVA9t3fW3jliQ9rBN0OvJLA==} + dependencies: + abort-controller: 3.0.0 + eventsource: 1.1.2 + fetch-cookie: 0.11.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + dependencies: + eslint-scope: 5.1.1 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@parcel/watcher-android-arm64@2.5.0: + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-darwin-arm64@2.5.0: + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-darwin-x64@2.5.0: + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-freebsd-x64@2.5.0: + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm-glibc@2.5.0: + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm-musl@2.5.0: + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm64-glibc@2.5.0: + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm64-musl@2.5.0: + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-x64-glibc@2.5.0: + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-x64-musl@2.5.0: + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-arm64@2.5.0: + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-ia32@2.5.0: + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-x64@2.5.0: + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher@2.5.0: + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + dev: true + optional: true + + /@react-dnd/asap@4.0.1: + resolution: {integrity: sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==} + dev: false + + /@react-dnd/invariant@2.0.0: + resolution: {integrity: sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==} + dev: false + + /@react-dnd/shallowequal@2.0.0: + resolution: {integrity: sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==} + dev: false + + /@rtsao/scc@1.1.0: + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + dev: true + + /@sentry-internal/feedback@7.119.1: + resolution: {integrity: sha512-EPyW6EKZmhKpw/OQUPRkTynXecZdYl4uhZwdZuGqnGMAzswPOgQvFrkwsOuPYvoMfXqCH7YuRqyJrox3uBOrTA==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry-internal/replay-canvas@7.119.1: + resolution: {integrity: sha512-O/lrzENbMhP/UDr7LwmfOWTjD9PLNmdaCF408Wx8SDuj7Iwc+VasGfHg7fPH4Pdr4nJON6oh+UqoV4IoG05u+A==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.119.1 + '@sentry/replay': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry-internal/tracing@7.119.1: + resolution: {integrity: sha512-cI0YraPd6qBwvUA3wQdPGTy8PzAoK0NZiaTN1LM3IczdPegehWOaEG5GVTnpGnTsmBAzn1xnBXNBhgiU4dgcrQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry/browser@7.119.1: + resolution: {integrity: sha512-aMwAnFU4iAPeLyZvqmOQaEDHt/Dkf8rpgYeJ0OEi50dmP6AjG+KIAMCXU7CYCCQDn70ITJo8QD5+KzCoZPYz0A==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/feedback': 7.119.1 + '@sentry-internal/replay-canvas': 7.119.1 + '@sentry-internal/tracing': 7.119.1 + '@sentry/core': 7.119.1 + '@sentry/integrations': 7.119.1 + '@sentry/replay': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry/core@7.119.1: + resolution: {integrity: sha512-YUNnH7O7paVd+UmpArWCPH4Phlb5LwrkWVqzFWqL3xPyCcTSof2RL8UmvpkTjgYJjJ+NDfq5mPFkqv3aOEn5Sw==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry/integrations@7.119.1: + resolution: {integrity: sha512-CGmLEPnaBqbUleVqrmGYjRjf5/OwjUXo57I9t0KKWViq81mWnYhaUhRZWFNoCNQHns+3+GPCOMvl0zlawt+evw==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + localforage: 1.10.0 + dev: false + + /@sentry/replay@7.119.1: + resolution: {integrity: sha512-4da+ruMEipuAZf35Ybt2StBdV1S+oJbSVccGpnl9w6RoeQoloT4ztR6ML3UcFDTXeTPT1FnHWDCyOfST0O7XMw==} + engines: {node: '>=12'} + dependencies: + '@sentry-internal/tracing': 7.119.1 + '@sentry/core': 7.119.1 + '@sentry/types': 7.119.1 + '@sentry/utils': 7.119.1 + dev: false + + /@sentry/types@7.119.1: + resolution: {integrity: sha512-4G2mcZNnYzK3pa2PuTq+M2GcwBRY/yy1rF+HfZU+LAPZr98nzq2X3+mJHNJoobeHRkvVh7YZMPi4ogXiIS5VNQ==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@7.119.1: + resolution: {integrity: sha512-ju/Cvyeu/vkfC5/XBV30UNet5kLEicZmXSyuLwZu95hEbL+foPdxN+re7pCI/eNqfe3B2vz7lvz5afLVOlQ2Hg==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.119.1 + dev: false + + /@tanstack/query-core@5.60.6: + resolution: {integrity: sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==} + dev: false + + /@tanstack/react-query@5.61.0(react@18.3.1): + resolution: {integrity: sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ==} + peerDependencies: + react: ^18 || ^19 + dependencies: + '@tanstack/query-core': 5.60.6 + react: 18.3.1 + dev: false + + /@types/archiver@5.3.4: + resolution: {integrity: sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw==} + dependencies: + '@types/readdir-glob': 1.1.5 + dev: true + + /@types/estree@1.0.6: + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + dev: true + + /@types/history@4.7.11: + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + dev: true + + /@types/hoist-non-react-statics@3.3.6: + resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + dependencies: + '@types/react': 18.3.12 + hoist-non-react-statics: 3.3.2 + dev: false + + /@types/html-minifier-terser@6.1.0: + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/lodash@4.14.195: + resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} + dev: true + + /@types/minimist@1.2.5: + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + dev: true + + /@types/node@20.16.11: + resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} + dependencies: + undici-types: 6.19.8 + + /@types/normalize-package-data@2.4.4: + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + dev: true + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: true + + /@types/postcss-modules-local-by-default@4.0.2: + resolution: {integrity: sha512-CtYCcD+L+trB3reJPny+bKWKMzPfxEyQpKIwit7kErnOexf5/faaGpkFy4I5AwbV4hp1sk7/aTg0tt0B67VkLQ==} + dependencies: + postcss: 8.4.47 + dev: true + + /@types/postcss-modules-scope@3.0.4: + resolution: {integrity: sha512-//ygSisVq9kVI0sqx3UPLzWIMCmtSVrzdljtuaAEJtGoGnpjBikZ2sXO5MpH9SnWX9HRfXxHifDAXcQjupWnIQ==} + dependencies: + postcss: 8.4.47 + dev: true + + /@types/prop-types@15.7.14: + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + + /@types/qs@6.9.16: + resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + dev: true + + /@types/react-autosuggest@10.1.11: + resolution: {integrity: sha512-lneJrX/5TZJzKHPJ6UuUjsh9OfeyQHKYEVHyBh5Y7LeRbCZxyIsjBmpxdPy1iH++Ger0qcyW+phPpYH+g3naLA==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react-document-title@2.0.10: + resolution: {integrity: sha512-a5RYXFccVqVhc429yXUn9zjJvaQwdx3Kueb8v8pEymUyExHoatHv0iS8BlOE3YuS+csA2pHbL2Hatnp7QEtLxQ==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react-dom@18.3.1: + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + dependencies: + '@types/react': 18.3.12 + dev: false + + /@types/react-google-recaptcha@2.1.9: + resolution: {integrity: sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react-lazyload@3.2.3: + resolution: {integrity: sha512-s03gWlHXiFqZr7TEFDTx8Lkl+ZEYrwTkXP9MNZ3Z3blzsPrnkYjgeSK2tjfzVv/TYVCnDk6TZwNRDHQlHy/1Ug==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react-redux@7.1.34: + resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + dependencies: + '@types/hoist-non-react-statics': 3.3.6 + '@types/react': 18.3.12 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + + /@types/react-router-dom@5.3.3: + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + '@types/react-router': 5.1.20 + dev: true + + /@types/react-router@5.1.20: + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + dev: true + + /@types/react-text-truncate@0.19.0: + resolution: {integrity: sha512-8H7BjVf7Rp3ERTTiFZpQf6a5hllwdJrWuQ92nwQGp7DWQ2Ju89GRuzXHuZHXU9T+hLTGLCUPbimjQnW1mAogqQ==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react-window@1.8.8: + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + dependencies: + '@types/react': 18.3.12 + dev: true + + /@types/react@18.3.12: + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + dependencies: + '@types/prop-types': 15.7.14 + csstype: 3.1.3 + + /@types/readdir-glob@1.1.5: + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + dependencies: + '@types/node': 20.16.11 + dev: true + + /@types/redux-actions@2.6.5: + resolution: {integrity: sha512-RgXOigay5cNweP+xH1ru+Vaaj1xXYLpWIfSVO8cSA8Ii2xvR+HRfWYdLe1UVOA8X0kIklalGOa0DTDyld0obkg==} + dev: true + + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + + /@types/source-list-map@0.1.6: + resolution: {integrity: sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==} + dev: true + + /@types/tapable@1.0.12: + resolution: {integrity: sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==} + dev: true + + /@types/uglify-js@3.17.5: + resolution: {integrity: sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==} + dependencies: + source-map: 0.6.1 + dev: true + + /@types/webpack-livereload-plugin@2.3.6: + resolution: {integrity: sha512-H8nZSOWSiY/6kCpOmbutZPu7Sai1xyEXo/SrXQPCymMPNBwpYWAdOsjKqr32d+IrVjnn9GGgKSYY34TEPRxJ/A==} + dependencies: + '@types/webpack': 4.41.40 + dev: true + + /@types/webpack-sources@3.2.3: + resolution: {integrity: sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==} + dependencies: + '@types/node': 20.16.11 + '@types/source-list-map': 0.1.6 + source-map: 0.7.4 + dev: true + + /@types/webpack@4.41.40: + resolution: {integrity: sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==} + dependencies: + '@types/node': 20.16.11 + '@types/tapable': 1.0.12 + '@types/uglify-js': 3.17.5 + '@types/webpack-sources': 3.2.3 + anymatch: 3.1.3 + source-map: 0.6.1 + dev: true + + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.7.2): + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2): + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + eslint: 8.57.1 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.21.0: + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + dev: true + + /@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.7.2): + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.2) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + debug: 4.4.0 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.21.0: + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.7.2): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.7.2): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.2) + eslint: 8.57.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.21.0: + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.1: + resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + dev: true + + /@webassemblyjs/ast@1.14.1: + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.13.2: + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + dev: true + + /@webassemblyjs/helper-api-error@1.13.2: + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + dev: true + + /@webassemblyjs/helper-buffer@1.14.1: + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + dev: true + + /@webassemblyjs/helper-numbers@1.13.2: + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.13.2: + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.14.1: + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + dev: true + + /@webassemblyjs/ieee754@1.13.2: + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.13.2: + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.13.2: + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + dev: true + + /@webassemblyjs/wasm-edit@1.14.1: + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + dev: true + + /@webassemblyjs/wasm-gen@1.14.1: + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + dev: true + + /@webassemblyjs/wasm-opt@1.14.1: + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + dev: true + + /@webassemblyjs/wasm-parser@1.14.1: + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + dev: true + + /@webassemblyjs/wast-printer@1.14.1: + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + dev: true + + /@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.95.0): + resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.95.0(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.95.0) + dev: true + + /@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.95.0): + resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.95.0(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.95.0) + dev: true + + /@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.95.0): + resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + dependencies: + webpack: 5.95.0(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.95.0) + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + + /acorn-import-attributes@1.9.5(acorn@8.14.0): + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.14.0 + dev: true + + /acorn-jsx@5.3.2(acorn@8.14.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.14.0 + dev: true + + /acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + dev: false + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /ajv-formats@2.1.1(ajv@8.17.1): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.17.1 + dev: true + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv-keywords@5.1.0(ajv@8.17.1): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + dev: true + + /archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: true + + /archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + is-array-buffer: 3.0.4 + dev: true + + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.5 + is-string: 1.1.0 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + dependencies: + lodash: 4.17.21 + dev: true + + /async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + dev: true + + /autoprefixer@10.4.20(postcss@8.4.47): + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001687 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /babel-loader@9.2.1(@babel/core@7.25.8)(webpack@5.95.0): + resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + dependencies: + '@babel/core': 7.25.8 + find-cache-dir: 4.0.0 + schema-utils: 4.2.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /babel-plugin-inline-classnames@2.0.1(@babel/core@7.25.8): + resolution: {integrity: sha512-Pq/jJ6hTiGiqcMmy2d4CyJcfBDeUHOdQl1t1MDWNaSKR2RxDmShSAx4Zqz6NDmFaiinaRqF8eQoTVgSRGU+McQ==} + engines: {node: '>=6'} + peerDependencies: + '@babel/core': 7.* + dependencies: + '@babel/core': 7.25.8 + dev: true + + /babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.25.8): + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.25.8) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.8): + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.25.8) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.25.8): + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-transform-react-remove-prop-types@0.4.24: + resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==} + dev: true + + /babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + dev: false + + /balanced-match@0.1.0: + resolution: {integrity: sha512-4xb6XqAEo3Z+5pEDJz33R8BZXI8FRJU+cDNLdKgDpmnz+pKKRVYLpdv+VvUAC7yUhBMj4izmyt19eCGv1QGV7A==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + dev: true + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /body@5.1.0: + resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} + dependencies: + continuable-cache: 0.3.1 + error: 7.2.1 + raw-body: 1.1.7 + safe-json-parse: 1.0.1 + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001687 + electron-to-chromium: 1.5.71 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + dev: true + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /bytes@1.0.0: + resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} + dev: true + + /call-bind-apply-helpers@1.0.0: + resolution: {integrity: sha512-CCKAP2tkPau7D3GE8+V8R6sQubA9R5foIzGp+85EXCVSCivuxBNAWqcpn72PKYiIcqoViv/kcUDpaEIMBVi1lQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + /call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.0 + es-define-property: 1.0.1 + get-intrinsic: 1.2.5 + set-function-length: 1.2.2 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001687: + resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + dependencies: + readdirp: 4.0.2 + dev: true + + /chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + dev: true + + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + + /clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + dependencies: + source-map: 0.6.1 + dev: true + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: true + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /color-string@0.3.0: + resolution: {integrity: sha512-sz29j1bmSDfoAxKIEU6zwoIZXN6BrFbAMIhfYCNyiZXBDuU/aiHlN84lp/xDzL2ubyFhLDobHIlU1X70XRrMDA==} + dependencies: + color-name: 1.1.4 + dev: true + + /color@0.11.4: + resolution: {integrity: sha512-Ajpjd8asqZ6EdxQeqGzU5WBhhTfJ/0cA4Wlbre7e5vXfmDSmda7Ov6jeKoru+b0vHcb1CqvuroTHp5zIWzhVMA==} + dependencies: + clone: 1.0.4 + color-convert: 1.9.3 + color-string: 0.3.0 + dev: true + + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + + /common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + dev: true + + /compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /connected-react-router@6.9.3(history@4.10.1)(react-redux@7.2.4)(react-router@5.2.0)(react@18.3.1)(redux@4.2.1): + resolution: {integrity: sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==} + peerDependencies: + history: ^4.7.2 + react: ^16.4.0 || ^17.0.0 + react-redux: ^6.0.0 || ^7.1.0 + react-router: ^4.3.1 || ^5.0.0 + redux: ^3.6.0 || ^4.0.0 + dependencies: + history: 4.10.1 + lodash.isequalwith: 4.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-redux: 7.2.4(react-dom@18.3.1)(react@18.3.1) + react-router: 5.2.0(react@18.3.1) + redux: 4.2.1 + optionalDependencies: + immutable: 4.3.7 + seamless-immutable: 7.1.4 + dev: false + + /continuable-cache@0.3.1: + resolution: {integrity: sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /copy-anything@2.0.6: + resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + dependencies: + is-what: 3.14.1 + dev: true + + /copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + dependencies: + toggle-selection: 1.0.6 + dev: false + + /core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + dependencies: + browserslist: 4.24.2 + dev: true + + /core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + + /core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + requiresBuild: true + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: true + + /cosmiconfig@8.3.6(typescript@5.7.2): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.7.2 + dev: true + + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: true + + /crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + dev: true + + /create-react-context@0.3.0(prop-types@15.8.1)(react@18.3.1): + resolution: {integrity: sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==} + peerDependencies: + prop-types: ^15.0.0 + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + gud: 1.0.0 + prop-types: 15.8.1 + react: 18.3.1 + warning: 4.0.3 + dev: false + + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-color-function@1.3.3: + resolution: {integrity: sha512-YD/WhiRZIYgadwFJ48X5QmlOQ/w8Me4yQI6/eSUoiE8spIFp+S/rGpsAH48iyq/0ZWkCDWqVQKUlQmUzn7BQ9w==} + dependencies: + balanced-match: 0.1.0 + color: 0.11.4 + debug: 3.2.7 + rgb: 0.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + dev: true + + /css-loader@6.7.3(webpack@5.95.0): + resolution: {integrity: sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.47) + postcss-modules-local-by-default: 4.1.0(postcss@8.4.47) + postcss-modules-scope: 3.2.1(postcss@8.4.47) + postcss-modules-values: 4.0.0(postcss@8.4.47) + postcss-value-parser: 4.2.0 + semver: 7.6.3 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /css-modules-typescript-loader@4.0.1: + resolution: {integrity: sha512-vXrUAwPGcRaopnGdg7I5oqv/NSSKQRN5L80m3f49uSGinenU5DTNsMFHS+2roh5tXqpY5+yAAKAl7A2HDvumzg==} + dependencies: + line-diff: 2.1.1 + loader-utils: 1.4.2 + dev: true + + /css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + dev: true + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + /cuint@0.2.2: + resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} + dev: true + + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: false + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.2.0 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.3 + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + /del@6.1.1: + resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} + engines: {node: '>=10'} + dependencies: + globby: 11.1.0 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 4.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + dev: true + + /detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dev: false + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /dnd-core@14.0.1: + resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==} + dependencies: + '@react-dnd/asap': 4.0.1 + '@react-dnd/invariant': 2.0.0 + redux: 4.2.1 + dev: false + + /dnd-multi-backend@6.0.0: + resolution: {integrity: sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g==} + dev: false + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + dependencies: + utila: 0.4.0 + dev: true + + /dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + dev: false + + /dom-helpers@3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.26.0 + dev: false + + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dev: true + + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dev: true + + /dunder-proto@1.0.0: + resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.0 + es-errors: 1.3.0 + gopd: 1.2.0 + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /electron-to-chromium@1.5.71: + resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==} + dev: true + + /element-class@0.2.2: + resolution: {integrity: sha512-e4tkRAFtQkGiZB8fzxAFdjEbx5zajMb1GpiRwKs3lhOLxQcvdOIG7XlERT1sTX3/ulIUGZrgL02YZ0cRNC5OLQ==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: true + + /envinfo@7.14.0: + resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + requiresBuild: true + dependencies: + prr: 1.0.1 + dev: true + optional: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: false + + /error@7.2.1: + resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} + dependencies: + string-template: 0.2.1 + dev: true + + /es-abstract@1.23.5: + resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.5 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.2.0 + is-shared-array-buffer: 1.0.3 + is-string: 1.1.0 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.3 + typed-array-length: 1.0.7 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.16 + dev: true + + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-iterator-helpers@1.2.0: + resolution: {integrity: sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.5 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.0.7 + iterator.prototype: 1.1.3 + safe-array-concat: 1.1.2 + dev: true + + /es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + dev: true + + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.5 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.1.0 + dev: true + + /es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + dev: false + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-prettier@8.10.0(eslint@8.57.1): + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.57.1 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + debug: 3.2.7 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-filenames@1.3.2(eslint@8.57.1): + resolution: {integrity: sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==} + peerDependencies: + eslint: '*' + dependencies: + eslint: 8.57.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.upperfirst: 4.3.1 + dev: true + + /eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1): + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@rtsao/scc': 1.1.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.2) + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.1)(prettier@2.8.8): + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.57.1 + eslint-config-prettier: 8.10.0(eslint@8.57.1) + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.57.1 + dev: true + + /eslint-plugin-react@7.37.1(eslint@8.57.1): + resolution: {integrity: sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.0 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + dev: true + + /eslint-plugin-simple-import-sort@12.1.1(eslint@8.57.1): + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + dependencies: + eslint: 8.57.1 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.1 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /eventsource@1.1.2: + resolution: {integrity: sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==} + engines: {node: '>=0.12.0'} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + dev: true + + /fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /faye-websocket@0.10.0: + resolution: {integrity: sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==} + engines: {node: '>=0.4.0'} + dependencies: + websocket-driver: 0.7.4 + dev: true + + /fetch-cookie@0.11.0: + resolution: {integrity: sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==} + engines: {node: '>=8'} + dependencies: + tough-cookie: 4.1.4 + dev: false + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /file-loader@6.2.0(webpack@5.95.0): + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /filemanager-webpack-plugin@8.0.0(webpack@5.95.0): + resolution: {integrity: sha512-TYwu62wgq2O2c3K80Sfj8vEys/tP5wdgYoySHgUwWoc2hPbQY3Mq3ahcAW634JvHCTcSV7IAfRxMI3wTXRt2Vw==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + webpack: ^5.0.0 + dependencies: + '@types/archiver': 5.3.4 + archiver: 5.3.2 + del: 6.1.1 + fast-glob: 3.3.2 + fs-extra: 10.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + schema-utils: 4.2.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + dev: false + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + dev: true + + /focus-lock@0.11.6: + resolution: {integrity: sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==} + engines: {node: '>=10'} + dependencies: + tslib: 2.8.1 + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + dev: true + + /fork-ts-checker-webpack-plugin@8.0.0(typescript@5.7.2)(webpack@5.95.0): + resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + dependencies: + '@babel/code-frame': 7.26.2 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.6.3 + tapable: 2.2.1 + typescript: 5.7.2 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /fuse.js@6.6.2: + resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==} + engines: {node: '>=10'} + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic@1.2.5: + resolution: {integrity: sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.0 + dunder-proto: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + function-bind: 1.1.2 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + + /get-node-dimensions@1.2.1: + resolution: {integrity: sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==} + dev: false + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + dev: true + + /global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + dev: true + + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /gud@1.0.0: + resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} + dev: false + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.1 + + /has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.0 + dev: true + + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + dependencies: + '@babel/runtime': 7.26.0 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + dev: false + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.37.0 + dev: true + + /html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + dev: true + + /html-webpack-plugin@5.6.0(webpack@5.95.0): + resolution: {integrity: sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.1 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + dev: true + + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + safer-buffer: 2.1.2 + dev: true + optional: true + + /icss-utils@5.1.0(postcss@8.4.47): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.47 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + + /image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + + /immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + requiresBuild: true + dev: false + optional: true + + /immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + dev: true + + /interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + dev: true + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + dev: false + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-boolean-object@1.2.0: + resolution: {integrity: sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-finalizationregistry@1.1.0: + resolution: {integrity: sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + dev: true + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.1.0: + resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: true + + /is-regex@1.2.0: + resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + /is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + dev: true + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + dev: true + + /is-string@1.1.0: + resolution: {integrity: sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.1.0: + resolution: {integrity: sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + has-symbols: 1.1.0 + safe-regex-test: 1.0.3 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.16 + dev: true + + /is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.8 + dev: true + + /is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + dev: true + + /is-what@3.14.1: + resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} + dev: true + + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /iterator.prototype@1.1.3: + resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.5 + has-symbols: 1.1.0 + reflect.getprototypeof: 1.0.8 + set-function-name: 2.0.2 + dev: true + + /jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + dependencies: + '@isaacs/cliui': 8.0.2 + dev: true + + /jdu@1.0.0: + resolution: {integrity: sha512-fa6WTUpCOM7/hpLBudes2zck0fyP5bR4xUkNbywS6b54Is2BxjF56nGpISr8fFCLNDdItxvZn4Qqd19Ej2wNwA==} + dev: false + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.16.11 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + dev: true + + /jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + dev: true + + /just-curry-it@3.2.1: + resolution: {integrity: sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==} + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + dev: true + + /known-css-properties@0.27.0: + resolution: {integrity: sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==} + dev: true + + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.8 + dev: true + + /less@4.2.1: + resolution: {integrity: sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==} + engines: {node: '>=6'} + hasBin: true + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.8.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + dev: true + + /levdist@1.0.0: + resolution: {integrity: sha512-YguwC2spb0pqpJM3a5OsBhih/GG2ZHoaSHnmBqhEI7997a36buhqcRTegEjozHxyxByIwLpZHZTVYMThq+Zd3g==} + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /line-diff@2.1.1: + resolution: {integrity: sha512-vswdynAI5AMPJacOo2o+JJ4caDJbnY2NEqms4MhMW0NJbjh3skP/brpVTAgBxrg55NRZ2Vtw88ef18hnagIpYQ==} + dependencies: + levdist: 1.0.0 + dev: true + + /linear-layout-vector@0.0.1: + resolution: {integrity: sha512-w+nr1ZOVFGyMhwr8JKo0YzqDc8C2Z7pc9UbTuJA4VG/ezlSFEx+7kNrfCYvq7JQ/jHKR+FWy6reNrkVVzm0hSA==} + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /livereload-js@2.4.0: + resolution: {integrity: sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==} + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + + /loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + dev: true + + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + dev: true + + /loader-utils@3.3.1: + resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} + engines: {node: '>= 12.13.0'} + dev: true + + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: true + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: true + + /lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + dev: true + + /lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + dev: true + + /lodash.isequalwith@4.4.0: + resolution: {integrity: sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true + + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + dev: true + + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + dev: true + + /lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.8.1 + dev: true + + /lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + pify: 4.0.1 + semver: 5.7.2 + dev: true + optional: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + dev: true + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + dependencies: + fs-monkey: 1.0.6 + dev: true + + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + + /meow@9.0.0: + resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize: 1.2.0 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /mime@2.5.2: + resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /mini-create-react-context@0.4.1(prop-types@15.8.1)(react@18.3.1): + resolution: {integrity: sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + prop-types: ^15.0.0 + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.26.0 + prop-types: 15.8.1 + react: 18.3.1 + tiny-warning: 1.0.3 + dev: false + + /mini-css-extract-plugin@2.9.1(webpack@5.95.0): + resolution: {integrity: sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + schema-utils: 4.2.0 + tapable: 2.2.1 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /mobile-detect@1.4.5: + resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==} + dev: false + + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + dev: false + + /mousetrap@1.6.5: + resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + engines: {node: '>= 4.4.x'} + hasBin: true + requiresBuild: true + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.1 + dev: true + optional: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + dev: true + + /node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + dev: true + + /node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + requiresBuild: true + dev: true + optional: true + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + dev: true + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.15.1 + semver: 7.6.3 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize.css@8.0.1: + resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} + dev: false + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-assign@3.0.0: + resolution: {integrity: sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==} + engines: {node: '>=0.10.0'} + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + dev: true + + /object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + dev: true + + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.1.1 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: true + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + dev: true + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + dev: true + + /path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + dependencies: + isarray: 0.0.1 + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + requiresBuild: true + dev: true + optional: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + dependencies: + find-up: 6.3.0 + dev: true + + /popper.js@1.16.1: + resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} + deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + dev: false + + /portfinder@1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + dependencies: + async: 2.6.4 + debug: 3.2.7 + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss-color-function@4.1.0: + resolution: {integrity: sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ==} + dependencies: + css-color-function: 1.3.3 + postcss: 6.0.23 + postcss-message-helpers: 2.0.0 + postcss-value-parser: 3.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /postcss-js@4.0.1(postcss@8.4.47): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.47 + dev: true + + /postcss-load-config@3.1.4(postcss@8.4.47): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.47 + yaml: 1.10.2 + dev: true + + /postcss-loader@7.3.0(postcss@8.4.47)(typescript@5.7.2)(webpack@5.95.0): + resolution: {integrity: sha512-qLAFjvR2BFNz1H930P7mj1iuWJFjGey/nVhimfOAAQ1ZyPpcClAxP8+A55Sl8mBvM+K2a9Pjgdj10KpANWrNfw==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + dependencies: + cosmiconfig: 8.3.6(typescript@5.7.2) + jiti: 1.21.6 + klona: 2.0.6 + postcss: 8.4.47 + semver: 7.6.3 + webpack: 5.95.0(webpack-cli@5.1.4) + transitivePeerDependencies: + - typescript + dev: true + + /postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + dev: true + + /postcss-message-helpers@2.0.0: + resolution: {integrity: sha512-tPLZzVAiIJp46TBbpXtrUAKqedXSyW5xDEo1sikrfEfnTs+49SBZR/xDdqCiJvSSbtr615xDsaMF3RrxS2jZlA==} + dev: true + + /postcss-mixins@9.0.4(postcss@8.4.47): + resolution: {integrity: sha512-XVq5jwQJDRu5M1XGkdpgASqLk37OqkH4JCFDXl/Dn7janOJjCTEKL+36cnRVy7bMtoBzALfO7bV7nTIsFnUWLA==} + engines: {node: '>=14.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + fast-glob: 3.3.2 + postcss: 8.4.47 + postcss-js: 4.0.1(postcss@8.4.47) + postcss-simple-vars: 7.0.1(postcss@8.4.47) + sugarss: 4.0.1(postcss@8.4.47) + dev: true + + /postcss-modules-extract-imports@3.1.0(postcss@8.4.47): + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.47 + dev: true + + /postcss-modules-local-by-default@4.1.0(postcss@8.4.47): + resolution: {integrity: sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-selector-parser: 7.0.0 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-modules-scope@3.2.1(postcss@8.4.47): + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 7.0.0 + dev: true + + /postcss-modules-values@4.0.0(postcss@8.4.47): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + dev: true + + /postcss-nested@6.2.0(postcss@8.4.47): + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 6.1.2 + dev: true + + /postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + dev: true + + /postcss-safe-parser@6.0.0(postcss@8.4.47): + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.47 + dev: true + + /postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-simple-vars@7.0.1(postcss@8.4.47): + resolution: {integrity: sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==} + engines: {node: '>=14.0'} + peerDependencies: + postcss: ^8.2.1 + dependencies: + postcss: 8.4.47 + dev: true + + /postcss-sorting@8.0.2(postcss@8.4.47): + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + dependencies: + postcss: 8.4.47 + dev: true + + /postcss-url@10.1.3(postcss@8.4.47): + resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + make-dir: 3.1.0 + mime: 2.5.2 + minimatch: 3.0.8 + postcss: 8.4.47 + xxhashjs: 0.2.2 + dev: true + + /postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@6.0.23: + resolution: {integrity: sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==} + engines: {node: '>=4.0.0'} + dependencies: + chalk: 2.4.2 + source-map: 0.6.1 + supports-color: 5.5.0 + dev: true + + /postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + + /prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + requiresBuild: true + dev: true + optional: true + + /psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + dependencies: + punycode: 2.3.1 + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /raw-body@1.1.7: + resolution: {integrity: sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==} + engines: {node: '>= 0.8.0'} + dependencies: + bytes: 1.0.0 + string_decoder: 0.10.31 + dev: true + + /react-addons-shallow-compare@15.6.3: + resolution: {integrity: sha512-EDJbgKTtGRLhr3wiGDXK/+AEJ59yqGS+tKE6mue0aNXT6ZMR7VJbbzIiT6akotmHg1BLj46ElJSb+NBMp80XBg==} + dependencies: + object-assign: 4.1.1 + dev: false + + /react-async-script@1.2.0(react@18.3.1): + resolution: {integrity: sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==} + peerDependencies: + react: '>=16.4.1' + dependencies: + hoist-non-react-statics: 3.3.2 + prop-types: 15.8.1 + react: 18.3.1 + dev: false + + /react-autosuggest@10.1.0(react@18.3.1): + resolution: {integrity: sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==} + peerDependencies: + react: '>=16.3.0' + dependencies: + es6-promise: 4.2.8 + prop-types: 15.8.1 + react: 18.3.1 + react-themeable: 1.1.0 + section-iterator: 2.0.0 + shallow-equal: 1.2.1 + dev: false + + /react-clientside-effect@1.2.6(react@18.3.1): + resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + dev: false + + /react-custom-scrollbars-2@4.5.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/z0nWAeXfMDr4+OXReTpYd1Atq9kkn4oI3qxq3iMXGQx1EEfwETSqB8HTAvg1X7dEqcCachbny1DRNGlqX5bDQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /react-dnd-html5-backend@14.0.2: + resolution: {integrity: sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw==} + dependencies: + dnd-core: 14.0.1 + dev: false + + /react-dnd-multi-backend@6.0.2(react-dnd-html5-backend@14.0.2)(react-dnd-touch-backend@14.1.1)(react-dnd@14.0.4)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==} + peerDependencies: + react: ^16.13 + react-dnd-html5-backend: ^11.1.3 + react-dnd-touch-backend: ^11.1.3 + react-dom: ^16.13 + dependencies: + dnd-multi-backend: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dnd-html5-backend: 14.0.2 + react-dnd-preview: 6.0.2(react-dnd@14.0.4)(react@18.3.1) + react-dnd-touch-backend: 14.1.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - react-dnd + dev: false + + /react-dnd-preview@6.0.2(react-dnd@14.0.4)(react@18.3.1): + resolution: {integrity: sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==} + peerDependencies: + react: ^16.13.1 + react-dnd: ^11.1.3 + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dnd: 14.0.4(@types/node@20.16.11)(@types/react@18.3.12)(react@18.3.1) + dev: false + + /react-dnd-touch-backend@14.1.1: + resolution: {integrity: sha512-ITmfzn3fJrkUBiVLO6aJZcnu7T8C+GfwZitFryGsXKn5wYcUv+oQBeh9FYcMychmVbDdeUCfvEtTk9O+DKmAaw==} + dependencies: + '@react-dnd/invariant': 2.0.0 + dnd-core: 14.0.1 + dev: false + + /react-dnd@14.0.4(@types/node@20.16.11)(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg==} + peerDependencies: + '@types/hoist-non-react-statics': '>= 3.3.1' + '@types/node': '>= 12' + '@types/react': '>= 16' + react: '>= 16.14' + peerDependenciesMeta: + '@types/hoist-non-react-statics': + optional: true + '@types/node': + optional: true + '@types/react': + optional: true + dependencies: + '@react-dnd/invariant': 2.0.0 + '@react-dnd/shallowequal': 2.0.0 + '@types/node': 20.16.11 + '@types/react': 18.3.12 + dnd-core: 14.0.1 + fast-deep-equal: 3.1.3 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + dev: false + + /react-document-title@2.0.3(react@18.3.1): + resolution: {integrity: sha512-T5y+quDAybtD7JhvVyc2BDW3a9xj6MoW6/VZU6OJkbASqwEMo5G4nB0RqFJCEHOqjQMcQI+wGRPDhUADnaHlQw==} + dependencies: + prop-types: 15.8.1 + react-side-effect: 1.2.0(react@18.3.1) + transitivePeerDependencies: + - react + dev: false + + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + + /react-focus-lock@2.9.4(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.26.0 + '@types/react': 18.3.12 + focus-lock: 0.11.6 + prop-types: 15.8.1 + react: 18.3.1 + react-clientside-effect: 1.2.6(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + dev: false + + /react-google-recaptcha@2.1.0(react@18.3.1): + resolution: {integrity: sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ==} + peerDependencies: + react: '>=16.4.1' + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-async-script: 1.2.0(react@18.3.1) + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-lazyload@3.2.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-zJlrG8QyVZz4+xkYZH5v1w3YaP5wEFaYSUWC4CT9UXfK75IfRAIEdnyIUF+dXr3kX2MOtL1lUaZmaQZqrETwgw==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-measure@1.4.7(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-eHW1uXJOWkiXXqNPPDHlM9ZdUX5L84p0QKpxN5dEogkXvDe/UzovP4gKFLfPW6+mQlbOsmZNdFc/HTNxtZ9kHg==} + peerDependencies: + react: '>0.13.0' + react-dom: '>0.13.0' + dependencies: + get-node-dimensions: 1.2.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resize-observer-polyfill: 1.5.1 + dev: false + + /react-popper@1.3.7(react@18.3.1): + resolution: {integrity: sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==} + peerDependencies: + react: 0.14.x || ^15.0.0 || ^16.0.0 + dependencies: + '@babel/runtime': 7.26.0 + create-react-context: 0.3.0(prop-types@15.8.1)(react@18.3.1) + deep-equal: 1.1.2 + popper.js: 1.16.1 + prop-types: 15.8.1 + react: 18.3.1 + typed-styles: 0.0.7 + warning: 4.0.3 + dev: false + + /react-redux@7.2.4(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==} + peerDependencies: + react: ^16.8.3 || ^17 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.26.0 + '@types/react-redux': 7.1.34 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 16.13.1 + dev: false + + /react-router-dom@5.2.0(react@18.3.1): + resolution: {integrity: sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==} + peerDependencies: + react: '>=15' + dependencies: + '@babel/runtime': 7.26.0 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-router: 5.2.0(react@18.3.1) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + dev: false + + /react-router@5.2.0(react@18.3.1): + resolution: {integrity: sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==} + peerDependencies: + react: '>=15' + dependencies: + '@babel/runtime': 7.26.0 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + mini-create-react-context: 0.4.1(prop-types@15.8.1)(react@18.3.1) + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + dev: false + + /react-side-effect@1.2.0(react@18.3.1): + resolution: {integrity: sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==} + peerDependencies: + react: ^0.13.0 || ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + react: 18.3.1 + shallowequal: 1.1.0 + dev: false + + /react-slider@1.1.4(prop-types@15.8.1)(react@18.3.1): + resolution: {integrity: sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ==} + peerDependencies: + prop-types: ^15.6 + react: ^16 || ^17 + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + dev: false + + /react-tabs@4.3.0(react@18.3.1): + resolution: {integrity: sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-0 || ^18.0.0 + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.3.1 + dev: false + + /react-text-truncate@0.19.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-QxHpZABfGG0Z3WEYbRTZ+rXdZn50Zvp+sWZXgVAd7FCKAMzv/kcwctTpNmWgXDTpAoHhMjOVwmgRtX3x5yeF4w==} + peerDependencies: + react: ^15.4.1 || ^16.0.0 || ^17.0.0 || || ^18.0.0 + react-dom: ^15.4.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /react-themeable@1.1.0: + resolution: {integrity: sha512-kl5tQ8K+r9IdQXZd8WLa+xxYN04lLnJXRVhHfdgwsUJr/SlKJxIejoc9z9obEkx1mdqbTw1ry43fxEUwyD9u7w==} + dependencies: + object-assign: 3.0.0 + dev: false + + /react-use-measure@2.1.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + dependencies: + debounce: 1.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /react-virtualized@9.21.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha + react-dom: ^15.3.0 || ^16.0.0-alpha + dependencies: + babel-runtime: 6.26.0 + clsx: 1.2.1 + dom-helpers: 3.4.0 + linear-layout-vector: 0.0.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + dev: false + + /react-window@1.8.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.26.0 + memoize-one: 5.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + dependencies: + minimatch: 5.1.6 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + dev: true + + /rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + dependencies: + resolve: 1.22.8 + dev: true + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /reduce-reducers@0.4.3: + resolution: {integrity: sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==} + dev: false + + /redux-actions@2.6.5: + resolution: {integrity: sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==} + dependencies: + invariant: 2.2.4 + just-curry-it: 3.2.1 + loose-envify: 1.4.0 + reduce-reducers: 0.4.3 + to-camel-case: 1.0.0 + dev: false + + /redux-batched-actions@0.5.0(redux@4.2.1): + resolution: {integrity: sha512-6orZWyCnIQXMGY4DUGM0oj0L7oYnwTACsfsru/J7r94RM3P9eS7SORGpr3LCeRCMoIMQcpfKZ7X4NdyFHBS8Eg==} + peerDependencies: + redux: '>=1.0.0' + dependencies: + redux: 4.2.1 + dev: false + + /redux-localstorage@0.4.1: + resolution: {integrity: sha512-dUha0YoH+BSZ2q15pakB+JWeqiuXUf3Ir4rObOpNrZ96HEdciGAjkL10k3KGdLI7qvQw/c096asw/SQ6TPjU/A==} + dev: false + + /redux-thunk@2.4.2(redux@4.2.1): + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + dependencies: + redux: 4.2.1 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.26.0 + dev: false + + /reflect.getprototypeof@1.0.8: + resolution: {integrity: sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + dunder-proto: 1.0.0 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + which-builtin-type: 1.2.0 + dev: true + + /regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + dev: false + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.26.0 + dev: true + + /regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + /regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + dev: true + + /regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + dev: true + + /regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + dependencies: + jsesc: 3.0.2 + dev: true + + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + dev: true + + /renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /require-nocache@1.0.0: + resolution: {integrity: sha512-nemgZOwvrnGtMH6DQ7n17RQNrQ779BBUiPJTeqrdXNE4ytaoX+HTflXvNlyKooJoFbNUJEtZmm9FlbmjxUXDgA==} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + + /reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + dev: false + + /reserved-words@0.1.2: + resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==} + dev: true + + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + dev: false + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rgb@0.1.0: + resolution: {integrity: sha512-F49dXX73a92N09uQkfCp2QjwXpmJcn9/i9PvjmwsSIXUGqRLCf/yx5Q9gRxuLQTq248kakqQuc8GX/U/CxSqlA==} + hasBin: true + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + glob: 11.0.0 + package-json-from-dist: 1.0.1 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + has-symbols: 1.1.0 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-json-parse@1.0.1: + resolution: {integrity: sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==} + dev: true + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-regex: 1.2.0 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + requiresBuild: true + dev: true + optional: true + + /sass@1.82.0: + resolution: {integrity: sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 4.0.1 + immutable: 5.0.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.0 + dev: true + + /sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: true + + /sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + requiresBuild: true + dev: true + optional: true + + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + dev: true + + /seamless-immutable@7.1.4: + resolution: {integrity: sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==} + requiresBuild: true + dev: false + optional: true + + /section-iterator@2.0.0: + resolution: {integrity: sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==} + dev: false + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: true + + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + + /shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + object-inspect: 1.13.3 + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.20 + dev: true + + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + dev: true + + /spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + dev: true + + /stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + dependencies: + stackframe: 1.3.4 + dev: false + + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: false + + /stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + dev: false + + /stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + dev: false + + /string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: true + + /string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.5 + dev: true + + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.1.0 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /style-loader@3.3.2(webpack@5.95.0): + resolution: {integrity: sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + dev: true + + /stylelint-order@6.0.4(stylelint@15.6.1): + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + dependencies: + postcss: 8.4.47 + postcss-sorting: 8.0.2(postcss@8.4.47) + stylelint: 15.6.1(typescript@5.7.2) + dev: true + + /stylelint@15.6.1(typescript@5.7.2): + resolution: {integrity: sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1)(@csstools/css-tokenizer@2.4.1) + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 8.3.6(typescript@5.7.2) + css-functions-list: 3.2.3 + css-tree: 2.3.1 + debug: 4.4.0 + fast-glob: 3.3.2 + fastest-levenshtein: 1.0.16 + file-entry-cache: 6.0.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.3.2 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.27.0 + mathml-tag-names: 2.1.3 + meow: 9.0.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.47 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 6.0.0(postcss@8.4.47) + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 3.1.0 + svg-tags: 1.0.0 + table: 6.9.0 + v8-compile-cache: 2.4.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /stylus@0.59.0: + resolution: {integrity: sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==} + hasBin: true + dependencies: + '@adobe/css-tools': 4.4.1 + debug: 4.4.0 + glob: 7.2.3 + sax: 1.2.4 + source-map: 0.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /sugarss@4.0.1(postcss@8.4.47): + resolution: {integrity: sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.47 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-hyperlinks@3.1.0: + resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} + engines: {node: '>=14.18'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + dev: true + + /table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /terser-webpack-plugin@5.3.10(webpack@5.95.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.37.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /terser@5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + + /tiny-lr@1.1.1: + resolution: {integrity: sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==} + dependencies: + body: 5.1.0 + debug: 3.2.7 + faye-websocket: 0.10.0 + livereload-js: 2.4.0 + object-assign: 4.1.1 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + dev: true + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + + /to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + dependencies: + to-space-case: 1.0.0 + dev: false + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + + /toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + dev: false + + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + + /ts-api-utils@1.4.3(typescript@5.7.2): + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.7.2 + dev: true + + /ts-loader@9.5.1(typescript@5.7.2)(webpack@5.95.0): + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + micromatch: 4.0.8 + semver: 7.6.3 + source-map: 0.7.4 + typescript: 5.7.2 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.3: + resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.13 + reflect.getprototypeof: 1.0.8 + dev: true + + /typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.8 + dev: true + + /typed-styles@0.0.7: + resolution: {integrity: sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==} + dev: false + + /typescript-plugin-css-modules@5.0.1(typescript@5.7.2): + resolution: {integrity: sha512-hKXObfwfjx2/myRq4JeQ8D3xIWYTFqusi0hS/Aka7RFX1xQEoEkdOGDWyXNb8LmObawsUzbI30gQnZvqYXCrkA==} + peerDependencies: + typescript: '>=4.0.0' + dependencies: + '@types/postcss-modules-local-by-default': 4.0.2 + '@types/postcss-modules-scope': 3.0.4 + dotenv: 16.4.7 + icss-utils: 5.1.0(postcss@8.4.47) + less: 4.2.1 + lodash.camelcase: 4.3.0 + postcss: 8.4.47 + postcss-load-config: 3.1.4(postcss@8.4.47) + postcss-modules-extract-imports: 3.1.0(postcss@8.4.47) + postcss-modules-local-by-default: 4.1.0(postcss@8.4.47) + postcss-modules-scope: 3.2.1(postcss@8.4.47) + reserved-words: 0.1.2 + sass: 1.82.0 + source-map-js: 1.2.1 + stylus: 0.59.0 + tsconfig-paths: 4.2.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + + /typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.8 + has-bigints: 1.0.2 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.0 + dev: true + + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + /unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /update-browserslist-db@1.1.1(browserslist@4.24.2): + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /url-loader@4.1.1(file-loader@6.2.0)(webpack@5.95.0): + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + file-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + file-loader: + optional: true + dependencies: + file-loader: 6.2.0(webpack@5.95.0) + loader-utils: 2.0.4 + mime-types: 2.1.35 + schema-utils: 3.3.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + + /use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.12 + react: 18.3.1 + tslib: 2.8.1 + dev: false + + /use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.12 + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + dev: true + + /v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + dev: false + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /webpack-cli@5.1.4(webpack@5.95.0): + resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} + engines: {node: '>=14.15.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + webpack: 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.95.0) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.95.0) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.95.0) + colorette: 2.0.20 + commander: 10.0.1 + cross-spawn: 7.0.6 + envinfo: 7.14.0 + fastest-levenshtein: 1.0.16 + import-local: 3.2.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.95.0(webpack-cli@5.1.4) + webpack-merge: 5.10.0 + dev: true + + /webpack-livereload-plugin@3.0.2(webpack@5.95.0): + resolution: {integrity: sha512-5JeZ2dgsvSNG+clrkD/u2sEiPcNk4qwCVZZmW8KpqKcNlkGv7IJjdVrq13+etAmMZYaCF1EGXdHkVFuLgP4zfw==} + engines: {node: '>= 10.18.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + anymatch: 3.1.3 + portfinder: 1.0.32 + schema-utils: 4.2.0 + tiny-lr: 1.1.1 + webpack: 5.95.0(webpack-cli@5.1.4) + transitivePeerDependencies: + - supports-color + dev: true + + /webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.95.0(webpack-cli@5.1.4): + resolution: {integrity: sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.95.0) + watchpack: 2.4.2 + webpack-cli: 5.1.4(webpack@5.95.0) + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + dev: true + + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /which-boxed-primitive@1.1.0: + resolution: {integrity: sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==} + engines: {node: '>= 0.4'} + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.0 + is-number-object: 1.1.0 + is-string: 1.1.0 + is-symbol: 1.1.0 + dev: true + + /which-builtin-type@1.2.0: + resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.1.0 + is-generator-function: 1.0.10 + is-regex: 1.2.0 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.1.0 + which-collection: 1.0.2 + which-typed-array: 1.1.16 + dev: true + + /which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + dev: true + + /which-typed-array@1.1.16: + resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + dev: true + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + dev: true + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /worker-loader@3.0.8(webpack@5.95.0): + resolution: {integrity: sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.95.0(webpack-cli@5.1.4) + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + dev: true + + /ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xxhashjs@0.2.2: + resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} + dependencies: + cuint: 0.2.2 + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + dev: true + + /zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + dev: true diff --git a/yarn.lock b/yarn.lock index 4e1c21419..23cd15e46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2434,10 +2434,10 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1: dependencies: browserslist "^4.23.3" -core-js@3.38.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== +core-js@3.39.0: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-js@^2.4.0: version "2.6.12" @@ -6713,10 +6713,10 @@ typescript-plugin-css-modules@5.0.1: stylus "^0.59.0" tsconfig-paths "^4.1.2" -typescript@5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== unbox-primitive@^1.0.2: version "1.0.2" From 811eb36c7b1a5124270ff93d18d16944e654de81 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 8 Dec 2024 17:24:58 -0800 Subject: [PATCH 697/762] Convert Calendar to TypeScript --- frontend/src/App/AppRoutes.tsx | 4 +- frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/CalendarAppState.ts | 25 +- frontend/src/Calendar/Agenda/Agenda.js | 38 --- frontend/src/Calendar/Agenda/Agenda.tsx | 25 ++ .../src/Calendar/Agenda/AgendaConnector.js | 14 - frontend/src/Calendar/Agenda/AgendaEvent.js | 254 ---------------- frontend/src/Calendar/Agenda/AgendaEvent.tsx | 227 ++++++++++++++ .../Calendar/Agenda/AgendaEventConnector.js | 30 -- frontend/src/Calendar/Calendar.js | 67 ----- frontend/src/Calendar/Calendar.tsx | 170 +++++++++++ frontend/src/Calendar/CalendarConnector.js | 196 ------------- frontend/src/Calendar/CalendarPage.js | 197 ------------- frontend/src/Calendar/CalendarPage.tsx | 226 ++++++++++++++ .../src/Calendar/CalendarPageConnector.js | 117 -------- frontend/src/Calendar/Day/CalendarDay.tsx | 107 ++++++- .../src/Calendar/Day/CalendarDayConnector.js | 91 ------ frontend/src/Calendar/Day/CalendarDays.js | 164 ----------- frontend/src/Calendar/Day/CalendarDays.tsx | 135 +++++++++ .../src/Calendar/Day/CalendarDaysConnector.js | 25 -- frontend/src/Calendar/Day/DayOfWeek.js | 56 ---- frontend/src/Calendar/Day/DayOfWeek.tsx | 54 ++++ frontend/src/Calendar/Day/DaysOfWeek.js | 97 ------ frontend/src/Calendar/Day/DaysOfWeek.tsx | 60 ++++ .../src/Calendar/Day/DaysOfWeekConnector.js | 22 -- frontend/src/Calendar/Events/CalendarEvent.js | 267 ----------------- .../src/Calendar/Events/CalendarEvent.tsx | 240 +++++++++++++++ .../Calendar/Events/CalendarEventConnector.js | 29 -- .../src/Calendar/Events/CalendarEventGroup.js | 259 ---------------- .../Calendar/Events/CalendarEventGroup.tsx | 253 ++++++++++++++++ .../Events/CalendarEventGroupConnector.js | 37 --- .../Events/CalendarEventQueueDetails.js | 56 ---- .../Events/CalendarEventQueueDetails.tsx | 58 ++++ .../src/Calendar/Header/CalendarHeader.js | 268 ----------------- .../src/Calendar/Header/CalendarHeader.tsx | 221 ++++++++++++++ .../Header/CalendarHeaderConnector.js | 85 ------ .../Header/CalendarHeaderViewButton.js | 45 --- .../Header/CalendarHeaderViewButton.tsx | 34 +++ .../Calendar/Legend/{Legend.js => Legend.tsx} | 53 ++-- .../src/Calendar/Legend/LegendConnector.js | 21 -- .../src/Calendar/Legend/LegendIconItem.js | 43 --- .../src/Calendar/Legend/LegendIconItem.tsx | 33 +++ .../Legend/{LegendItem.js => LegendItem.tsx} | 24 +- .../Calendar/Options/CalendarOptionsModal.js | 29 -- .../Calendar/Options/CalendarOptionsModal.tsx | 21 ++ .../Options/CalendarOptionsModalContent.js | 276 ------------------ .../Options/CalendarOptionsModalContent.tsx | 228 +++++++++++++++ .../CalendarOptionsModalContentConnector.js | 25 -- .../{calendarViews.js => calendarViews.ts} | 2 + .../{getStatusStyle.js => getStatusStyle.ts} | 10 +- .../src/Calendar/iCal/CalendarLinkModal.js | 29 -- .../src/Calendar/iCal/CalendarLinkModal.tsx | 20 ++ .../Calendar/iCal/CalendarLinkModalContent.js | 222 -------------- .../iCal/CalendarLinkModalContent.tsx | 166 +++++++++++ .../iCal/CalendarLinkModalContentConnector.js | 17 -- .../src/Components/Form/FormInputGroup.tsx | 4 +- frontend/src/Components/Form/TextInput.tsx | 5 +- frontend/src/Components/Icon.tsx | 2 +- frontend/src/Components/Link/Button.tsx | 8 +- frontend/src/Episode/Episode.ts | 2 + frontend/src/Episode/useEpisode.ts | 3 +- .../createSeriesQualityProfileSelector.ts | 3 +- frontend/src/typings/Calendar.ts | 25 ++ frontend/src/typings/CalendarEventGroup.ts | 15 - frontend/src/typings/Queue.ts | 2 + frontend/src/typings/Settings/UiSettings.ts | 3 + 66 files changed, 2378 insertions(+), 3168 deletions(-) delete mode 100644 frontend/src/Calendar/Agenda/Agenda.js create mode 100644 frontend/src/Calendar/Agenda/Agenda.tsx delete mode 100644 frontend/src/Calendar/Agenda/AgendaConnector.js delete mode 100644 frontend/src/Calendar/Agenda/AgendaEvent.js create mode 100644 frontend/src/Calendar/Agenda/AgendaEvent.tsx delete mode 100644 frontend/src/Calendar/Agenda/AgendaEventConnector.js delete mode 100644 frontend/src/Calendar/Calendar.js create mode 100644 frontend/src/Calendar/Calendar.tsx delete mode 100644 frontend/src/Calendar/CalendarConnector.js delete mode 100644 frontend/src/Calendar/CalendarPage.js create mode 100644 frontend/src/Calendar/CalendarPage.tsx delete mode 100644 frontend/src/Calendar/CalendarPageConnector.js delete mode 100644 frontend/src/Calendar/Day/CalendarDayConnector.js delete mode 100644 frontend/src/Calendar/Day/CalendarDays.js create mode 100644 frontend/src/Calendar/Day/CalendarDays.tsx delete mode 100644 frontend/src/Calendar/Day/CalendarDaysConnector.js delete mode 100644 frontend/src/Calendar/Day/DayOfWeek.js create mode 100644 frontend/src/Calendar/Day/DayOfWeek.tsx delete mode 100644 frontend/src/Calendar/Day/DaysOfWeek.js create mode 100644 frontend/src/Calendar/Day/DaysOfWeek.tsx delete mode 100644 frontend/src/Calendar/Day/DaysOfWeekConnector.js delete mode 100644 frontend/src/Calendar/Events/CalendarEvent.js create mode 100644 frontend/src/Calendar/Events/CalendarEvent.tsx delete mode 100644 frontend/src/Calendar/Events/CalendarEventConnector.js delete mode 100644 frontend/src/Calendar/Events/CalendarEventGroup.js create mode 100644 frontend/src/Calendar/Events/CalendarEventGroup.tsx delete mode 100644 frontend/src/Calendar/Events/CalendarEventGroupConnector.js delete mode 100644 frontend/src/Calendar/Events/CalendarEventQueueDetails.js create mode 100644 frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx delete mode 100644 frontend/src/Calendar/Header/CalendarHeader.js create mode 100644 frontend/src/Calendar/Header/CalendarHeader.tsx delete mode 100644 frontend/src/Calendar/Header/CalendarHeaderConnector.js delete mode 100644 frontend/src/Calendar/Header/CalendarHeaderViewButton.js create mode 100644 frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx rename frontend/src/Calendar/Legend/{Legend.js => Legend.tsx} (77%) delete mode 100644 frontend/src/Calendar/Legend/LegendConnector.js delete mode 100644 frontend/src/Calendar/Legend/LegendIconItem.js create mode 100644 frontend/src/Calendar/Legend/LegendIconItem.tsx rename frontend/src/Calendar/Legend/{LegendItem.js => LegendItem.tsx} (61%) delete mode 100644 frontend/src/Calendar/Options/CalendarOptionsModal.js create mode 100644 frontend/src/Calendar/Options/CalendarOptionsModal.tsx delete mode 100644 frontend/src/Calendar/Options/CalendarOptionsModalContent.js create mode 100644 frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx delete mode 100644 frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js rename frontend/src/Calendar/{calendarViews.js => calendarViews.ts} (72%) rename frontend/src/Calendar/{getStatusStyle.js => getStatusStyle.ts} (67%) delete mode 100644 frontend/src/Calendar/iCal/CalendarLinkModal.js create mode 100644 frontend/src/Calendar/iCal/CalendarLinkModal.tsx delete mode 100644 frontend/src/Calendar/iCal/CalendarLinkModalContent.js create mode 100644 frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx delete mode 100644 frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js create mode 100644 frontend/src/typings/Calendar.ts delete mode 100644 frontend/src/typings/CalendarEventGroup.ts diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 1b4fea9c2..fbe4a15bb 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -5,7 +5,7 @@ import History from 'Activity/History/History'; import Queue from 'Activity/Queue/Queue'; import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector'; import ImportSeries from 'AddSeries/ImportSeries/ImportSeries'; -import CalendarPageConnector from 'Calendar/CalendarPageConnector'; +import CalendarPage from 'Calendar/CalendarPage'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector'; @@ -72,7 +72,7 @@ function AppRoutes() { Calendar */} - <Route path="/calendar" component={CalendarPageConnector} /> + <Route path="/calendar" component={CalendarPage} /> {/* Activity diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 36047cc4e..84bd5d0b4 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -54,10 +54,12 @@ export interface CustomFilter { export interface AppSectionState { isConnected: boolean; isReconnecting: boolean; + isSidebarVisible: boolean; version: string; prevVersion?: string; dimensions: { isSmallScreen: boolean; + isLargeScreen: boolean; width: number; height: number; }; diff --git a/frontend/src/App/State/CalendarAppState.ts b/frontend/src/App/State/CalendarAppState.ts index de6a523b3..75c8b5e50 100644 --- a/frontend/src/App/State/CalendarAppState.ts +++ b/frontend/src/App/State/CalendarAppState.ts @@ -1,10 +1,29 @@ +import moment from 'moment'; import AppSectionState, { AppSectionFilterState, } from 'App/State/AppSectionState'; -import Episode from 'Episode/Episode'; +import { CalendarView } from 'Calendar/calendarViews'; +import { CalendarItem } from 'typings/Calendar'; + +interface CalendarOptions { + showEpisodeInformation: boolean; + showFinaleIcon: boolean; + showSpecialIcon: boolean; + showCutoffUnmetIcon: boolean; + collapseMultipleEpisodes: boolean; + fullColorEvents: boolean; +} interface CalendarAppState - extends AppSectionState<Episode>, - AppSectionFilterState<Episode> {} + extends AppSectionState<CalendarItem>, + AppSectionFilterState<CalendarItem> { + searchMissingCommandId: number | null; + start: moment.Moment; + end: moment.Moment; + dates: string[]; + time: string; + view: CalendarView; + options: CalendarOptions; +} export default CalendarAppState; diff --git a/frontend/src/Calendar/Agenda/Agenda.js b/frontend/src/Calendar/Agenda/Agenda.js deleted file mode 100644 index 89472301d..000000000 --- a/frontend/src/Calendar/Agenda/Agenda.js +++ /dev/null @@ -1,38 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React from 'react'; -import AgendaEventConnector from './AgendaEventConnector'; -import styles from './Agenda.css'; - -function Agenda(props) { - const { - items - } = props; - - return ( - <div className={styles.agenda}> - { - items.map((item, index) => { - const momentDate = moment(item.airDateUtc); - const showDate = index === 0 || - !moment(items[index - 1].airDateUtc).isSame(momentDate, 'day'); - - return ( - <AgendaEventConnector - key={item.id} - episodeId={item.id} - showDate={showDate} - {...item} - /> - ); - }) - } - </div> - ); -} - -Agenda.propTypes = { - items: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Agenda; diff --git a/frontend/src/Calendar/Agenda/Agenda.tsx b/frontend/src/Calendar/Agenda/Agenda.tsx new file mode 100644 index 000000000..fdef40466 --- /dev/null +++ b/frontend/src/Calendar/Agenda/Agenda.tsx @@ -0,0 +1,25 @@ +import moment from 'moment'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import AgendaEvent from './AgendaEvent'; +import styles from './Agenda.css'; + +function Agenda() { + const { items } = useSelector((state: AppState) => state.calendar); + + return ( + <div className={styles.agenda}> + {items.map((item, index) => { + const momentDate = moment(item.airDateUtc); + const showDate = + index === 0 || + !moment(items[index - 1].airDateUtc).isSame(momentDate, 'day'); + + return <AgendaEvent key={item.id} showDate={showDate} {...item} />; + })} + </div> + ); +} + +export default Agenda; diff --git a/frontend/src/Calendar/Agenda/AgendaConnector.js b/frontend/src/Calendar/Agenda/AgendaConnector.js deleted file mode 100644 index b6f238873..000000000 --- a/frontend/src/Calendar/Agenda/AgendaConnector.js +++ /dev/null @@ -1,14 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import Agenda from './Agenda'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - (calendar) => { - return calendar; - } - ); -} - -export default connect(createMapStateToProps)(Agenda); diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js deleted file mode 100644 index 608528478..000000000 --- a/frontend/src/Calendar/Agenda/AgendaEvent.js +++ /dev/null @@ -1,254 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails'; -import getStatusStyle from 'Calendar/getStatusStyle'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; -import episodeEntities from 'Episode/episodeEntities'; -import getFinaleTypeName from 'Episode/getFinaleTypeName'; -import { icons, kinds } from 'Helpers/Props'; -import formatTime from 'Utilities/Date/formatTime'; -import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; -import styles from './AgendaEvent.css'; - -class AgendaEvent extends Component { - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - // - // Render - - render() { - const { - id, - series, - episodeFile, - title, - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - airDateUtc, - monitored, - unverifiedSceneNumbering, - finaleType, - hasFile, - grabbed, - queueItem, - showDate, - showEpisodeInformation, - showFinaleIcon, - showSpecialIcon, - showCutoffUnmetIcon, - timeFormat, - longDateFormat, - colorImpairedMode - } = this.props; - - const startTime = moment(airDateUtc); - const endTime = moment(airDateUtc).add(series.runtime, 'minutes'); - const downloading = !!(queueItem || grabbed); - const isMonitored = series.monitored && monitored; - const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored); - const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; - - return ( - <div className={styles.event}> - <Link - className={styles.underlay} - onPress={this.onPress} - /> - - <div className={styles.overlay}> - <div className={styles.date}> - { - showDate && - startTime.format(longDateFormat) - } - </div> - - <div - className={classNames( - styles.eventWrapper, - styles[statusStyle], - colorImpairedMode && 'colorImpaired' - )} - > - <div className={styles.time}> - {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} - </div> - - <div className={styles.seriesTitle}> - {series.title} - </div> - - { - showEpisodeInformation && - <div className={styles.seasonEpisodeNumber}> - {seasonNumber}x{padNumber(episodeNumber, 2)} - - { - series.seriesType === 'anime' && absoluteEpisodeNumber && - <span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span> - } - - <div className={styles.episodeSeparator}> - </div> - </div> - } - - <div className={styles.episodeTitle}> - { - showEpisodeInformation && - title - } - </div> - - { - missingAbsoluteNumber && - <Icon - className={styles.statusIcon} - name={icons.WARNING} - title={translate('EpisodeMissingAbsoluteNumber')} - /> - } - - { - unverifiedSceneNumbering && !missingAbsoluteNumber ? - <Icon - className={styles.statusIcon} - name={icons.WARNING} - title={translate('SceneNumberNotVerified')} - /> : - null - } - - { - !!queueItem && - <span className={styles.statusIcon}> - <CalendarEventQueueDetails - seriesType={series.seriesType} - seasonNumber={seasonNumber} - absoluteEpisodeNumber={absoluteEpisodeNumber} - {...queueItem} - /> - </span> - } - - { - !queueItem && grabbed && - <Icon - className={styles.statusIcon} - name={icons.DOWNLOADING} - title={translate('EpisodeIsDownloading')} - /> - } - - { - showCutoffUnmetIcon && - !!episodeFile && - episodeFile.qualityCutoffNotMet && - <Icon - className={styles.statusIcon} - name={icons.EPISODE_FILE} - kind={kinds.WARNING} - title={translate('QualityCutoffNotMet')} - /> - } - - { - episodeNumber === 1 && seasonNumber > 0 && - <Icon - className={styles.statusIcon} - name={icons.INFO} - kind={kinds.INFO} - title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')} - /> - } - - { - showFinaleIcon && - finaleType ? - <Icon - className={styles.statusIcon} - name={icons.INFO} - kind={kinds.WARNING} - title={getFinaleTypeName(finaleType)} - /> : - null - } - - { - showSpecialIcon && - (episodeNumber === 0 || seasonNumber === 0) && - <Icon - className={styles.statusIcon} - name={icons.INFO} - kind={kinds.PINK} - title={translate('Special')} - /> - } - </div> - </div> - - <EpisodeDetailsModal - isOpen={this.state.isDetailsModalOpen} - episodeId={id} - episodeEntity={episodeEntities.CALENDAR} - seriesId={series.id} - episodeTitle={title} - showOpenSeriesButton={true} - onModalClose={this.onDetailsModalClose} - /> - </div> - ); - } -} - -AgendaEvent.propTypes = { - id: PropTypes.number.isRequired, - series: PropTypes.object.isRequired, - episodeFile: PropTypes.object, - title: PropTypes.string.isRequired, - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - airDateUtc: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - unverifiedSceneNumbering: PropTypes.bool, - finaleType: PropTypes.string, - hasFile: PropTypes.bool.isRequired, - grabbed: PropTypes.bool, - queueItem: PropTypes.object, - showDate: PropTypes.bool.isRequired, - showEpisodeInformation: PropTypes.bool.isRequired, - showFinaleIcon: PropTypes.bool.isRequired, - showSpecialIcon: PropTypes.bool.isRequired, - showCutoffUnmetIcon: PropTypes.bool.isRequired, - timeFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - colorImpairedMode: PropTypes.bool.isRequired -}; - -export default AgendaEvent; diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.tsx b/frontend/src/Calendar/Agenda/AgendaEvent.tsx new file mode 100644 index 000000000..2fd2d7c54 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEvent.tsx @@ -0,0 +1,227 @@ +import classNames from 'classnames'; +import moment from 'moment'; +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; +import episodeEntities from 'Episode/episodeEntities'; +import getFinaleTypeName from 'Episode/getFinaleTypeName'; +import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; +import { icons, kinds } from 'Helpers/Props'; +import useSeries from 'Series/useSeries'; +import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import translate from 'Utilities/String/translate'; +import styles from './AgendaEvent.css'; + +interface AgendaEventProps { + id: number; + seriesId: number; + episodeFileId: number; + title: string; + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + airDateUtc: string; + monitored: boolean; + unverifiedSceneNumbering?: boolean; + finaleType?: string; + hasFile: boolean; + grabbed?: boolean; + showDate: boolean; +} + +function AgendaEvent(props: AgendaEventProps) { + const { + id, + seriesId, + episodeFileId, + title, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDateUtc, + monitored, + unverifiedSceneNumbering, + finaleType, + hasFile, + grabbed, + showDate, + } = props; + + const series = useSeries(seriesId)!; + const episodeFile = useEpisodeFile(episodeFileId); + const queueItem = useSelector(createQueueItemSelectorForHook(id)); + const { timeFormat, longDateFormat, enableColorImpairedMode } = useSelector( + createUISettingsSelector() + ); + + const { + showEpisodeInformation, + showFinaleIcon, + showSpecialIcon, + showCutoffUnmetIcon, + } = useSelector((state: AppState) => state.calendar.options); + + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const startTime = moment(airDateUtc); + const endTime = moment(airDateUtc).add(series.runtime, 'minutes'); + const downloading = !!(queueItem || grabbed); + const isMonitored = series.monitored && monitored; + const statusStyle = getStatusStyle( + hasFile, + downloading, + startTime, + endTime, + isMonitored + ); + const missingAbsoluteNumber = + series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; + + const handlePress = useCallback(() => { + setIsDetailsModalOpen(true); + }, []); + + const handleDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(false); + }, []); + + return ( + <div className={styles.event}> + <Link className={styles.underlay} onPress={handlePress} /> + + <div className={styles.overlay}> + <div className={styles.date}> + {showDate && startTime.format(longDateFormat)} + </div> + + <div + className={classNames( + styles.eventWrapper, + styles[statusStyle], + enableColorImpairedMode && 'colorImpaired' + )} + > + <div className={styles.time}> + {formatTime(airDateUtc, timeFormat)} -{' '} + {formatTime(endTime.toISOString(), timeFormat, { + includeMinuteZero: true, + })} + </div> + + <div className={styles.seriesTitle}>{series.title}</div> + + {showEpisodeInformation ? ( + <div className={styles.seasonEpisodeNumber}> + {seasonNumber}x{padNumber(episodeNumber, 2)} + {series.seriesType === 'anime' && absoluteEpisodeNumber && ( + <span className={styles.absoluteEpisodeNumber}> + ({absoluteEpisodeNumber}) + </span> + )} + <div className={styles.episodeSeparator}> - </div> + </div> + ) : null} + + <div className={styles.episodeTitle}> + {showEpisodeInformation ? title : null} + </div> + + {missingAbsoluteNumber ? ( + <Icon + className={styles.statusIcon} + name={icons.WARNING} + title={translate('EpisodeMissingAbsoluteNumber')} + /> + ) : null} + + {unverifiedSceneNumbering && !missingAbsoluteNumber ? ( + <Icon + className={styles.statusIcon} + name={icons.WARNING} + title={translate('SceneNumberNotVerified')} + /> + ) : null} + + {queueItem ? ( + <span className={styles.statusIcon}> + <CalendarEventQueueDetails + seasonNumber={seasonNumber} + {...queueItem} + /> + </span> + ) : null} + + {!queueItem && grabbed ? ( + <Icon + className={styles.statusIcon} + name={icons.DOWNLOADING} + title={translate('EpisodeIsDownloading')} + /> + ) : null} + + {showCutoffUnmetIcon && + episodeFile && + episodeFile.qualityCutoffNotMet ? ( + <Icon + className={styles.statusIcon} + name={icons.EPISODE_FILE} + kind={kinds.WARNING} + title={translate('QualityCutoffNotMet')} + /> + ) : null} + + {episodeNumber === 1 && seasonNumber > 0 && ( + <Icon + className={styles.statusIcon} + name={icons.INFO} + kind={kinds.INFO} + title={ + seasonNumber === 1 + ? translate('SeriesPremiere') + : translate('SeasonPremiere') + } + /> + )} + + {showFinaleIcon && finaleType ? ( + <Icon + className={styles.statusIcon} + name={icons.INFO} + kind={kinds.WARNING} + title={getFinaleTypeName(finaleType)} + /> + ) : null} + + {showSpecialIcon && (episodeNumber === 0 || seasonNumber === 0) ? ( + <Icon + className={styles.statusIcon} + name={icons.INFO} + kind={kinds.PINK} + title={translate('Special')} + /> + ) : null} + </div> + </div> + + <EpisodeDetailsModal + isOpen={isDetailsModalOpen} + episodeId={id} + episodeEntity={episodeEntities.CALENDAR} + seriesId={series.id} + episodeTitle={title} + showOpenSeriesButton={true} + onModalClose={handleDetailsModalClose} + /> + </div> + ); +} + +export default AgendaEvent; diff --git a/frontend/src/Calendar/Agenda/AgendaEventConnector.js b/frontend/src/Calendar/Agenda/AgendaEventConnector.js deleted file mode 100644 index d476acf80..000000000 --- a/frontend/src/Calendar/Agenda/AgendaEventConnector.js +++ /dev/null @@ -1,30 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import AgendaEvent from './AgendaEvent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.options, - createSeriesSelector(), - createEpisodeFileSelector(), - createQueueItemSelector(), - createUISettingsSelector(), - (calendarOptions, series, episodeFile, queueItem, uiSettings) => { - return { - series, - episodeFile, - queueItem, - ...calendarOptions, - timeFormat: uiSettings.timeFormat, - longDateFormat: uiSettings.longDateFormat, - colorImpairedMode: uiSettings.enableColorImpairedMode - }; - } - ); -} - -export default connect(createMapStateToProps)(AgendaEvent); diff --git a/frontend/src/Calendar/Calendar.js b/frontend/src/Calendar/Calendar.js deleted file mode 100644 index 0a2fd671d..000000000 --- a/frontend/src/Calendar/Calendar.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AgendaConnector from './Agenda/AgendaConnector'; -import * as calendarViews from './calendarViews'; -import CalendarDaysConnector from './Day/CalendarDaysConnector'; -import DaysOfWeekConnector from './Day/DaysOfWeekConnector'; -import CalendarHeaderConnector from './Header/CalendarHeaderConnector'; -import styles from './Calendar.css'; - -class Calendar extends Component { - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - view - } = this.props; - - return ( - <div className={styles.calendar}> - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - !isFetching && !!error && - <Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert> - } - - { - !error && isPopulated && view === calendarViews.AGENDA && - <div className={styles.calendarContent}> - <CalendarHeaderConnector /> - <AgendaConnector /> - </div> - } - - { - !error && isPopulated && view !== calendarViews.AGENDA && - <div className={styles.calendarContent}> - <CalendarHeaderConnector /> - <DaysOfWeekConnector /> - <CalendarDaysConnector /> - </div> - } - </div> - ); - } -} - -Calendar.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - view: PropTypes.string.isRequired -}; - -export default Calendar; diff --git a/frontend/src/Calendar/Calendar.tsx b/frontend/src/Calendar/Calendar.tsx new file mode 100644 index 000000000..caa337cf0 --- /dev/null +++ b/frontend/src/Calendar/Calendar.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Episode from 'Episode/Episode'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { kinds } from 'Helpers/Props'; +import { + clearCalendar, + fetchCalendar, + gotoCalendarToday, +} from 'Store/Actions/calendarActions'; +import { + clearEpisodeFiles, + fetchEpisodeFiles, +} from 'Store/Actions/episodeFileActions'; +import { + clearQueueDetails, + fetchQueueDetails, +} from 'Store/Actions/queueActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import Agenda from './Agenda/Agenda'; +import CalendarDays from './Day/CalendarDays'; +import DaysOfWeek from './Day/DaysOfWeek'; +import CalendarHeader from './Header/CalendarHeader'; +import styles from './Calendar.css'; + +const UPDATE_DELAY = 3600000; // 1 hour + +function Calendar() { + const dispatch = useDispatch(); + const requestCurrentPage = useCurrentPage(); + const updateTimeout = useRef<ReturnType<typeof setTimeout>>(); + + const { isFetching, isPopulated, error, items, time, view } = useSelector( + (state: AppState) => state.calendar + ); + + const isRefreshingSeries = useSelector( + createCommandExecutingSelector(commandNames.REFRESH_SERIES) + ); + + const firstDayOfWeek = useSelector( + (state: AppState) => state.settings.ui.item.firstDayOfWeek + ); + + const wasRefreshingSeries = usePrevious(isRefreshingSeries); + const previousFirstDayOfWeek = usePrevious(firstDayOfWeek); + const previousItems = usePrevious(items); + + const handleScheduleUpdate = useCallback(() => { + clearTimeout(updateTimeout.current); + + function updateCalendar() { + dispatch(gotoCalendarToday()); + updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY); + } + + updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY); + }, [dispatch]); + + useEffect(() => { + handleScheduleUpdate(); + + return () => { + dispatch(clearCalendar()); + dispatch(clearQueueDetails()); + dispatch(clearEpisodeFiles()); + clearTimeout(updateTimeout.current); + }; + }, [dispatch, handleScheduleUpdate]); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchCalendar()); + } else { + dispatch(gotoCalendarToday()); + } + }, [requestCurrentPage, dispatch]); + + useEffect(() => { + const repopulate = () => { + dispatch(fetchQueueDetails({ time, view })); + dispatch(fetchCalendar({ time, view })); + }; + + registerPagePopulator(repopulate, [ + 'episodeFileUpdated', + 'episodeFileDeleted', + ]); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [time, view, dispatch]); + + useEffect(() => { + handleScheduleUpdate(); + }, [time, handleScheduleUpdate]); + + useEffect(() => { + if ( + previousFirstDayOfWeek != null && + firstDayOfWeek !== previousFirstDayOfWeek + ) { + dispatch(fetchCalendar({ time, view })); + } + }, [time, view, firstDayOfWeek, previousFirstDayOfWeek, dispatch]); + + useEffect(() => { + if (wasRefreshingSeries && !isRefreshingSeries) { + dispatch(fetchCalendar({ time, view })); + } + }, [time, view, isRefreshingSeries, wasRefreshingSeries, dispatch]); + + useEffect(() => { + if (!previousItems || hasDifferentItems(items, previousItems)) { + const episodeIds = selectUniqueIds<Episode, number>(items, 'id'); + const episodeFileIds = selectUniqueIds<Episode, number>( + items, + 'episodeFileId' + ); + + if (items.length) { + dispatch(fetchQueueDetails({ episodeIds })); + } + + if (episodeFileIds.length) { + dispatch(fetchEpisodeFiles({ episodeFileIds })); + } + } + }, [items, previousItems, dispatch]); + + return ( + <div className={styles.calendar}> + {isFetching && !isPopulated ? <LoadingIndicator /> : null} + + {!isFetching && error ? ( + <Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert> + ) : null} + + {!error && isPopulated && view === 'agenda' ? ( + <div className={styles.calendarContent}> + <CalendarHeader /> + <Agenda /> + </div> + ) : null} + + {!error && isPopulated && view !== 'agenda' ? ( + <div className={styles.calendarContent}> + <CalendarHeader /> + <DaysOfWeek /> + <CalendarDays /> + </div> + ) : null} + </div> + ); +} + +export default Calendar; diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js deleted file mode 100644 index 47c769126..000000000 --- a/frontend/src/Calendar/CalendarConnector.js +++ /dev/null @@ -1,196 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import * as calendarActions from 'Store/Actions/calendarActions'; -import { clearEpisodeFiles, fetchEpisodeFiles } from 'Store/Actions/episodeFileActions'; -import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; -import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; -import Calendar from './Calendar'; - -const UPDATE_DELAY = 3600000; // 1 hour - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - (state) => state.settings.ui.item.firstDayOfWeek, - createCommandExecutingSelector(commandNames.REFRESH_SERIES), - (calendar, firstDayOfWeek, isRefreshingSeries) => { - return { - ...calendar, - isRefreshingSeries, - firstDayOfWeek - }; - } - ); -} - -const mapDispatchToProps = { - ...calendarActions, - fetchEpisodeFiles, - clearEpisodeFiles, - fetchQueueDetails, - clearQueueDetails -}; - -class CalendarConnector extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.updateTimeoutId = null; - } - - componentDidMount() { - const { - useCurrentPage, - fetchCalendar, - gotoCalendarToday - } = this.props; - - registerPagePopulator(this.repopulate, ['episodeFileUpdated', 'episodeFileDeleted']); - - if (useCurrentPage) { - fetchCalendar(); - } else { - gotoCalendarToday(); - } - - this.scheduleUpdate(); - } - - componentDidUpdate(prevProps) { - const { - items, - time, - view, - isRefreshingSeries, - firstDayOfWeek - } = this.props; - - if (hasDifferentItems(prevProps.items, items)) { - const episodeIds = selectUniqueIds(items, 'id'); - const episodeFileIds = selectUniqueIds(items, 'episodeFileId'); - - if (items.length) { - this.props.fetchQueueDetails({ episodeIds }); - } - - if (episodeFileIds.length) { - this.props.fetchEpisodeFiles({ episodeFileIds }); - } - } - - if (prevProps.time !== time) { - this.scheduleUpdate(); - } - - if (prevProps.firstDayOfWeek !== firstDayOfWeek) { - this.props.fetchCalendar({ time, view }); - } - - if (prevProps.isRefreshingSeries && !isRefreshingSeries) { - this.props.fetchCalendar({ time, view }); - } - } - - componentWillUnmount() { - unregisterPagePopulator(this.repopulate); - this.props.clearCalendar(); - this.props.clearQueueDetails(); - this.props.clearEpisodeFiles(); - this.clearUpdateTimeout(); - } - - // - // Control - - repopulate = () => { - const { - time, - view - } = this.props; - - this.props.fetchQueueDetails({ time, view }); - this.props.fetchCalendar({ time, view }); - }; - - scheduleUpdate = () => { - this.clearUpdateTimeout(); - - this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY); - }; - - clearUpdateTimeout = () => { - if (this.updateTimeoutId) { - clearTimeout(this.updateTimeoutId); - } - }; - - updateCalendar = () => { - this.props.gotoCalendarToday(); - this.scheduleUpdate(); - }; - - // - // Listeners - - onCalendarViewChange = (view) => { - this.props.setCalendarView({ view }); - }; - - onTodayPress = () => { - this.props.gotoCalendarToday(); - }; - - onPreviousPress = () => { - this.props.gotoCalendarPreviousRange(); - }; - - onNextPress = () => { - this.props.gotoCalendarNextRange(); - }; - - // - // Render - - render() { - return ( - <Calendar - {...this.props} - onCalendarViewChange={this.onCalendarViewChange} - onTodayPress={this.onTodayPress} - onPreviousPress={this.onPreviousPress} - onNextPress={this.onNextPress} - /> - ); - } -} - -CalendarConnector.propTypes = { - useCurrentPage: PropTypes.bool.isRequired, - time: PropTypes.string, - view: PropTypes.string.isRequired, - firstDayOfWeek: PropTypes.number.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - isRefreshingSeries: PropTypes.bool.isRequired, - setCalendarView: PropTypes.func.isRequired, - gotoCalendarToday: PropTypes.func.isRequired, - gotoCalendarPreviousRange: PropTypes.func.isRequired, - gotoCalendarNextRange: PropTypes.func.isRequired, - clearCalendar: PropTypes.func.isRequired, - fetchCalendar: PropTypes.func.isRequired, - fetchEpisodeFiles: PropTypes.func.isRequired, - clearEpisodeFiles: PropTypes.func.isRequired, - fetchQueueDetails: PropTypes.func.isRequired, - clearQueueDetails: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector); diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js deleted file mode 100644 index 2e4d56b6b..000000000 --- a/frontend/src/Calendar/CalendarPage.js +++ /dev/null @@ -1,197 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Measure from 'Components/Measure'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import { align, icons } from 'Helpers/Props'; -import NoSeries from 'Series/NoSeries'; -import translate from 'Utilities/String/translate'; -import CalendarConnector from './CalendarConnector'; -import CalendarFilterModal from './CalendarFilterModal'; -import CalendarLinkModal from './iCal/CalendarLinkModal'; -import LegendConnector from './Legend/LegendConnector'; -import CalendarOptionsModal from './Options/CalendarOptionsModal'; -import styles from './CalendarPage.css'; - -const MINIMUM_DAY_WIDTH = 120; - -class CalendarPage extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isCalendarLinkModalOpen: false, - isOptionsModalOpen: false, - width: 0 - }; - } - - // - // Listeners - - onMeasure = ({ width }) => { - this.setState({ width }); - const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))); - - this.props.onDaysCountChange(days); - }; - - onGetCalendarLinkPress = () => { - this.setState({ isCalendarLinkModalOpen: true }); - }; - - onGetCalendarLinkModalClose = () => { - this.setState({ isCalendarLinkModalOpen: false }); - }; - - onOptionsPress = () => { - this.setState({ isOptionsModalOpen: true }); - }; - - onOptionsModalClose = () => { - this.setState({ isOptionsModalOpen: false }); - }; - - onSearchMissingPress = () => { - const { - missingEpisodeIds, - onSearchMissingPress - } = this.props; - - onSearchMissingPress(missingEpisodeIds); - }; - - // - // Render - - render() { - const { - selectedFilterKey, - filters, - customFilters, - hasSeries, - missingEpisodeIds, - isRssSyncExecuting, - isSearchingForMissing, - useCurrentPage, - onRssSyncPress, - onFilterSelect - } = this.props; - - const { - isCalendarLinkModalOpen, - isOptionsModalOpen - } = this.state; - - const isMeasured = this.state.width > 0; - const PageComponent = hasSeries ? CalendarConnector : NoSeries; - - return ( - <PageContent title={translate('Calendar')}> - <PageToolbar> - <PageToolbarSection> - <PageToolbarButton - label={translate('ICalLink')} - iconName={icons.CALENDAR} - onPress={this.onGetCalendarLinkPress} - /> - - <PageToolbarSeparator /> - - <PageToolbarButton - label={translate('RssSync')} - iconName={icons.RSS} - isSpinning={isRssSyncExecuting} - onPress={onRssSyncPress} - /> - - <PageToolbarButton - label={translate('SearchForMissing')} - iconName={icons.SEARCH} - isDisabled={!missingEpisodeIds.length} - isSpinning={isSearchingForMissing} - onPress={this.onSearchMissingPress} - /> - </PageToolbarSection> - - <PageToolbarSection alignContent={align.RIGHT}> - <PageToolbarButton - label={translate('Options')} - iconName={icons.POSTER} - onPress={this.onOptionsPress} - /> - - <FilterMenu - alignMenu={align.RIGHT} - isDisabled={!hasSeries} - selectedFilterKey={selectedFilterKey} - filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={CalendarFilterModal} - onFilterSelect={onFilterSelect} - /> - </PageToolbarSection> - </PageToolbar> - - <PageContentBody - className={styles.calendarPageBody} - innerClassName={styles.calendarInnerPageBody} - > - <Measure - whitelist={['width']} - onMeasure={this.onMeasure} - > - { - isMeasured ? - <PageComponent - useCurrentPage={useCurrentPage} - /> : - <div /> - } - </Measure> - - { - hasSeries && - <LegendConnector /> - } - </PageContentBody> - - <CalendarLinkModal - isOpen={isCalendarLinkModalOpen} - onModalClose={this.onGetCalendarLinkModalClose} - /> - - <CalendarOptionsModal - isOpen={isOptionsModalOpen} - onModalClose={this.onOptionsModalClose} - /> - </PageContent> - ); - } -} - -CalendarPage.propTypes = { - selectedFilterKey: PropTypes.string.isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - hasSeries: PropTypes.bool.isRequired, - missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired, - isRssSyncExecuting: PropTypes.bool.isRequired, - isSearchingForMissing: PropTypes.bool.isRequired, - useCurrentPage: PropTypes.bool.isRequired, - onSearchMissingPress: PropTypes.func.isRequired, - onDaysCountChange: PropTypes.func.isRequired, - onRssSyncPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired -}; - -export default CalendarPage; diff --git a/frontend/src/Calendar/CalendarPage.tsx b/frontend/src/Calendar/CalendarPage.tsx new file mode 100644 index 000000000..f408b6a60 --- /dev/null +++ b/frontend/src/Calendar/CalendarPage.tsx @@ -0,0 +1,226 @@ +import moment from 'moment'; +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Measure from 'Components/Measure'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import { align, icons } from 'Helpers/Props'; +import NoSeries from 'Series/NoSeries'; +import { + searchMissing, + setCalendarDaysCount, + setCalendarFilter, +} from 'Store/Actions/calendarActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector'; +import { isCommandExecuting } from 'Utilities/Command'; +import isBefore from 'Utilities/Date/isBefore'; +import translate from 'Utilities/String/translate'; +import Calendar from './Calendar'; +import CalendarFilterModal from './CalendarFilterModal'; +import CalendarLinkModal from './iCal/CalendarLinkModal'; +import Legend from './Legend/Legend'; +import CalendarOptionsModal from './Options/CalendarOptionsModal'; +import styles from './CalendarPage.css'; + +const MINIMUM_DAY_WIDTH = 120; + +function createMissingEpisodeIdsSelector() { + return createSelector( + (state: AppState) => state.calendar.start, + (state: AppState) => state.calendar.end, + (state: AppState) => state.calendar.items, + (state: AppState) => state.queue.details.items, + (start, end, episodes, queueDetails) => { + return episodes.reduce<number[]>((acc, episode) => { + const airDateUtc = episode.airDateUtc; + + if ( + !episode.episodeFileId && + moment(airDateUtc).isAfter(start) && + moment(airDateUtc).isBefore(end) && + isBefore(episode.airDateUtc) && + !queueDetails.some( + (details) => !!details.episode && details.episode.id === episode.id + ) + ) { + acc.push(episode.id); + } + + return acc; + }, []); + } + ); +} + +function createIsSearchingSelector() { + return createSelector( + (state: AppState) => state.calendar.searchMissingCommandId, + createCommandsSelector(), + (searchMissingCommandId, commands) => { + if (searchMissingCommandId == null) { + return false; + } + + return isCommandExecuting( + commands.find((command) => { + return command.id === searchMissingCommandId; + }) + ); + } + ); +} + +function CalendarPage() { + const dispatch = useDispatch(); + + const { selectedFilterKey, filters } = useSelector( + (state: AppState) => state.calendar + ); + const missingEpisodeIds = useSelector(createMissingEpisodeIdsSelector()); + const isSearchingForMissing = useSelector(createIsSearchingSelector()); + const isRssSyncExecuting = useSelector( + createCommandExecutingSelector(commandNames.RSS_SYNC) + ); + const customFilters = useSelector(createCustomFiltersSelector('calendar')); + const hasSeries = !!useSelector(createSeriesCountSelector()); + + const [isCalendarLinkModalOpen, setIsCalendarLinkModalOpen] = useState(false); + const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); + const [width, setWidth] = useState(0); + + const isMeasured = width > 0; + const PageComponent = hasSeries ? Calendar : NoSeries; + + const handleMeasure = useCallback( + ({ width: newWidth }: { width: number }) => { + setWidth(newWidth); + + const dayCount = Math.max( + 3, + Math.min(7, Math.floor(newWidth / MINIMUM_DAY_WIDTH)) + ); + + dispatch(setCalendarDaysCount({ dayCount })); + }, + [dispatch] + ); + + const handleGetCalendarLinkPress = useCallback(() => { + setIsCalendarLinkModalOpen(true); + }, []); + + const handleGetCalendarLinkModalClose = useCallback(() => { + setIsCalendarLinkModalOpen(false); + }, []); + + const handleOptionsPress = useCallback(() => { + setIsOptionsModalOpen(true); + }, []); + + const handleOptionsModalClose = useCallback(() => { + setIsOptionsModalOpen(false); + }, []); + + const handleRssSyncPress = useCallback(() => { + dispatch( + executeCommand({ + name: commandNames.RSS_SYNC, + }) + ); + }, [dispatch]); + + const handleSearchMissingPress = useCallback(() => { + dispatch(searchMissing({ episodeIds: missingEpisodeIds })); + }, [missingEpisodeIds, dispatch]); + + const handleFilterSelect = useCallback( + (key: string) => { + dispatch(setCalendarFilter({ selectedFilterKey: key })); + }, + [dispatch] + ); + + return ( + <PageContent title={translate('Calendar')}> + <PageToolbar> + <PageToolbarSection> + <PageToolbarButton + label={translate('ICalLink')} + iconName={icons.CALENDAR} + onPress={handleGetCalendarLinkPress} + /> + + <PageToolbarSeparator /> + + <PageToolbarButton + label={translate('RssSync')} + iconName={icons.RSS} + isSpinning={isRssSyncExecuting} + onPress={handleRssSyncPress} + /> + + <PageToolbarButton + label={translate('SearchForMissing')} + iconName={icons.SEARCH} + isDisabled={!missingEpisodeIds.length} + isSpinning={isSearchingForMissing} + onPress={handleSearchMissingPress} + /> + </PageToolbarSection> + + <PageToolbarSection alignContent={align.RIGHT}> + <PageToolbarButton + label={translate('Options')} + iconName={icons.POSTER} + onPress={handleOptionsPress} + /> + + <FilterMenu + alignMenu={align.RIGHT} + isDisabled={!hasSeries} + selectedFilterKey={selectedFilterKey} + filters={filters} + customFilters={customFilters} + filterModalConnectorComponent={CalendarFilterModal} + onFilterSelect={handleFilterSelect} + /> + </PageToolbarSection> + </PageToolbar> + + <PageContentBody + className={styles.calendarPageBody} + innerClassName={styles.calendarInnerPageBody} + > + <Measure whitelist={['width']} onMeasure={handleMeasure}> + {isMeasured ? <PageComponent totalItems={0} /> : <div />} + </Measure> + + {hasSeries && <Legend />} + </PageContentBody> + + <CalendarLinkModal + isOpen={isCalendarLinkModalOpen} + onModalClose={handleGetCalendarLinkModalClose} + /> + + <CalendarOptionsModal + isOpen={isOptionsModalOpen} + onModalClose={handleOptionsModalClose} + /> + </PageContent> + ); +} + +export default CalendarPage; diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js deleted file mode 100644 index b47142b64..000000000 --- a/frontend/src/Calendar/CalendarPageConnector.js +++ /dev/null @@ -1,117 +0,0 @@ -import moment from 'moment'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import withCurrentPage from 'Components/withCurrentPage'; -import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { isCommandExecuting } from 'Utilities/Command'; -import isBefore from 'Utilities/Date/isBefore'; -import CalendarPage from './CalendarPage'; - -function createMissingEpisodeIdsSelector() { - return createSelector( - (state) => state.calendar.start, - (state) => state.calendar.end, - (state) => state.calendar.items, - (state) => state.queue.details.items, - (start, end, episodes, queueDetails) => { - return episodes.reduce((acc, episode) => { - const airDateUtc = episode.airDateUtc; - - if ( - !episode.episodeFileId && - moment(airDateUtc).isAfter(start) && - moment(airDateUtc).isBefore(end) && - isBefore(episode.airDateUtc) && - !queueDetails.some((details) => !!details.episode && details.episode.id === episode.id) - ) { - acc.push(episode.id); - } - - return acc; - }, []); - } - ); -} - -function createIsSearchingSelector() { - return createSelector( - (state) => state.calendar.searchMissingCommandId, - createCommandsSelector(), - (searchMissingCommandId, commands) => { - if (searchMissingCommandId == null) { - return false; - } - - return isCommandExecuting(commands.find((command) => { - return command.id === searchMissingCommandId; - })); - } - ); -} - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.selectedFilterKey, - (state) => state.calendar.filters, - createCustomFiltersSelector('calendar'), - createSeriesCountSelector(), - createUISettingsSelector(), - createMissingEpisodeIdsSelector(), - createCommandExecutingSelector(commandNames.RSS_SYNC), - createIsSearchingSelector(), - ( - selectedFilterKey, - filters, - customFilters, - seriesCount, - uiSettings, - missingEpisodeIds, - isRssSyncExecuting, - isSearchingForMissing - ) => { - return { - selectedFilterKey, - filters, - customFilters, - colorImpairedMode: uiSettings.enableColorImpairedMode, - hasSeries: !!seriesCount, - missingEpisodeIds, - isRssSyncExecuting, - isSearchingForMissing - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onRssSyncPress() { - dispatch(executeCommand({ - name: commandNames.RSS_SYNC - })); - }, - - onSearchMissingPress(episodeIds) { - dispatch(searchMissing({ episodeIds })); - }, - - onDaysCountChange(dayCount) { - dispatch(setCalendarDaysCount({ dayCount })); - }, - - onFilterSelect(selectedFilterKey) { - dispatch(setCalendarFilter({ selectedFilterKey })); - } - }; -} - -export default withCurrentPage( - connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage) -); diff --git a/frontend/src/Calendar/Day/CalendarDay.tsx b/frontend/src/Calendar/Day/CalendarDay.tsx index 7538b0467..a619109ca 100644 --- a/frontend/src/Calendar/Day/CalendarDay.tsx +++ b/frontend/src/Calendar/Day/CalendarDay.tsx @@ -1,25 +1,104 @@ import classNames from 'classnames'; import moment from 'moment'; import React from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import * as calendarViews from 'Calendar/calendarViews'; -import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector'; -import CalendarEventGroupConnector from 'Calendar/Events/CalendarEventGroupConnector'; -import Series from 'Series/Series'; -import CalendarEventGroup, { CalendarEvent } from 'typings/CalendarEventGroup'; +import CalendarEvent from 'Calendar/Events/CalendarEvent'; +import CalendarEventGroup from 'Calendar/Events/CalendarEventGroup'; +import { + CalendarEvent as CalendarEventModel, + CalendarEventGroup as CalendarEventGroupModel, + CalendarItem, +} from 'typings/Calendar'; import styles from './CalendarDay.css'; +function sort(items: (CalendarEventModel | CalendarEventGroupModel)[]) { + return items.sort((a, b) => { + const aDate = a.isGroup + ? moment(a.events[0].airDateUtc).unix() + : moment(a.airDateUtc).unix(); + + const bDate = b.isGroup + ? moment(b.events[0].airDateUtc).unix() + : moment(b.airDateUtc).unix(); + + return aDate - bDate; + }); +} + +function createCalendarEventsConnector(date: string) { + return createSelector( + (state: AppState) => state.calendar.items, + (state: AppState) => state.calendar.options.collapseMultipleEpisodes, + (items, collapseMultipleEpisodes) => { + const momentDate = moment(date); + + const filtered = items.filter((item) => { + return momentDate.isSame(moment(item.airDateUtc), 'day'); + }); + + if (!collapseMultipleEpisodes) { + return sort( + filtered.map((item) => ({ + isGroup: false, + ...item, + })) + ); + } + + const groupedObject = Object.groupBy( + filtered, + (item: CalendarItem) => `${item.seriesId}-${item.seasonNumber}` + ); + + const grouped = Object.entries(groupedObject).reduce< + (CalendarEventModel | CalendarEventGroupModel)[] + >((acc, [, events]) => { + if (!events) { + return acc; + } + + if (events.length === 1) { + acc.push({ + isGroup: false, + ...events[0], + }); + } else { + acc.push({ + isGroup: true, + seriesId: events[0].seriesId, + seasonNumber: events[0].seasonNumber, + episodeIds: events.map((event) => event.id), + events: events.sort( + (a, b) => + moment(a.airDateUtc).unix() - moment(b.airDateUtc).unix() + ), + }); + } + + return acc; + }, []); + + return sort(grouped); + } + ); +} + interface CalendarDayProps { date: string; - time: string; isTodaysDate: boolean; - events: (CalendarEvent | CalendarEventGroup)[]; - view: string; - onEventModalOpenToggle(...args: unknown[]): unknown; + onEventModalOpenToggle(isOpen: boolean): unknown; } -function CalendarDay(props: CalendarDayProps) { - const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } = - props; +function CalendarDay({ + date, + isTodaysDate, + onEventModalOpenToggle, +}: CalendarDayProps) { + const { time, view } = useSelector((state: AppState) => state.calendar); + const events = useSelector(createCalendarEventsConnector(date)); const ref = React.useRef<HTMLDivElement>(null); @@ -53,7 +132,7 @@ function CalendarDay(props: CalendarDayProps) { {events.map((event) => { if (event.isGroup) { return ( - <CalendarEventGroupConnector + <CalendarEventGroup key={event.seriesId} {...event} onEventModalOpenToggle={onEventModalOpenToggle} @@ -62,11 +141,11 @@ function CalendarDay(props: CalendarDayProps) { } return ( - <CalendarEventConnector + <CalendarEvent key={event.id} {...event} episodeId={event.id} - series={event.series as Series} + seriesId={event.seriesId} airDateUtc={event.airDateUtc as string} onEventModalOpenToggle={onEventModalOpenToggle} /> diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js deleted file mode 100644 index 8fd6cc5a1..000000000 --- a/frontend/src/Calendar/Day/CalendarDayConnector.js +++ /dev/null @@ -1,91 +0,0 @@ -import _ from 'lodash'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import CalendarDay from './CalendarDay'; - -function sort(items) { - return _.sortBy(items, (item) => { - if (item.isGroup) { - return moment(item.events[0].airDateUtc).unix(); - } - - return moment(item.airDateUtc).unix(); - }); -} - -function createCalendarEventsConnector() { - return createSelector( - (state, { date }) => date, - (state) => state.calendar.items, - (state) => state.calendar.options.collapseMultipleEpisodes, - (date, items, collapseMultipleEpisodes) => { - const filtered = _.filter(items, (item) => { - return moment(date).isSame(moment(item.airDateUtc), 'day'); - }); - - if (!collapseMultipleEpisodes) { - return sort(filtered); - } - - const groupedObject = _.groupBy(filtered, (item) => `${item.seriesId}-${item.seasonNumber}`); - const grouped = []; - - Object.keys(groupedObject).forEach((key) => { - const events = groupedObject[key]; - - if (events.length === 1) { - grouped.push(events[0]); - } else { - grouped.push({ - isGroup: true, - seriesId: events[0].seriesId, - seasonNumber: events[0].seasonNumber, - episodeIds: events.map((event) => event.id), - events: _.sortBy(events, (item) => moment(item.airDateUtc).unix()) - }); - } - }); - - const sorted = sort(grouped); - - return sorted; - } - ); -} - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - createCalendarEventsConnector(), - (calendar, events) => { - return { - time: calendar.time, - view: calendar.view, - events - }; - } - ); -} - -class CalendarDayConnector extends Component { - - // - // Render - - render() { - return ( - <CalendarDay - {...this.props} - /> - ); - } -} - -CalendarDayConnector.propTypes = { - date: PropTypes.string.isRequired -}; - -export default connect(createMapStateToProps)(CalendarDayConnector); diff --git a/frontend/src/Calendar/Day/CalendarDays.js b/frontend/src/Calendar/Day/CalendarDays.js deleted file mode 100644 index f2bb4c8d4..000000000 --- a/frontend/src/Calendar/Day/CalendarDays.js +++ /dev/null @@ -1,164 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import * as calendarViews from 'Calendar/calendarViews'; -import isToday from 'Utilities/Date/isToday'; -import CalendarDayConnector from './CalendarDayConnector'; -import styles from './CalendarDays.css'; - -class CalendarDays extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._touchStart = null; - - this.state = { - todaysDate: moment().startOf('day').toISOString(), - isEventModalOpen: false - }; - - this.updateTimeoutId = null; - } - - // Lifecycle - - componentDidMount() { - const view = this.props.view; - - if (view === calendarViews.MONTH) { - this.scheduleUpdate(); - } - - window.addEventListener('touchstart', this.onTouchStart); - window.addEventListener('touchend', this.onTouchEnd); - window.addEventListener('touchcancel', this.onTouchCancel); - window.addEventListener('touchmove', this.onTouchMove); - } - - componentWillUnmount() { - this.clearUpdateTimeout(); - - window.removeEventListener('touchstart', this.onTouchStart); - window.removeEventListener('touchend', this.onTouchEnd); - window.removeEventListener('touchcancel', this.onTouchCancel); - window.removeEventListener('touchmove', this.onTouchMove); - } - - // - // Control - - scheduleUpdate = () => { - this.clearUpdateTimeout(); - const todaysDate = moment().startOf('day'); - const diff = moment().diff(todaysDate.clone().add(1, 'day')); - - this.setState({ todaysDate: todaysDate.toISOString() }); - - this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); - }; - - clearUpdateTimeout = () => { - if (this.updateTimeoutId) { - clearTimeout(this.updateTimeoutId); - } - }; - - // - // Listeners - - onEventModalOpenToggle = (isEventModalOpen) => { - this.setState({ isEventModalOpen }); - }; - - onTouchStart = (event) => { - const touches = event.touches; - const touchStart = touches[0].pageX; - - if (touches.length !== 1) { - return; - } - - if ( - touchStart < 50 || - this.props.isSidebarVisible || - this.state.isEventModalOpen - ) { - return; - } - - this._touchStart = touchStart; - }; - - onTouchEnd = (event) => { - const touches = event.changedTouches; - const currentTouch = touches[0].pageX; - - if (!this._touchStart) { - return; - } - - if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) { - this.props.onNavigatePrevious(); - } else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) { - this.props.onNavigateNext(); - } - - this._touchStart = null; - }; - - onTouchCancel = (event) => { - this._touchStart = null; - }; - - onTouchMove = (event) => { - if (!this._touchStart) { - return; - } - }; - - // - // Render - - render() { - const { - dates, - view - } = this.props; - - return ( - <div className={classNames( - styles.days, - styles[view] - )} - > - { - dates.map((date) => { - return ( - <CalendarDayConnector - key={date} - date={date} - isTodaysDate={isToday(date)} - onEventModalOpenToggle={this.onEventModalOpenToggle} - /> - ); - }) - } - </div> - ); - } -} - -CalendarDays.propTypes = { - dates: PropTypes.arrayOf(PropTypes.string).isRequired, - view: PropTypes.string.isRequired, - isSidebarVisible: PropTypes.bool.isRequired, - onNavigatePrevious: PropTypes.func.isRequired, - onNavigateNext: PropTypes.func.isRequired -}; - -export default CalendarDays; diff --git a/frontend/src/Calendar/Day/CalendarDays.tsx b/frontend/src/Calendar/Day/CalendarDays.tsx new file mode 100644 index 000000000..149dc1455 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDays.tsx @@ -0,0 +1,135 @@ +import classNames from 'classnames'; +import moment from 'moment'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as calendarViews from 'Calendar/calendarViews'; +import { + gotoCalendarNextRange, + gotoCalendarPreviousRange, +} from 'Store/Actions/calendarActions'; +import CalendarDay from './CalendarDay'; +import styles from './CalendarDays.css'; + +function CalendarDays() { + const dispatch = useDispatch(); + const { dates, view } = useSelector((state: AppState) => state.calendar); + const isSidebarVisible = useSelector( + (state: AppState) => state.app.isSidebarVisible + ); + + const updateTimeout = useRef<ReturnType<typeof setTimeout>>(); + const touchStart = useRef<number | null>(null); + const isEventModalOpen = useRef(false); + const [todaysDate, setTodaysDate] = useState( + moment().startOf('day').toISOString() + ); + + const handleEventModalOpenToggle = useCallback((isOpen: boolean) => { + isEventModalOpen.current = isOpen; + }, []); + + const scheduleUpdate = useCallback(() => { + clearTimeout(updateTimeout.current); + + const todaysDate = moment().startOf('day'); + const diff = moment().diff(todaysDate.clone().add(1, 'day')); + + setTodaysDate(todaysDate.toISOString()); + + updateTimeout.current = setTimeout(scheduleUpdate, diff); + }, []); + + const handleTouchStart = useCallback( + (event: TouchEvent) => { + const touches = event.touches; + const currentTouch = touches[0].pageX; + + if (touches.length !== 1) { + return; + } + + if (currentTouch < 50 || isSidebarVisible || isEventModalOpen.current) { + return; + } + + touchStart.current = currentTouch; + }, + [isSidebarVisible] + ); + + const handleTouchEnd = useCallback( + (event: TouchEvent) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; + + if (!touchStart.current) { + return; + } + + if ( + currentTouch > touchStart.current && + currentTouch - touchStart.current > 100 + ) { + dispatch(gotoCalendarPreviousRange()); + } else if ( + currentTouch < touchStart.current && + touchStart.current - currentTouch > 100 + ) { + dispatch(gotoCalendarNextRange()); + } + + touchStart.current = null; + }, + [dispatch] + ); + + const handleTouchCancel = useCallback(() => { + touchStart.current = null; + }, []); + + const handleTouchMove = useCallback(() => { + if (!touchStart.current) { + return; + } + }, []); + + useEffect(() => { + if (view === calendarViews.MONTH) { + scheduleUpdate(); + } + }, [view, scheduleUpdate]); + + useEffect(() => { + window.addEventListener('touchstart', handleTouchStart); + window.addEventListener('touchend', handleTouchEnd); + window.addEventListener('touchcancel', handleTouchCancel); + window.addEventListener('touchmove', handleTouchMove); + + return () => { + window.removeEventListener('touchstart', handleTouchStart); + window.removeEventListener('touchend', handleTouchEnd); + window.removeEventListener('touchcancel', handleTouchCancel); + window.removeEventListener('touchmove', handleTouchMove); + }; + }, [handleTouchStart, handleTouchEnd, handleTouchCancel, handleTouchMove]); + + return ( + <div + className={classNames(styles.days, styles[view as keyof typeof styles])} + > + {dates.map((date) => { + return ( + <CalendarDay + key={date} + date={date} + isTodaysDate={date === todaysDate} + onEventModalOpenToggle={handleEventModalOpenToggle} + /> + ); + })} + </div> + ); +} + +export default CalendarDays; diff --git a/frontend/src/Calendar/Day/CalendarDaysConnector.js b/frontend/src/Calendar/Day/CalendarDaysConnector.js deleted file mode 100644 index 0acce70b9..000000000 --- a/frontend/src/Calendar/Day/CalendarDaysConnector.js +++ /dev/null @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { gotoCalendarNextRange, gotoCalendarPreviousRange } from 'Store/Actions/calendarActions'; -import CalendarDays from './CalendarDays'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - (state) => state.app.isSidebarVisible, - (calendar, isSidebarVisible) => { - return { - dates: calendar.dates, - view: calendar.view, - isSidebarVisible - }; - } - ); -} - -const mapDispatchToProps = { - onNavigatePrevious: gotoCalendarPreviousRange, - onNavigateNext: gotoCalendarNextRange -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays); diff --git a/frontend/src/Calendar/Day/DayOfWeek.js b/frontend/src/Calendar/Day/DayOfWeek.js deleted file mode 100644 index 0f1d38f0b..000000000 --- a/frontend/src/Calendar/Day/DayOfWeek.js +++ /dev/null @@ -1,56 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import * as calendarViews from 'Calendar/calendarViews'; -import getRelativeDate from 'Utilities/Date/getRelativeDate'; -import styles from './DayOfWeek.css'; - -class DayOfWeek extends Component { - - // - // Render - - render() { - const { - date, - view, - isTodaysDate, - calendarWeekColumnHeader, - shortDateFormat, - showRelativeDates - } = this.props; - - const highlightToday = view !== calendarViews.MONTH && isTodaysDate; - const momentDate = moment(date); - let formatedDate = momentDate.format('dddd'); - - if (view === calendarViews.WEEK) { - formatedDate = momentDate.format(calendarWeekColumnHeader); - } else if (view === calendarViews.FORECAST) { - formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates }); - } - - return ( - <div className={classNames( - styles.dayOfWeek, - view === calendarViews.DAY && styles.isSingleDay, - highlightToday && styles.isToday - )} - > - {formatedDate} - </div> - ); - } -} - -DayOfWeek.propTypes = { - date: PropTypes.string.isRequired, - view: PropTypes.string.isRequired, - isTodaysDate: PropTypes.bool.isRequired, - calendarWeekColumnHeader: PropTypes.string.isRequired, - shortDateFormat: PropTypes.string.isRequired, - showRelativeDates: PropTypes.bool.isRequired -}; - -export default DayOfWeek; diff --git a/frontend/src/Calendar/Day/DayOfWeek.tsx b/frontend/src/Calendar/Day/DayOfWeek.tsx new file mode 100644 index 000000000..c8b493b7c --- /dev/null +++ b/frontend/src/Calendar/Day/DayOfWeek.tsx @@ -0,0 +1,54 @@ +import classNames from 'classnames'; +import moment from 'moment'; +import React from 'react'; +import * as calendarViews from 'Calendar/calendarViews'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import styles from './DayOfWeek.css'; + +interface DayOfWeekProps { + date: string; + view: string; + isTodaysDate: boolean; + calendarWeekColumnHeader: string; + shortDateFormat: string; + showRelativeDates: boolean; +} + +function DayOfWeek(props: DayOfWeekProps) { + const { + date, + view, + isTodaysDate, + calendarWeekColumnHeader, + shortDateFormat, + showRelativeDates, + } = props; + + const highlightToday = view !== calendarViews.MONTH && isTodaysDate; + const momentDate = moment(date); + let formatedDate = momentDate.format('dddd'); + + if (view === calendarViews.WEEK) { + formatedDate = momentDate.format(calendarWeekColumnHeader); + } else if (view === calendarViews.FORECAST) { + formatedDate = getRelativeDate({ + date, + shortDateFormat, + showRelativeDates, + }); + } + + return ( + <div + className={classNames( + styles.dayOfWeek, + view === calendarViews.DAY && styles.isSingleDay, + highlightToday && styles.isToday + )} + > + {formatedDate} + </div> + ); +} + +export default DayOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeek.js b/frontend/src/Calendar/Day/DaysOfWeek.js deleted file mode 100644 index 9f94b1079..000000000 --- a/frontend/src/Calendar/Day/DaysOfWeek.js +++ /dev/null @@ -1,97 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import * as calendarViews from 'Calendar/calendarViews'; -import DayOfWeek from './DayOfWeek'; -import styles from './DaysOfWeek.css'; - -class DaysOfWeek extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - todaysDate: moment().startOf('day').toISOString() - }; - - this.updateTimeoutId = null; - } - - // Lifecycle - - componentDidMount() { - const view = this.props.view; - - if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) { - this.scheduleUpdate(); - } - } - - componentWillUnmount() { - this.clearUpdateTimeout(); - } - - // - // Control - - scheduleUpdate = () => { - this.clearUpdateTimeout(); - const todaysDate = moment().startOf('day'); - const diff = todaysDate.clone().add(1, 'day').diff(moment()); - - this.setState({ - todaysDate: todaysDate.toISOString() - }); - - this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); - }; - - clearUpdateTimeout = () => { - if (this.updateTimeoutId) { - clearTimeout(this.updateTimeoutId); - } - }; - - // - // Render - - render() { - const { - dates, - view, - ...otherProps - } = this.props; - - if (view === calendarViews.AGENDA) { - return null; - } - - return ( - <div className={styles.daysOfWeek}> - { - dates.map((date) => { - return ( - <DayOfWeek - key={date} - date={date} - view={view} - isTodaysDate={date === this.state.todaysDate} - {...otherProps} - /> - ); - }) - } - </div> - ); - } -} - -DaysOfWeek.propTypes = { - dates: PropTypes.arrayOf(PropTypes.string), - view: PropTypes.string.isRequired -}; - -export default DaysOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeek.tsx b/frontend/src/Calendar/Day/DaysOfWeek.tsx new file mode 100644 index 000000000..64bc886cc --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeek.tsx @@ -0,0 +1,60 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as calendarViews from 'Calendar/calendarViews'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import DayOfWeek from './DayOfWeek'; +import styles from './DaysOfWeek.css'; + +function DaysOfWeek() { + const { dates, view } = useSelector((state: AppState) => state.calendar); + const { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } = + useSelector(createUISettingsSelector()); + + const updateTimeout = useRef<ReturnType<typeof setTimeout>>(); + const [todaysDate, setTodaysDate] = useState( + moment().startOf('day').toISOString() + ); + + const scheduleUpdate = useCallback(() => { + clearTimeout(updateTimeout.current); + + const todaysDate = moment().startOf('day'); + const diff = moment().diff(todaysDate.clone().add(1, 'day')); + + setTodaysDate(todaysDate.toISOString()); + + updateTimeout.current = setTimeout(scheduleUpdate, diff); + }, []); + + useEffect(() => { + if (view !== calendarViews.AGENDA && view !== calendarViews.MONTH) { + scheduleUpdate(); + } + }, [view, scheduleUpdate]); + + if (view === calendarViews.AGENDA) { + return null; + } + + return ( + <div className={styles.daysOfWeek}> + {dates.map((date) => { + return ( + <DayOfWeek + key={date} + date={date} + view={view} + isTodaysDate={date === todaysDate} + calendarWeekColumnHeader={calendarWeekColumnHeader} + shortDateFormat={shortDateFormat} + showRelativeDates={showRelativeDates} + /> + ); + })} + </div> + ); +} + +export default DaysOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeekConnector.js b/frontend/src/Calendar/Day/DaysOfWeekConnector.js deleted file mode 100644 index 7f5cdef19..000000000 --- a/frontend/src/Calendar/Day/DaysOfWeekConnector.js +++ /dev/null @@ -1,22 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import DaysOfWeek from './DaysOfWeek'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - createUISettingsSelector(), - (calendar, UiSettings) => { - return { - dates: calendar.dates.slice(0, 7), - view: calendar.view, - calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader, - shortDateFormat: UiSettings.shortDateFormat, - showRelativeDates: UiSettings.showRelativeDates - }; - } - ); -} - -export default connect(createMapStateToProps)(DaysOfWeek); diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js deleted file mode 100644 index 1f9d59b2b..000000000 --- a/frontend/src/Calendar/Events/CalendarEvent.js +++ /dev/null @@ -1,267 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import getStatusStyle from 'Calendar/getStatusStyle'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; -import episodeEntities from 'Episode/episodeEntities'; -import getFinaleTypeName from 'Episode/getFinaleTypeName'; -import { icons, kinds } from 'Helpers/Props'; -import formatTime from 'Utilities/Date/formatTime'; -import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; -import CalendarEventQueueDetails from './CalendarEventQueueDetails'; -import styles from './CalendarEvent.css'; - -class CalendarEvent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onPress = () => { - this.setState({ isDetailsModalOpen: true }, () => { - this.props.onEventModalOpenToggle(true); - }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }, () => { - this.props.onEventModalOpenToggle(false); - }); - }; - - // - // Render - - render() { - const { - id, - series, - episodeFile, - title, - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - airDateUtc, - monitored, - unverifiedSceneNumbering, - finaleType, - hasFile, - grabbed, - queueItem, - showEpisodeInformation, - showFinaleIcon, - showSpecialIcon, - showCutoffUnmetIcon, - fullColorEvents, - timeFormat, - colorImpairedMode - } = this.props; - - if (!series) { - return null; - } - - const startTime = moment(airDateUtc); - const endTime = moment(airDateUtc).add(series.runtime, 'minutes'); - const isDownloading = !!(queueItem || grabbed); - const isMonitored = series.monitored && monitored; - const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored); - const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; - - return ( - <div - className={classNames( - styles.event, - styles[statusStyle], - colorImpairedMode && 'colorImpaired', - fullColorEvents && 'fullColor' - )} - > - <Link - className={styles.underlay} - onPress={this.onPress} - /> - - <div className={styles.overlay} > - <div className={styles.info}> - <div className={styles.seriesTitle}> - {series.title} - </div> - - <div - className={classNames( - styles.statusContainer, - fullColorEvents && 'fullColor' - )} - > - { - missingAbsoluteNumber ? - <Icon - className={styles.statusIcon} - name={icons.WARNING} - title={translate('EpisodeMissingAbsoluteNumber')} - /> : - null - } - - { - unverifiedSceneNumbering && !missingAbsoluteNumber ? - <Icon - className={styles.statusIcon} - name={icons.WARNING} - title={translate('SceneNumberNotVerified')} - /> : - null - } - - { - queueItem ? - <span className={styles.statusIcon}> - <CalendarEventQueueDetails - {...queueItem} - fullColorEvents={fullColorEvents} - /> - </span> : - null - } - - { - !queueItem && grabbed ? - <Icon - className={styles.statusIcon} - name={icons.DOWNLOADING} - title={translate('EpisodeIsDownloading')} - /> : - null - } - - { - showCutoffUnmetIcon && - !!episodeFile && - episodeFile.qualityCutoffNotMet ? - <Icon - className={styles.statusIcon} - name={icons.EPISODE_FILE} - kind={kinds.WARNING} - title={translate('QualityCutoffNotMet')} - /> : - null - } - - { - episodeNumber === 1 && seasonNumber > 0 ? - <Icon - className={styles.statusIcon} - name={icons.PREMIERE} - kind={kinds.INFO} - title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')} - /> : - null - } - - { - showFinaleIcon && - finaleType ? - <Icon - className={styles.statusIcon} - name={finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON} - kind={finaleType === 'series' ? kinds.DANGER : kinds.WARNING} - title={getFinaleTypeName(finaleType)} - /> : - null - } - - { - showSpecialIcon && - (episodeNumber === 0 || seasonNumber === 0) ? - <Icon - className={styles.statusIcon} - name={icons.INFO} - kind={kinds.PINK} - title={translate('Special')} - /> : - null - } - </div> - </div> - - { - showEpisodeInformation ? - <div className={styles.episodeInfo}> - <div className={styles.episodeTitle}> - {title} - </div> - - <div> - {seasonNumber}x{padNumber(episodeNumber, 2)} - - { - series.seriesType === 'anime' && absoluteEpisodeNumber ? - <span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span> : null - } - </div> - </div> : - null - } - - <div className={styles.airTime}> - {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} - </div> - </div> - - <EpisodeDetailsModal - isOpen={this.state.isDetailsModalOpen} - episodeId={id} - episodeEntity={episodeEntities.CALENDAR} - seriesId={series.id} - episodeTitle={title} - showOpenSeriesButton={true} - onModalClose={this.onDetailsModalClose} - /> - </div> - ); - } -} - -CalendarEvent.propTypes = { - id: PropTypes.number.isRequired, - episodeId: PropTypes.number.isRequired, - series: PropTypes.object.isRequired, - episodeFile: PropTypes.object, - title: PropTypes.string.isRequired, - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - airDateUtc: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - unverifiedSceneNumbering: PropTypes.bool, - finaleType: PropTypes.string, - hasFile: PropTypes.bool.isRequired, - grabbed: PropTypes.bool, - queueItem: PropTypes.object, - // These props come from the connector, not marked as required to appease TS for now. - showEpisodeInformation: PropTypes.bool, - showFinaleIcon: PropTypes.bool, - showSpecialIcon: PropTypes.bool, - showCutoffUnmetIcon: PropTypes.bool, - fullColorEvents: PropTypes.bool, - timeFormat: PropTypes.string, - colorImpairedMode: PropTypes.bool, - onEventModalOpenToggle: PropTypes.func -}; - -export default CalendarEvent; diff --git a/frontend/src/Calendar/Events/CalendarEvent.tsx b/frontend/src/Calendar/Events/CalendarEvent.tsx new file mode 100644 index 000000000..83452a5ce --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEvent.tsx @@ -0,0 +1,240 @@ +import classNames from 'classnames'; +import moment from 'moment'; +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; +import episodeEntities from 'Episode/episodeEntities'; +import getFinaleTypeName from 'Episode/getFinaleTypeName'; +import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; +import { icons, kinds } from 'Helpers/Props'; +import useSeries from 'Series/useSeries'; +import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import translate from 'Utilities/String/translate'; +import CalendarEventQueueDetails from './CalendarEventQueueDetails'; +import styles from './CalendarEvent.css'; + +interface CalendarEventProps { + id: number; + episodeId: number; + seriesId: number; + episodeFileId?: number; + title: string; + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + airDateUtc: string; + monitored: boolean; + unverifiedSceneNumbering?: boolean; + finaleType?: string; + hasFile: boolean; + grabbed?: boolean; + onEventModalOpenToggle: (isOpen: boolean) => void; +} + +function CalendarEvent(props: CalendarEventProps) { + const { + id, + seriesId, + episodeFileId, + title, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDateUtc, + monitored, + unverifiedSceneNumbering, + finaleType, + hasFile, + grabbed, + onEventModalOpenToggle, + } = props; + + const series = useSeries(seriesId); + const episodeFile = useEpisodeFile(episodeFileId); + const queueItem = useSelector(createQueueItemSelectorForHook(id)); + + const { timeFormat, enableColorImpairedMode } = useSelector( + createUISettingsSelector() + ); + + const { + showEpisodeInformation, + showFinaleIcon, + showSpecialIcon, + showCutoffUnmetIcon, + fullColorEvents, + } = useSelector((state: AppState) => state.calendar.options); + + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const handleDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(true); + onEventModalOpenToggle(true); + }, [onEventModalOpenToggle]); + + const handlePress = useCallback(() => { + setIsDetailsModalOpen(false); + onEventModalOpenToggle(false); + }, [onEventModalOpenToggle]); + + if (!series) { + return null; + } + + const startTime = moment(airDateUtc); + const endTime = moment(airDateUtc).add(series.runtime, 'minutes'); + const isDownloading = !!(queueItem || grabbed); + const isMonitored = series.monitored && monitored; + const statusStyle = getStatusStyle( + hasFile, + isDownloading, + startTime, + endTime, + isMonitored + ); + const missingAbsoluteNumber = + series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; + + return ( + <div + className={classNames( + styles.event, + styles[statusStyle], + enableColorImpairedMode && 'colorImpaired', + fullColorEvents && 'fullColor' + )} + > + <Link className={styles.underlay} onPress={handlePress} /> + + <div className={styles.overlay}> + <div className={styles.info}> + <div className={styles.seriesTitle}>{series.title}</div> + + <div + className={classNames( + styles.statusContainer, + fullColorEvents && 'fullColor' + )} + > + {missingAbsoluteNumber ? ( + <Icon + className={styles.statusIcon} + name={icons.WARNING} + title={translate('EpisodeMissingAbsoluteNumber')} + /> + ) : null} + + {unverifiedSceneNumbering && !missingAbsoluteNumber ? ( + <Icon + className={styles.statusIcon} + name={icons.WARNING} + title={translate('SceneNumberNotVerified')} + /> + ) : null} + + {queueItem ? ( + <span className={styles.statusIcon}> + <CalendarEventQueueDetails {...queueItem} /> + </span> + ) : null} + + {!queueItem && grabbed ? ( + <Icon + className={styles.statusIcon} + name={icons.DOWNLOADING} + title={translate('EpisodeIsDownloading')} + /> + ) : null} + + {showCutoffUnmetIcon && + !!episodeFile && + episodeFile.qualityCutoffNotMet ? ( + <Icon + className={styles.statusIcon} + name={icons.EPISODE_FILE} + kind={kinds.WARNING} + title={translate('QualityCutoffNotMet')} + /> + ) : null} + + {episodeNumber === 1 && seasonNumber > 0 ? ( + <Icon + className={styles.statusIcon} + name={icons.PREMIERE} + kind={kinds.INFO} + title={ + seasonNumber === 1 + ? translate('SeriesPremiere') + : translate('SeasonPremiere') + } + /> + ) : null} + + {showFinaleIcon && finaleType ? ( + <Icon + className={styles.statusIcon} + name={ + finaleType === 'series' + ? icons.FINALE_SERIES + : icons.FINALE_SEASON + } + kind={finaleType === 'series' ? kinds.DANGER : kinds.WARNING} + title={getFinaleTypeName(finaleType)} + /> + ) : null} + + {showSpecialIcon && (episodeNumber === 0 || seasonNumber === 0) ? ( + <Icon + className={styles.statusIcon} + name={icons.INFO} + kind={kinds.PINK} + title={translate('Special')} + /> + ) : null} + </div> + </div> + + {showEpisodeInformation ? ( + <div className={styles.episodeInfo}> + <div className={styles.episodeTitle}>{title}</div> + + <div> + {seasonNumber}x{padNumber(episodeNumber, 2)} + {series.seriesType === 'anime' && absoluteEpisodeNumber ? ( + <span className={styles.absoluteEpisodeNumber}> + ({absoluteEpisodeNumber}) + </span> + ) : null} + </div> + </div> + ) : null} + + <div className={styles.airTime}> + {formatTime(airDateUtc, timeFormat)} -{' '} + {formatTime(endTime.toISOString(), timeFormat, { + includeMinuteZero: true, + })} + </div> + </div> + + <EpisodeDetailsModal + isOpen={isDetailsModalOpen} + episodeId={id} + episodeEntity={episodeEntities.CALENDAR} + seriesId={series.id} + episodeTitle={title} + showOpenSeriesButton={true} + onModalClose={handleDetailsModalClose} + /> + </div> + ); +} + +export default CalendarEvent; diff --git a/frontend/src/Calendar/Events/CalendarEventConnector.js b/frontend/src/Calendar/Events/CalendarEventConnector.js deleted file mode 100644 index e1ac2096d..000000000 --- a/frontend/src/Calendar/Events/CalendarEventConnector.js +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import CalendarEvent from './CalendarEvent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.options, - createSeriesSelector(), - createEpisodeFileSelector(), - createQueueItemSelector(), - createUISettingsSelector(), - (calendarOptions, series, episodeFile, queueItem, uiSettings) => { - return { - series, - episodeFile, - queueItem, - ...calendarOptions, - timeFormat: uiSettings.timeFormat, - colorImpairedMode: uiSettings.enableColorImpairedMode - }; - } - ); -} - -export default connect(createMapStateToProps)(CalendarEvent); diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.js b/frontend/src/Calendar/Events/CalendarEventGroup.js deleted file mode 100644 index 2bec49df2..000000000 --- a/frontend/src/Calendar/Events/CalendarEventGroup.js +++ /dev/null @@ -1,259 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector'; -import getStatusStyle from 'Calendar/getStatusStyle'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import getFinaleTypeName from 'Episode/getFinaleTypeName'; -import { icons, kinds } from 'Helpers/Props'; -import formatTime from 'Utilities/Date/formatTime'; -import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; -import styles from './CalendarEventGroup.css'; - -function getEventsInfo(series, events) { - let files = 0; - let queued = 0; - let monitored = 0; - let absoluteEpisodeNumbers = 0; - - events.forEach((event) => { - if (event.episodeFileId) { - files++; - } - - if (event.queued) { - queued++; - } - - if (series.monitored && event.monitored) { - monitored++; - } - - if (event.absoluteEpisodeNumber) { - absoluteEpisodeNumbers++; - } - }); - - return { - allDownloaded: files === events.length, - anyQueued: queued > 0, - anyMonitored: monitored > 0, - allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length - }; -} - -class CalendarEventGroup extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isExpanded: false - }; - } - - // - // Listeners - - onExpandPress = () => { - this.setState({ isExpanded: !this.state.isExpanded }); - }; - - // - // Render - - render() { - const { - series, - events, - isDownloading, - showEpisodeInformation, - showFinaleIcon, - timeFormat, - fullColorEvents, - colorImpairedMode, - onEventModalOpenToggle - } = this.props; - - const { isExpanded } = this.state; - const { - allDownloaded, - anyQueued, - anyMonitored, - allAbsoluteEpisodeNumbers - } = getEventsInfo(series, events); - const anyDownloading = isDownloading || anyQueued; - const firstEpisode = events[0]; - const lastEpisode = events[events.length -1]; - const airDateUtc = firstEpisode.airDateUtc; - const startTime = moment(airDateUtc); - const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes'); - const seasonNumber = firstEpisode.seasonNumber; - const statusStyle = getStatusStyle(allDownloaded, anyDownloading, startTime, endTime, anyMonitored); - const isMissingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !allAbsoluteEpisodeNumbers; - - if (isExpanded) { - return ( - <div> - { - events.map((event) => { - if (event.isGroup) { - return null; - } - - return ( - <CalendarEventConnector - key={event.id} - episodeId={event.id} - {...event} - onEventModalOpenToggle={onEventModalOpenToggle} - /> - ); - }) - } - - <Link - className={styles.collapseContainer} - component="div" - onPress={this.onExpandPress} - > - <Icon - name={icons.COLLAPSE} - /> - </Link> - </div> - ); - } - - return ( - <div - className={classNames( - styles.eventGroup, - styles[statusStyle], - colorImpairedMode && 'colorImpaired', - fullColorEvents && 'fullColor' - )} - > - <div className={styles.info}> - <div className={styles.seriesTitle}> - {series.title} - </div> - - <div - className={classNames( - styles.statusContainer, - fullColorEvents && 'fullColor' - )} - > - { - isMissingAbsoluteNumber && - <Icon - containerClassName={styles.statusIcon} - name={icons.WARNING} - title={translate('EpisodeMissingAbsoluteNumber')} - /> - } - - { - anyDownloading && - <Icon - containerClassName={styles.statusIcon} - name={icons.DOWNLOADING} - title={translate('AnEpisodeIsDownloading')} - /> - } - - { - firstEpisode.episodeNumber === 1 && seasonNumber > 0 && - <Icon - containerClassName={styles.statusIcon} - name={icons.PREMIERE} - kind={kinds.INFO} - title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')} - /> - } - - { - showFinaleIcon && - lastEpisode.finaleType ? - <Icon - containerClassName={styles.statusIcon} - name={lastEpisode.finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON} - kind={lastEpisode.finaleType === 'series' ? kinds.DANGER : kinds.WARNING} - title={getFinaleTypeName(lastEpisode.finaleType)} - /> : null - } - </div> - </div> - - <div className={styles.airingInfo}> - <div className={styles.airTime}> - {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} - </div> - - { - showEpisodeInformation ? - <div className={styles.episodeInfo}> - {seasonNumber}x{padNumber(firstEpisode.episodeNumber, 2)}-{padNumber(lastEpisode.episodeNumber, 2)} - - { - series.seriesType === 'anime' && - firstEpisode.absoluteEpisodeNumber && - lastEpisode.absoluteEpisodeNumber && - <span className={styles.absoluteEpisodeNumber}> - ({firstEpisode.absoluteEpisodeNumber}-{lastEpisode.absoluteEpisodeNumber}) - </span> - } - </div> : - <Link - className={styles.expandContainerInline} - component="div" - onPress={this.onExpandPress} - > - <Icon - name={icons.EXPAND} - /> - </Link> - } - </div> - - { - showEpisodeInformation ? - <Link - className={styles.expandContainer} - component="div" - onPress={this.onExpandPress} - > -   - <Icon - name={icons.EXPAND} - /> -   - </Link> : - null - } - </div> - ); - } -} - -CalendarEventGroup.propTypes = { - // Most of these props come from the connector and are required, but TS is confused. - series: PropTypes.object, - events: PropTypes.arrayOf(PropTypes.object).isRequired, - isDownloading: PropTypes.bool, - showEpisodeInformation: PropTypes.bool, - showFinaleIcon: PropTypes.bool, - fullColorEvents: PropTypes.bool, - timeFormat: PropTypes.string, - colorImpairedMode: PropTypes.bool, - onEventModalOpenToggle: PropTypes.func.isRequired -}; - -export default CalendarEventGroup; diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.tsx b/frontend/src/Calendar/Events/CalendarEventGroup.tsx new file mode 100644 index 000000000..1ee981cfd --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventGroup.tsx @@ -0,0 +1,253 @@ +import classNames from 'classnames'; +import moment from 'moment'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import getFinaleTypeName from 'Episode/getFinaleTypeName'; +import { icons, kinds } from 'Helpers/Props'; +import useSeries from 'Series/useSeries'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { CalendarItem } from 'typings/Calendar'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import translate from 'Utilities/String/translate'; +import CalendarEvent from './CalendarEvent'; +import styles from './CalendarEventGroup.css'; + +function createIsDownloadingSelector(episodeIds: number[]) { + return createSelector( + (state: AppState) => state.queue.details, + (details) => { + return details.items.some((item) => { + return !!(item.episodeId && episodeIds.includes(item.episodeId)); + }); + } + ); +} + +interface CalendarEventGroupProps { + episodeIds: number[]; + seriesId: number; + events: CalendarItem[]; + onEventModalOpenToggle: (isOpen: boolean) => void; +} + +function CalendarEventGroup({ + episodeIds, + seriesId, + events, + onEventModalOpenToggle, +}: CalendarEventGroupProps) { + const isDownloading = useSelector(createIsDownloadingSelector(episodeIds)); + const series = useSeries(seriesId)!; + + const { timeFormat, enableColorImpairedMode } = useSelector( + createUISettingsSelector() + ); + + const { showEpisodeInformation, showFinaleIcon, fullColorEvents } = + useSelector((state: AppState) => state.calendar.options); + + const [isExpanded, setIsExpanded] = useState(false); + + const firstEpisode = events[0]; + const lastEpisode = events[events.length - 1]; + const airDateUtc = firstEpisode.airDateUtc; + const startTime = moment(airDateUtc); + const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes'); + const seasonNumber = firstEpisode.seasonNumber; + + const { allDownloaded, anyQueued, anyMonitored, allAbsoluteEpisodeNumbers } = + useMemo(() => { + let files = 0; + let queued = 0; + let monitored = 0; + let absoluteEpisodeNumbers = 0; + + events.forEach((event) => { + if (event.episodeFileId) { + files++; + } + + if (event.queued) { + queued++; + } + + if (series.monitored && event.monitored) { + monitored++; + } + + if (event.absoluteEpisodeNumber) { + absoluteEpisodeNumbers++; + } + }); + + return { + allDownloaded: files === events.length, + anyQueued: queued > 0, + anyMonitored: monitored > 0, + allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length, + }; + }, [series, events]); + + const anyDownloading = isDownloading || anyQueued; + + const statusStyle = getStatusStyle( + allDownloaded, + anyDownloading, + startTime, + endTime, + anyMonitored + ); + const isMissingAbsoluteNumber = + series.seriesType === 'anime' && + seasonNumber > 0 && + !allAbsoluteEpisodeNumbers; + + const handleExpandPress = useCallback(() => { + setIsExpanded((state) => !state); + }, []); + + if (isExpanded) { + return ( + <div> + {events.map((event) => { + return ( + <CalendarEvent + key={event.id} + episodeId={event.id} + {...event} + onEventModalOpenToggle={onEventModalOpenToggle} + /> + ); + })} + + <Link + className={styles.collapseContainer} + component="div" + onPress={handleExpandPress} + > + <Icon name={icons.COLLAPSE} /> + </Link> + </div> + ); + } + + return ( + <div + className={classNames( + styles.eventGroup, + styles[statusStyle], + enableColorImpairedMode && 'colorImpaired', + fullColorEvents && 'fullColor' + )} + > + <div className={styles.info}> + <div className={styles.seriesTitle}>{series.title}</div> + + <div + className={classNames( + styles.statusContainer, + fullColorEvents && 'fullColor' + )} + > + {isMissingAbsoluteNumber ? ( + <Icon + containerClassName={styles.statusIcon} + name={icons.WARNING} + title={translate('EpisodeMissingAbsoluteNumber')} + /> + ) : null} + + {anyDownloading ? ( + <Icon + containerClassName={styles.statusIcon} + name={icons.DOWNLOADING} + title={translate('AnEpisodeIsDownloading')} + /> + ) : null} + + {firstEpisode.episodeNumber === 1 && seasonNumber > 0 ? ( + <Icon + containerClassName={styles.statusIcon} + name={icons.PREMIERE} + kind={kinds.INFO} + title={ + seasonNumber === 1 + ? translate('SeriesPremiere') + : translate('SeasonPremiere') + } + /> + ) : null} + + {showFinaleIcon && lastEpisode.finaleType ? ( + <Icon + containerClassName={styles.statusIcon} + name={ + lastEpisode.finaleType === 'series' + ? icons.FINALE_SERIES + : icons.FINALE_SEASON + } + kind={ + lastEpisode.finaleType === 'series' + ? kinds.DANGER + : kinds.WARNING + } + title={getFinaleTypeName(lastEpisode.finaleType)} + /> + ) : null} + </div> + </div> + + <div className={styles.airingInfo}> + <div className={styles.airTime}> + {formatTime(airDateUtc, timeFormat)} -{' '} + {formatTime(endTime.toISOString(), timeFormat, { + includeMinuteZero: true, + })} + </div> + + {showEpisodeInformation ? ( + <div className={styles.episodeInfo}> + {seasonNumber}x{padNumber(firstEpisode.episodeNumber, 2)}- + {padNumber(lastEpisode.episodeNumber, 2)} + {series.seriesType === 'anime' && + firstEpisode.absoluteEpisodeNumber && + lastEpisode.absoluteEpisodeNumber ? ( + <span className={styles.absoluteEpisodeNumber}> + ({firstEpisode.absoluteEpisodeNumber}- + {lastEpisode.absoluteEpisodeNumber}) + </span> + ) : null} + </div> + ) : ( + <Link + className={styles.expandContainerInline} + component="div" + onPress={handleExpandPress} + > + <Icon name={icons.EXPAND} /> + </Link> + )} + </div> + + {showEpisodeInformation ? ( + <Link + className={styles.expandContainer} + component="div" + onPress={handleExpandPress} + > +   + <Icon name={icons.EXPAND} /> +   + </Link> + ) : null} + </div> + ); +} + +export default CalendarEventGroup; diff --git a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js deleted file mode 100644 index dca227a85..000000000 --- a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js +++ /dev/null @@ -1,37 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import CalendarEventGroup from './CalendarEventGroup'; - -function createIsDownloadingSelector() { - return createSelector( - (state, { episodeIds }) => episodeIds, - (state) => state.queue.details, - (episodeIds, details) => { - return details.items.some((item) => { - return !!(item.episodeId && episodeIds.includes(item.episodeId)); - }); - } - ); -} - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.options, - createSeriesSelector(), - createIsDownloadingSelector(), - createUISettingsSelector(), - (calendarOptions, series, isDownloading, uiSettings) => { - return { - series, - isDownloading, - ...calendarOptions, - timeFormat: uiSettings.timeFormat, - colorImpairedMode: uiSettings.enableColorImpairedMode - }; - } - ); -} - -export default connect(createMapStateToProps)(CalendarEventGroup); diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js deleted file mode 100644 index db26eb1d2..000000000 --- a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import QueueDetails from 'Activity/Queue/QueueDetails'; -import CircularProgressBar from 'Components/CircularProgressBar'; - -function CalendarEventQueueDetails(props) { - const { - title, - size, - sizeleft, - estimatedCompletionTime, - status, - trackedDownloadState, - trackedDownloadStatus, - statusMessages, - errorMessage - } = props; - - const progress = size ? (100 - sizeleft / size * 100) : 0; - - return ( - <QueueDetails - title={title} - size={size} - sizeleft={sizeleft} - estimatedCompletionTime={estimatedCompletionTime} - status={status} - trackedDownloadState={trackedDownloadState} - trackedDownloadStatus={trackedDownloadStatus} - statusMessages={statusMessages} - errorMessage={errorMessage} - progressBar={ - <CircularProgressBar - progress={progress} - size={20} - strokeWidth={2} - strokeColor={'#7a43b6'} - /> - } - /> - ); -} - -CalendarEventQueueDetails.propTypes = { - title: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - sizeleft: PropTypes.number.isRequired, - estimatedCompletionTime: PropTypes.string, - status: PropTypes.string.isRequired, - trackedDownloadState: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string.isRequired, - statusMessages: PropTypes.arrayOf(PropTypes.object), - errorMessage: PropTypes.string -}; - -export default CalendarEventQueueDetails; diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx b/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx new file mode 100644 index 000000000..2372bc78e --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import QueueDetails from 'Activity/Queue/QueueDetails'; +import CircularProgressBar from 'Components/CircularProgressBar'; +import { + QueueTrackedDownloadState, + QueueTrackedDownloadStatus, + StatusMessage, +} from 'typings/Queue'; + +interface CalendarEventQueueDetailsProps { + title: string; + size: number; + sizeleft: number; + estimatedCompletionTime?: string; + status: string; + trackedDownloadState: QueueTrackedDownloadState; + trackedDownloadStatus: QueueTrackedDownloadStatus; + statusMessages?: StatusMessage[]; + errorMessage?: string; +} + +function CalendarEventQueueDetails({ + title, + size, + sizeleft, + estimatedCompletionTime, + status, + trackedDownloadState, + trackedDownloadStatus, + statusMessages, + errorMessage, +}: CalendarEventQueueDetailsProps) { + const progress = size ? 100 - (sizeleft / size) * 100 : 0; + + return ( + <QueueDetails + title={title} + size={size} + sizeleft={sizeleft} + estimatedCompletionTime={estimatedCompletionTime} + status={status} + trackedDownloadState={trackedDownloadState} + trackedDownloadStatus={trackedDownloadStatus} + statusMessages={statusMessages} + errorMessage={errorMessage} + progressBar={ + <CircularProgressBar + progress={progress} + size={20} + strokeWidth={2} + strokeColor="#7a43b6" + /> + } + /> + ); +} + +export default CalendarEventQueueDetails; diff --git a/frontend/src/Calendar/Header/CalendarHeader.js b/frontend/src/Calendar/Header/CalendarHeader.js deleted file mode 100644 index 4555fc63b..000000000 --- a/frontend/src/Calendar/Header/CalendarHeader.js +++ /dev/null @@ -1,268 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import * as calendarViews from 'Calendar/calendarViews'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Menu from 'Components/Menu/Menu'; -import MenuButton from 'Components/Menu/MenuButton'; -import MenuContent from 'Components/Menu/MenuContent'; -import ViewMenuItem from 'Components/Menu/ViewMenuItem'; -import { align, icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import CalendarHeaderViewButton from './CalendarHeaderViewButton'; -import styles from './CalendarHeader.css'; - -function getTitle(time, start, end, view, longDateFormat) { - const timeMoment = moment(time); - const startMoment = moment(start); - const endMoment = moment(end); - - if (view === 'day') { - return timeMoment.format(longDateFormat); - } else if (view === 'month') { - return timeMoment.format('MMMM YYYY'); - } else if (view === 'agenda') { - return translate('Agenda'); - } - - let startFormat = 'MMM D YYYY'; - let endFormat = 'MMM D YYYY'; - - if (startMoment.isSame(endMoment, 'month')) { - startFormat = 'MMM D'; - endFormat = 'D YYYY'; - } else if (startMoment.isSame(endMoment, 'year')) { - startFormat = 'MMM D'; - endFormat = 'MMM D YYYY'; - } - - return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`; -} - -// TODO Convert to a stateful Component so we can track view internally when changed - -class CalendarHeader extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - view: props.view - }; - } - - componentDidUpdate(prevProps) { - const view = this.props.view; - - if (prevProps.view !== view) { - this.setState({ view }); - } - } - - // - // Listeners - - onViewChange = (view) => { - this.setState({ view }, () => { - this.props.onViewChange(view); - }); - }; - - // - // Render - - render() { - const { - isFetching, - time, - start, - end, - longDateFormat, - isSmallScreen, - collapseViewButtons, - onTodayPress, - onPreviousPress, - onNextPress - } = this.props; - - const view = this.state.view; - - const title = getTitle(time, start, end, view, longDateFormat); - - return ( - <div> - { - isSmallScreen && - <div className={styles.titleMobile}> - {title} - </div> - } - - <div className={styles.header}> - <div className={styles.navigationButtons}> - <Button - buttonGroupPosition={align.LEFT} - isDisabled={view === calendarViews.AGENDA} - onPress={onPreviousPress} - > - <Icon name={icons.PAGE_PREVIOUS} /> - </Button> - - <Button - buttonGroupPosition={align.RIGHT} - isDisabled={view === calendarViews.AGENDA} - onPress={onNextPress} - > - <Icon name={icons.PAGE_NEXT} /> - </Button> - - <Button - className={styles.todayButton} - isDisabled={view === calendarViews.AGENDA} - onPress={onTodayPress} - > - {translate('Today')} - </Button> - </div> - - { - !isSmallScreen && - <div className={styles.titleDesktop}> - {title} - </div> - } - - <div className={styles.viewButtonsContainer}> - { - isFetching && - <LoadingIndicator - className={styles.loading} - size={20} - /> - } - - { - collapseViewButtons ? - <Menu - className={styles.viewMenu} - alignMenu={align.RIGHT} - > - <MenuButton> - <Icon - name={icons.VIEW} - size={22} - /> - </MenuButton> - - <MenuContent> - { - isSmallScreen ? - null : - <ViewMenuItem - name={calendarViews.MONTH} - selectedView={view} - onPress={this.onViewChange} - > - {translate('Month')} - </ViewMenuItem> - } - - <ViewMenuItem - name={calendarViews.WEEK} - selectedView={view} - onPress={this.onViewChange} - > - {translate('Week')} - </ViewMenuItem> - - <ViewMenuItem - name={calendarViews.FORECAST} - selectedView={view} - onPress={this.onViewChange} - > - {translate('Forecast')} - </ViewMenuItem> - - <ViewMenuItem - name={calendarViews.DAY} - selectedView={view} - onPress={this.onViewChange} - > - {translate('Day')} - </ViewMenuItem> - - <ViewMenuItem - name={calendarViews.AGENDA} - selectedView={view} - onPress={this.onViewChange} - > - {translate('Agenda')} - </ViewMenuItem> - </MenuContent> - </Menu> : - - <div className={styles.viewButtons}> - <CalendarHeaderViewButton - view={calendarViews.MONTH} - selectedView={view} - buttonGroupPosition={align.LEFT} - onPress={this.onViewChange} - /> - - <CalendarHeaderViewButton - view={calendarViews.WEEK} - selectedView={view} - buttonGroupPosition={align.CENTER} - onPress={this.onViewChange} - /> - - <CalendarHeaderViewButton - view={calendarViews.FORECAST} - selectedView={view} - buttonGroupPosition={align.CENTER} - onPress={this.onViewChange} - /> - - <CalendarHeaderViewButton - view={calendarViews.DAY} - selectedView={view} - buttonGroupPosition={align.CENTER} - onPress={this.onViewChange} - /> - - <CalendarHeaderViewButton - view={calendarViews.AGENDA} - selectedView={view} - buttonGroupPosition={align.RIGHT} - onPress={this.onViewChange} - /> - </div> - } - </div> - </div> - </div> - ); - } -} - -CalendarHeader.propTypes = { - isFetching: PropTypes.bool.isRequired, - time: PropTypes.string.isRequired, - start: PropTypes.string.isRequired, - end: PropTypes.string.isRequired, - view: PropTypes.oneOf(calendarViews.all).isRequired, - isSmallScreen: PropTypes.bool.isRequired, - collapseViewButtons: PropTypes.bool.isRequired, - longDateFormat: PropTypes.string.isRequired, - onViewChange: PropTypes.func.isRequired, - onTodayPress: PropTypes.func.isRequired, - onPreviousPress: PropTypes.func.isRequired, - onNextPress: PropTypes.func.isRequired -}; - -export default CalendarHeader; diff --git a/frontend/src/Calendar/Header/CalendarHeader.tsx b/frontend/src/Calendar/Header/CalendarHeader.tsx new file mode 100644 index 000000000..2faaca25e --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeader.tsx @@ -0,0 +1,221 @@ +import moment from 'moment'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { CalendarView } from 'Calendar/calendarViews'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; +import { align, icons } from 'Helpers/Props'; +import { + gotoCalendarNextRange, + gotoCalendarPreviousRange, + gotoCalendarToday, + setCalendarView, +} from 'Store/Actions/calendarActions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import translate from 'Utilities/String/translate'; +import CalendarHeaderViewButton from './CalendarHeaderViewButton'; +import styles from './CalendarHeader.css'; + +function CalendarHeader() { + const dispatch = useDispatch(); + + const { isFetching, view, time, start, end } = useSelector( + (state: AppState) => state.calendar + ); + + const { isSmallScreen, isLargeScreen } = useSelector( + createDimensionsSelector() + ); + + const { longDateFormat } = useSelector(createUISettingsSelector()); + + const handleViewChange = useCallback( + (newView: CalendarView) => { + dispatch(setCalendarView({ view: newView })); + }, + [dispatch] + ); + + const handleTodayPress = useCallback(() => { + dispatch(gotoCalendarToday()); + }, [dispatch]); + + const handlePreviousPress = useCallback(() => { + dispatch(gotoCalendarPreviousRange()); + }, [dispatch]); + + const handleNextPress = useCallback(() => { + dispatch(gotoCalendarNextRange()); + }, [dispatch]); + + const title = useMemo(() => { + const timeMoment = moment(time); + const startMoment = moment(start); + const endMoment = moment(end); + + if (view === 'day') { + return timeMoment.format(longDateFormat); + } else if (view === 'month') { + return timeMoment.format('MMMM YYYY'); + } else if (view === 'agenda') { + return translate('Agenda'); + } + + let startFormat = 'MMM D YYYY'; + let endFormat = 'MMM D YYYY'; + + if (startMoment.isSame(endMoment, 'month')) { + startFormat = 'MMM D'; + endFormat = 'D YYYY'; + } else if (startMoment.isSame(endMoment, 'year')) { + startFormat = 'MMM D'; + endFormat = 'MMM D YYYY'; + } + + return `${startMoment.format(startFormat)} \u2014 ${endMoment.format( + endFormat + )}`; + }, [time, start, end, view, longDateFormat]); + + return ( + <div> + {isSmallScreen ? <div className={styles.titleMobile}>{title}</div> : null} + + <div className={styles.header}> + <div className={styles.navigationButtons}> + <Button + buttonGroupPosition="left" + isDisabled={view === 'agenda'} + onPress={handlePreviousPress} + > + <Icon name={icons.PAGE_PREVIOUS} /> + </Button> + + <Button + buttonGroupPosition="right" + isDisabled={view === 'agenda'} + onPress={handleNextPress} + > + <Icon name={icons.PAGE_NEXT} /> + </Button> + + <Button + className={styles.todayButton} + isDisabled={view === 'agenda'} + onPress={handleTodayPress} + > + {translate('Today')} + </Button> + </div> + + {isSmallScreen ? null : ( + <div className={styles.titleDesktop}>{title}</div> + )} + + <div className={styles.viewButtonsContainer}> + {isFetching ? ( + <LoadingIndicator className={styles.loading} size={20} /> + ) : null} + + {isLargeScreen ? ( + <Menu className={styles.viewMenu} alignMenu={align.RIGHT}> + <MenuButton> + <Icon name={icons.VIEW} size={22} /> + </MenuButton> + + <MenuContent> + {isSmallScreen ? null : ( + <ViewMenuItem + name="month" + selectedView={view} + onPress={handleViewChange} + > + {translate('Month')} + </ViewMenuItem> + )} + + <ViewMenuItem + name="week" + selectedView={view} + onPress={handleViewChange} + > + {translate('Week')} + </ViewMenuItem> + + <ViewMenuItem + name="forecast" + selectedView={view} + onPress={handleViewChange} + > + {translate('Forecast')} + </ViewMenuItem> + + <ViewMenuItem + name="day" + selectedView={view} + onPress={handleViewChange} + > + {translate('Day')} + </ViewMenuItem> + + <ViewMenuItem + name="agenda" + selectedView={view} + onPress={handleViewChange} + > + {translate('Agenda')} + </ViewMenuItem> + </MenuContent> + </Menu> + ) : ( + <> + <CalendarHeaderViewButton + view="month" + selectedView={view} + buttonGroupPosition="left" + onPress={handleViewChange} + /> + + <CalendarHeaderViewButton + view="week" + selectedView={view} + buttonGroupPosition="center" + onPress={handleViewChange} + /> + + <CalendarHeaderViewButton + view="forecast" + selectedView={view} + buttonGroupPosition="center" + onPress={handleViewChange} + /> + + <CalendarHeaderViewButton + view="day" + selectedView={view} + buttonGroupPosition="center" + onPress={handleViewChange} + /> + + <CalendarHeaderViewButton + view="agenda" + selectedView={view} + buttonGroupPosition="right" + onPress={handleViewChange} + /> + </> + )} + </div> + </div> + </div> + ); +} + +export default CalendarHeader; diff --git a/frontend/src/Calendar/Header/CalendarHeaderConnector.js b/frontend/src/Calendar/Header/CalendarHeaderConnector.js deleted file mode 100644 index 616e48650..000000000 --- a/frontend/src/Calendar/Header/CalendarHeaderConnector.js +++ /dev/null @@ -1,85 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { gotoCalendarNextRange, gotoCalendarPreviousRange, gotoCalendarToday, setCalendarView } from 'Store/Actions/calendarActions'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import CalendarHeader from './CalendarHeader'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar, - createDimensionsSelector(), - createUISettingsSelector(), - (calendar, dimensions, uiSettings) => { - const result = _.pick(calendar, [ - 'isFetching', - 'view', - 'time', - 'start', - 'end' - ]); - - result.isSmallScreen = dimensions.isSmallScreen; - result.collapseViewButtons = dimensions.isLargeScreen; - result.longDateFormat = uiSettings.longDateFormat; - - return result; - } - ); -} - -const mapDispatchToProps = { - setCalendarView, - gotoCalendarToday, - gotoCalendarPreviousRange, - gotoCalendarNextRange -}; - -class CalendarHeaderConnector extends Component { - - // - // Listeners - - onViewChange = (view) => { - this.props.setCalendarView({ view }); - }; - - onTodayPress = () => { - this.props.gotoCalendarToday(); - }; - - onPreviousPress = () => { - this.props.gotoCalendarPreviousRange(); - }; - - onNextPress = () => { - this.props.gotoCalendarNextRange(); - }; - - // - // Render - - render() { - return ( - <CalendarHeader - {...this.props} - onViewChange={this.onViewChange} - onTodayPress={this.onTodayPress} - onPreviousPress={this.onPreviousPress} - onNextPress={this.onNextPress} - /> - ); - } -} - -CalendarHeaderConnector.propTypes = { - setCalendarView: PropTypes.func.isRequired, - gotoCalendarToday: PropTypes.func.isRequired, - gotoCalendarPreviousRange: PropTypes.func.isRequired, - gotoCalendarNextRange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector); diff --git a/frontend/src/Calendar/Header/CalendarHeaderViewButton.js b/frontend/src/Calendar/Header/CalendarHeaderViewButton.js deleted file mode 100644 index 98958af03..000000000 --- a/frontend/src/Calendar/Header/CalendarHeaderViewButton.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import * as calendarViews from 'Calendar/calendarViews'; -import Button from 'Components/Link/Button'; -import titleCase from 'Utilities/String/titleCase'; -// import styles from './CalendarHeaderViewButton.css'; - -class CalendarHeaderViewButton extends Component { - - // - // Listeners - - onPress = () => { - this.props.onPress(this.props.view); - }; - - // - // Render - - render() { - const { - view, - selectedView, - ...otherProps - } = this.props; - - return ( - <Button - isDisabled={selectedView === view} - {...otherProps} - onPress={this.onPress} - > - {titleCase(view)} - </Button> - ); - } -} - -CalendarHeaderViewButton.propTypes = { - view: PropTypes.oneOf(calendarViews.all).isRequired, - selectedView: PropTypes.oneOf(calendarViews.all).isRequired, - onPress: PropTypes.func.isRequired -}; - -export default CalendarHeaderViewButton; diff --git a/frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx b/frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx new file mode 100644 index 000000000..c9366f9ef --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react'; +import { CalendarView } from 'Calendar/calendarViews'; +import Button, { ButtonProps } from 'Components/Link/Button'; +import titleCase from 'Utilities/String/titleCase'; + +interface CalendarHeaderViewButtonProps + extends Omit<ButtonProps, 'children' | 'onPress'> { + view: CalendarView; + selectedView: CalendarView; + onPress: (view: CalendarView) => void; +} + +function CalendarHeaderViewButton({ + view, + selectedView, + onPress, + ...otherProps +}: CalendarHeaderViewButtonProps) { + const handlePress = useCallback(() => { + onPress(view); + }, [view, onPress]); + + return ( + <Button + isDisabled={selectedView === view} + {...otherProps} + onPress={handlePress} + > + {titleCase(view)} + </Button> + ); +} + +export default CalendarHeaderViewButton; diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.tsx similarity index 77% rename from frontend/src/Calendar/Legend/Legend.js rename to frontend/src/Calendar/Legend/Legend.tsx index 6413665d3..b9887f856 100644 --- a/frontend/src/Calendar/Legend/Legend.js +++ b/frontend/src/Calendar/Legend/Legend.tsx @@ -1,20 +1,22 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; import { icons, kinds } from 'Helpers/Props'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import translate from 'Utilities/String/translate'; import LegendIconItem from './LegendIconItem'; import LegendItem from './LegendItem'; import styles from './Legend.css'; -function Legend(props) { +function Legend() { + const view = useSelector((state: AppState) => state.calendar.view); const { - view, showFinaleIcon, showSpecialIcon, showCutoffUnmetIcon, fullColorEvents, - colorImpairedMode - } = props; + } = useSelector((state: AppState) => state.calendar.options); + const { enableColorImpairedMode } = useSelector(createUISettingsSelector()); const iconsToShow = []; const isAgendaView = view === 'agenda'; @@ -73,7 +75,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeUnairedTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> <LegendItem @@ -81,7 +83,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeUnmonitoredTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> </div> @@ -92,7 +94,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeOnAirTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> <LegendItem @@ -100,7 +102,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeMissingTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> </div> @@ -110,7 +112,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeDownloadingTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> <LegendItem @@ -118,7 +120,7 @@ function Legend(props) { tooltip={translate('CalendarLegendEpisodeDownloadedTooltip')} isAgendaView={isAgendaView} fullColorEvents={fullColorEvents} - colorImpairedMode={colorImpairedMode} + colorImpairedMode={enableColorImpairedMode} /> </div> @@ -134,30 +136,15 @@ function Legend(props) { {iconsToShow[0]} </div> - { - iconsToShow.length > 1 && - <div> - {iconsToShow[1]} - {iconsToShow[2]} - </div> - } - { - iconsToShow.length > 3 && - <div> - {iconsToShow[3]} - </div> - } + {iconsToShow.length > 1 ? ( + <div> + {iconsToShow[1]} + {iconsToShow[2]} + </div> + ) : null} + {iconsToShow.length > 3 ? <div>{iconsToShow[3]}</div> : null} </div> ); } -Legend.propTypes = { - view: PropTypes.string.isRequired, - showFinaleIcon: PropTypes.bool.isRequired, - showSpecialIcon: PropTypes.bool.isRequired, - showCutoffUnmetIcon: PropTypes.bool.isRequired, - fullColorEvents: PropTypes.bool.isRequired, - colorImpairedMode: PropTypes.bool.isRequired -}; - export default Legend; diff --git a/frontend/src/Calendar/Legend/LegendConnector.js b/frontend/src/Calendar/Legend/LegendConnector.js deleted file mode 100644 index 889b7a002..000000000 --- a/frontend/src/Calendar/Legend/LegendConnector.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import Legend from './Legend'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.options, - (state) => state.calendar.view, - createUISettingsSelector(), - (calendarOptions, view, uiSettings) => { - return { - ...calendarOptions, - view, - colorImpairedMode: uiSettings.enableColorImpairedMode - }; - } - ); -} - -export default connect(createMapStateToProps)(Legend); diff --git a/frontend/src/Calendar/Legend/LegendIconItem.js b/frontend/src/Calendar/Legend/LegendIconItem.js deleted file mode 100644 index b6bdeeff7..000000000 --- a/frontend/src/Calendar/Legend/LegendIconItem.js +++ /dev/null @@ -1,43 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import styles from './LegendIconItem.css'; - -function LegendIconItem(props) { - const { - name, - fullColorEvents, - icon, - kind, - tooltip - } = props; - - return ( - <div - className={styles.legendIconItem} - title={tooltip} - > - <Icon - className={classNames( - styles.icon, - fullColorEvents && 'fullColorEvents' - )} - name={icon} - kind={kind} - /> - - {name} - </div> - ); -} - -LegendIconItem.propTypes = { - name: PropTypes.string.isRequired, - fullColorEvents: PropTypes.bool.isRequired, - icon: PropTypes.object.isRequired, - kind: PropTypes.string.isRequired, - tooltip: PropTypes.string.isRequired -}; - -export default LegendIconItem; diff --git a/frontend/src/Calendar/Legend/LegendIconItem.tsx b/frontend/src/Calendar/Legend/LegendIconItem.tsx new file mode 100644 index 000000000..88a758c44 --- /dev/null +++ b/frontend/src/Calendar/Legend/LegendIconItem.tsx @@ -0,0 +1,33 @@ +import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import React from 'react'; +import Icon, { IconProps } from 'Components/Icon'; +import styles from './LegendIconItem.css'; + +interface LegendIconItemProps extends Pick<IconProps, 'kind'> { + name: string; + fullColorEvents: boolean; + icon: FontAwesomeIconProps['icon']; + tooltip: string; +} + +function LegendIconItem(props: LegendIconItemProps) { + const { name, fullColorEvents, icon, kind, tooltip } = props; + + return ( + <div className={styles.legendIconItem} title={tooltip}> + <Icon + className={classNames( + styles.icon, + fullColorEvents && 'fullColorEvents' + )} + name={icon} + kind={kind} + /> + + {name} + </div> + ); +} + +export default LegendIconItem; diff --git a/frontend/src/Calendar/Legend/LegendItem.js b/frontend/src/Calendar/Legend/LegendItem.tsx similarity index 61% rename from frontend/src/Calendar/Legend/LegendItem.js rename to frontend/src/Calendar/Legend/LegendItem.tsx index f0304b9e6..40466ab9d 100644 --- a/frontend/src/Calendar/Legend/LegendItem.js +++ b/frontend/src/Calendar/Legend/LegendItem.tsx @@ -1,17 +1,26 @@ import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; +import { CalendarStatus } from 'typings/Calendar'; import titleCase from 'Utilities/String/titleCase'; import styles from './LegendItem.css'; -function LegendItem(props) { +interface LegendItemProps { + name?: string; + status: CalendarStatus; + tooltip: string; + isAgendaView: boolean; + fullColorEvents: boolean; + colorImpairedMode: boolean; +} + +function LegendItem(props: LegendItemProps) { const { name, status, tooltip, isAgendaView, fullColorEvents, - colorImpairedMode + colorImpairedMode, } = props; return ( @@ -29,13 +38,4 @@ function LegendItem(props) { ); } -LegendItem.propTypes = { - name: PropTypes.string, - status: PropTypes.string.isRequired, - tooltip: PropTypes.string.isRequired, - isAgendaView: PropTypes.bool.isRequired, - fullColorEvents: PropTypes.bool.isRequired, - colorImpairedMode: PropTypes.bool.isRequired -}; - export default LegendItem; diff --git a/frontend/src/Calendar/Options/CalendarOptionsModal.js b/frontend/src/Calendar/Options/CalendarOptionsModal.js deleted file mode 100644 index b68c83f30..000000000 --- a/frontend/src/Calendar/Options/CalendarOptionsModal.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector'; - -function CalendarOptionsModal(props) { - const { - isOpen, - onModalClose - } = props; - - return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > - <CalendarOptionsModalContentConnector - onModalClose={onModalClose} - /> - </Modal> - ); -} - -CalendarOptionsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default CalendarOptionsModal; diff --git a/frontend/src/Calendar/Options/CalendarOptionsModal.tsx b/frontend/src/Calendar/Options/CalendarOptionsModal.tsx new file mode 100644 index 000000000..ae782a684 --- /dev/null +++ b/frontend/src/Calendar/Options/CalendarOptionsModal.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import CalendarOptionsModalContent from './CalendarOptionsModalContent'; + +interface CalendarOptionsModalProps { + isOpen: boolean; + onModalClose: () => void; +} + +function CalendarOptionsModal({ + isOpen, + onModalClose, +}: CalendarOptionsModalProps) { + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <CalendarOptionsModalContent onModalClose={onModalClose} /> + </Modal> + ); +} + +export default CalendarOptionsModal; diff --git a/frontend/src/Calendar/Options/CalendarOptionsModalContent.js b/frontend/src/Calendar/Options/CalendarOptionsModalContent.js deleted file mode 100644 index c34401315..000000000 --- a/frontend/src/Calendar/Options/CalendarOptionsModalContent.js +++ /dev/null @@ -1,276 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -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 } from 'Helpers/Props'; -import { firstDayOfWeekOptions, timeFormatOptions, weekColumnOptions } from 'Settings/UI/UISettings'; -import translate from 'Utilities/String/translate'; - -class CalendarOptionsModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - firstDayOfWeek, - calendarWeekColumnHeader, - timeFormat, - enableColorImpairedMode, - fullColorEvents - } = props; - - this.state = { - firstDayOfWeek, - calendarWeekColumnHeader, - timeFormat, - enableColorImpairedMode, - fullColorEvents - }; - } - - componentDidUpdate(prevProps) { - const { - firstDayOfWeek, - calendarWeekColumnHeader, - timeFormat, - enableColorImpairedMode - } = this.props; - - if ( - prevProps.firstDayOfWeek !== firstDayOfWeek || - prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader || - prevProps.timeFormat !== timeFormat || - prevProps.enableColorImpairedMode !== enableColorImpairedMode - ) { - this.setState({ - firstDayOfWeek, - calendarWeekColumnHeader, - timeFormat, - enableColorImpairedMode - }); - } - } - - // - // Listeners - - onOptionInputChange = ({ name, value }) => { - const { - dispatchSetCalendarOption - } = this.props; - - dispatchSetCalendarOption({ [name]: value }); - }; - - onGlobalInputChange = ({ name, value }) => { - const { - dispatchSaveUISettings - } = this.props; - - const setting = { [name]: value }; - - this.setState(setting, () => { - dispatchSaveUISettings(setting); - }); - }; - - onLinkFocus = (event) => { - event.target.select(); - }; - - // - // Render - - render() { - const { - collapseMultipleEpisodes, - showEpisodeInformation, - showFinaleIcon, - showSpecialIcon, - showCutoffUnmetIcon, - fullColorEvents, - onModalClose - } = this.props; - - const { - firstDayOfWeek, - calendarWeekColumnHeader, - timeFormat, - enableColorImpairedMode - } = this.state; - - return ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('CalendarOptions')} - </ModalHeader> - - <ModalBody> - <FieldSet legend={translate('Local')}> - <Form> - <FormGroup> - <FormLabel>{translate('CollapseMultipleEpisodes')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="collapseMultipleEpisodes" - value={collapseMultipleEpisodes} - helpText={translate('CollapseMultipleEpisodesHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('ShowEpisodeInformation')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="showEpisodeInformation" - value={showEpisodeInformation} - helpText={translate('ShowEpisodeInformationHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('IconForFinales')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="showFinaleIcon" - value={showFinaleIcon} - helpText={translate('IconForFinalesHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('IconForSpecials')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="showSpecialIcon" - value={showSpecialIcon} - helpText={translate('IconForSpecialsHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('IconForCutoffUnmet')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="showCutoffUnmetIcon" - value={showCutoffUnmetIcon} - helpText={translate('IconForCutoffUnmetHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('FullColorEvents')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="fullColorEvents" - value={fullColorEvents} - helpText={translate('FullColorEventsHelpText')} - onChange={this.onOptionInputChange} - /> - </FormGroup> - </Form> - </FieldSet> - - <FieldSet legend={translate('Global')}> - <Form> - <FormGroup> - <FormLabel>{translate('FirstDayOfWeek')}</FormLabel> - - <FormInputGroup - type={inputTypes.SELECT} - name="firstDayOfWeek" - values={firstDayOfWeekOptions} - value={firstDayOfWeek} - onChange={this.onGlobalInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('WeekColumnHeader')}</FormLabel> - - <FormInputGroup - type={inputTypes.SELECT} - name="calendarWeekColumnHeader" - values={weekColumnOptions} - value={calendarWeekColumnHeader} - onChange={this.onGlobalInputChange} - helpText={translate('WeekColumnHeaderHelpText')} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('TimeFormat')}</FormLabel> - - <FormInputGroup - type={inputTypes.SELECT} - name="timeFormat" - values={timeFormatOptions} - value={timeFormat} - onChange={this.onGlobalInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('EnableColorImpairedMode')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="enableColorImpairedMode" - value={enableColorImpairedMode} - helpText={translate('EnableColorImpairedModeHelpText')} - onChange={this.onGlobalInputChange} - /> - </FormGroup> - </Form> - </FieldSet> - </ModalBody> - - <ModalFooter> - <Button onPress={onModalClose}> - {translate('Close')} - </Button> - </ModalFooter> - </ModalContent> - ); - } -} - -CalendarOptionsModalContent.propTypes = { - collapseMultipleEpisodes: PropTypes.bool.isRequired, - showEpisodeInformation: PropTypes.bool.isRequired, - showFinaleIcon: PropTypes.bool.isRequired, - showSpecialIcon: PropTypes.bool.isRequired, - showCutoffUnmetIcon: PropTypes.bool.isRequired, - firstDayOfWeek: PropTypes.number.isRequired, - calendarWeekColumnHeader: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - enableColorImpairedMode: PropTypes.bool.isRequired, - fullColorEvents: PropTypes.bool.isRequired, - dispatchSetCalendarOption: PropTypes.func.isRequired, - dispatchSaveUISettings: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default CalendarOptionsModalContent; diff --git a/frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx b/frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx new file mode 100644 index 000000000..4f974dda3 --- /dev/null +++ b/frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx @@ -0,0 +1,228 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +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 } from 'Helpers/Props'; +import { + firstDayOfWeekOptions, + timeFormatOptions, + weekColumnOptions, +} from 'Settings/UI/UISettings'; +import { setCalendarOption } from 'Store/Actions/calendarActions'; +import { saveUISettings } from 'Store/Actions/settingsActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { InputChanged } from 'typings/inputs'; +import UiSettings from 'typings/Settings/UiSettings'; +import translate from 'Utilities/String/translate'; + +interface CalendarOptionsModalContentProps { + onModalClose: () => void; +} + +function CalendarOptionsModalContent({ + onModalClose, +}: CalendarOptionsModalContentProps) { + const dispatch = useDispatch(); + + const { + collapseMultipleEpisodes, + showEpisodeInformation, + showFinaleIcon, + showSpecialIcon, + showCutoffUnmetIcon, + fullColorEvents, + } = useSelector((state: AppState) => state.calendar.options); + + const uiSettings = useSelector(createUISettingsSelector()); + + const [state, setState] = useState<Partial<UiSettings>>({ + firstDayOfWeek: uiSettings.firstDayOfWeek, + calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader, + timeFormat: uiSettings.timeFormat, + enableColorImpairedMode: uiSettings.enableColorImpairedMode, + }); + + const { + firstDayOfWeek, + calendarWeekColumnHeader, + timeFormat, + enableColorImpairedMode, + } = state; + + const handleOptionInputChange = useCallback( + ({ name, value }: InputChanged) => { + dispatch(setCalendarOption({ [name]: value })); + }, + [dispatch] + ); + + const handleGlobalInputChange = useCallback( + ({ name, value }: InputChanged) => { + setState((prevState) => ({ ...prevState, [name]: value })); + + dispatch(saveUISettings({ [name]: value })); + }, + [dispatch] + ); + + useEffect(() => { + setState({ + firstDayOfWeek: uiSettings.firstDayOfWeek, + calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader, + timeFormat: uiSettings.timeFormat, + enableColorImpairedMode: uiSettings.enableColorImpairedMode, + }); + }, [uiSettings]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('CalendarOptions')}</ModalHeader> + + <ModalBody> + <FieldSet legend={translate('Local')}> + <Form> + <FormGroup> + <FormLabel>{translate('CollapseMultipleEpisodes')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="collapseMultipleEpisodes" + value={collapseMultipleEpisodes} + helpText={translate('CollapseMultipleEpisodesHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('ShowEpisodeInformation')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showEpisodeInformation" + value={showEpisodeInformation} + helpText={translate('ShowEpisodeInformationHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('IconForFinales')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showFinaleIcon" + value={showFinaleIcon} + helpText={translate('IconForFinalesHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('IconForSpecials')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showSpecialIcon" + value={showSpecialIcon} + helpText={translate('IconForSpecialsHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('IconForCutoffUnmet')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="showCutoffUnmetIcon" + value={showCutoffUnmetIcon} + helpText={translate('IconForCutoffUnmetHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('FullColorEvents')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="fullColorEvents" + value={fullColorEvents} + helpText={translate('FullColorEventsHelpText')} + onChange={handleOptionInputChange} + /> + </FormGroup> + </Form> + </FieldSet> + + <FieldSet legend={translate('Global')}> + <Form> + <FormGroup> + <FormLabel>{translate('FirstDayOfWeek')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="firstDayOfWeek" + values={firstDayOfWeekOptions} + value={firstDayOfWeek} + onChange={handleGlobalInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('WeekColumnHeader')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="calendarWeekColumnHeader" + values={weekColumnOptions} + value={calendarWeekColumnHeader} + helpText={translate('WeekColumnHeaderHelpText')} + onChange={handleGlobalInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('TimeFormat')}</FormLabel> + + <FormInputGroup + type={inputTypes.SELECT} + name="timeFormat" + values={timeFormatOptions} + value={timeFormat} + onChange={handleGlobalInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('EnableColorImpairedMode')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="enableColorImpairedMode" + value={enableColorImpairedMode} + helpText={translate('EnableColorImpairedModeHelpText')} + onChange={handleGlobalInputChange} + /> + </FormGroup> + </Form> + </FieldSet> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Close')}</Button> + </ModalFooter> + </ModalContent> + ); +} + +export default CalendarOptionsModalContent; diff --git a/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js b/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js deleted file mode 100644 index 1f517b698..000000000 --- a/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js +++ /dev/null @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { setCalendarOption } from 'Store/Actions/calendarActions'; -import { saveUISettings } from 'Store/Actions/settingsActions'; -import CalendarOptionsModalContent from './CalendarOptionsModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.calendar.options, - (state) => state.settings.ui.item, - (options, uiSettings) => { - return { - ...options, - ...uiSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetCalendarOption: setCalendarOption, - dispatchSaveUISettings: saveUISettings -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent); diff --git a/frontend/src/Calendar/calendarViews.js b/frontend/src/Calendar/calendarViews.ts similarity index 72% rename from frontend/src/Calendar/calendarViews.js rename to frontend/src/Calendar/calendarViews.ts index 929958b66..4f5549dbd 100644 --- a/frontend/src/Calendar/calendarViews.js +++ b/frontend/src/Calendar/calendarViews.ts @@ -5,3 +5,5 @@ export const FORECAST = 'forecast'; export const AGENDA = 'agenda'; export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA]; + +export type CalendarView = 'agenda' | 'day' | 'forecast' | 'month' | 'week'; diff --git a/frontend/src/Calendar/getStatusStyle.js b/frontend/src/Calendar/getStatusStyle.ts similarity index 67% rename from frontend/src/Calendar/getStatusStyle.js rename to frontend/src/Calendar/getStatusStyle.ts index b149a8aab..678e6c2a1 100644 --- a/frontend/src/Calendar/getStatusStyle.js +++ b/frontend/src/Calendar/getStatusStyle.ts @@ -1,7 +1,13 @@ -/* eslint max-params: 0 */ import moment from 'moment'; +import { CalendarStatus } from 'typings/Calendar'; -function getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored) { +function getStatusStyle( + hasFile: boolean, + downloading: boolean, + startTime: moment.Moment, + endTime: moment.Moment, + isMonitored: boolean +): CalendarStatus { const currentTime = moment(); if (hasFile) { diff --git a/frontend/src/Calendar/iCal/CalendarLinkModal.js b/frontend/src/Calendar/iCal/CalendarLinkModal.js deleted file mode 100644 index 8cc487c16..000000000 --- a/frontend/src/Calendar/iCal/CalendarLinkModal.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector'; - -function CalendarLinkModal(props) { - const { - isOpen, - onModalClose - } = props; - - return ( - <Modal - isOpen={isOpen} - onModalClose={onModalClose} - > - <CalendarLinkModalContentConnector - onModalClose={onModalClose} - /> - </Modal> - ); -} - -CalendarLinkModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default CalendarLinkModal; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModal.tsx b/frontend/src/Calendar/iCal/CalendarLinkModal.tsx new file mode 100644 index 000000000..f0eecbd4a --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import CalendarLinkModalContent from './CalendarLinkModalContent'; + +interface CalendarLinkModalProps { + isOpen: boolean; + onModalClose: () => void; +} + +function CalendarLinkModal(props: CalendarLinkModalProps) { + const { isOpen, onModalClose } = props; + + return ( + <Modal isOpen={isOpen} onModalClose={onModalClose}> + <CalendarLinkModalContent onModalClose={onModalClose} /> + </Modal> + ); +} + +export default CalendarLinkModal; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js deleted file mode 100644 index eb64cb207..000000000 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js +++ /dev/null @@ -1,222 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputButton from 'Components/Form/FormInputButton'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import ClipboardButton from 'Components/Link/ClipboardButton'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function getUrls(state) { - const { - unmonitored, - premieresOnly, - asAllDay, - tags - } = state; - - let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/v3/calendar/Sonarr.ics?`; - - if (unmonitored) { - icalUrl += 'unmonitored=true&'; - } - - if (premieresOnly) { - icalUrl += 'premieresOnly=true&'; - } - - if (asAllDay) { - icalUrl += 'asAllDay=true&'; - } - - if (tags.length) { - icalUrl += `tags=${tags.toString()}&`; - } - - icalUrl += `apikey=${encodeURIComponent(window.Sonarr.apiKey)}`; - - const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`; - const iCalWebCalUrl = `webcal://${icalUrl}`; - - return { - iCalHttpUrl, - iCalWebCalUrl - }; -} - -class CalendarLinkModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const defaultState = { - unmonitored: false, - premieresOnly: false, - asAllDay: false, - tags: [] - }; - - const urls = getUrls(defaultState); - - this.state = { - ...defaultState, - ...urls - }; - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - const state = { - ...this.state, - [name]: value - }; - - const urls = getUrls(state); - - this.setState({ - [name]: value, - ...urls - }); - }; - - onLinkFocus = (event) => { - event.target.select(); - }; - - // - // Render - - render() { - const { - onModalClose - } = this.props; - - const { - unmonitored, - premieresOnly, - asAllDay, - tags, - iCalHttpUrl, - iCalWebCalUrl - } = this.state; - - return ( - <ModalContent onModalClose={onModalClose}> - <ModalHeader> - {translate('CalendarFeed')} - </ModalHeader> - - <ModalBody> - <Form> - <FormGroup> - <FormLabel>{translate('IncludeUnmonitored')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="unmonitored" - value={unmonitored} - helpText={translate('ICalIncludeUnmonitoredEpisodesHelpText')} - onChange={this.onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('SeasonPremieresOnly')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="premieresOnly" - value={premieresOnly} - helpText={translate('ICalSeasonPremieresOnlyHelpText')} - onChange={this.onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel> - - <FormInputGroup - type={inputTypes.CHECK} - name="asAllDay" - value={asAllDay} - helpText={translate('ICalShowAsAllDayEventsHelpText')} - onChange={this.onInputChange} - /> - </FormGroup> - - <FormGroup> - <FormLabel>{translate('Tags')}</FormLabel> - - <FormInputGroup - type={inputTypes.TAG} - name="tags" - value={tags} - helpText={translate('ICalTagsSeriesHelpText')} - onChange={this.onInputChange} - /> - </FormGroup> - - <FormGroup - size={sizes.LARGE} - > - <FormLabel>{translate('ICalFeed')}</FormLabel> - - <FormInputGroup - type={inputTypes.TEXT} - name="iCalHttpUrl" - value={iCalHttpUrl} - readOnly={true} - helpText={translate('ICalFeedHelpText')} - buttons={[ - <ClipboardButton - key="copy" - value={iCalHttpUrl} - kind={kinds.DEFAULT} - />, - - <FormInputButton - key="webcal" - kind={kinds.DEFAULT} - to={iCalWebCalUrl} - target="_blank" - noRouter={true} - > - <Icon name={icons.CALENDAR_O} /> - </FormInputButton> - ]} - onChange={this.onInputChange} - onFocus={this.onLinkFocus} - /> - </FormGroup> - </Form> - </ModalBody> - - <ModalFooter> - <Button onPress={onModalClose}> - {translate('Close')} - </Button> - </ModalFooter> - </ModalContent> - ); - } -} - -CalendarLinkModalContent.propTypes = { - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default CalendarLinkModalContent; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx b/frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx new file mode 100644 index 000000000..aa90db301 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx @@ -0,0 +1,166 @@ +import React, { FocusEvent, useCallback, useMemo, useState } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; + +interface CalendarLinkModalContentProps { + onModalClose: () => void; +} + +function CalendarLinkModalContent({ + onModalClose, +}: CalendarLinkModalContentProps) { + const [state, setState] = useState({ + unmonitored: false, + premieresOnly: false, + asAllDay: false, + tags: [], + }); + + const { unmonitored, premieresOnly, asAllDay, tags } = state; + + const handleInputChange = useCallback(({ name, value }: InputChanged) => { + setState((prevState) => ({ ...prevState, [name]: value })); + }, []); + + const handleLinkFocus = useCallback( + (event: FocusEvent<HTMLInputElement, Element>) => { + event.target.select(); + }, + [] + ); + + const { iCalHttpUrl, iCalWebCalUrl } = useMemo(() => { + let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/v3/calendar/Sonarr.ics?`; + + if (unmonitored) { + icalUrl += 'unmonitored=true&'; + } + + if (premieresOnly) { + icalUrl += 'premieresOnly=true&'; + } + + if (asAllDay) { + icalUrl += 'asAllDay=true&'; + } + + if (tags.length) { + icalUrl += `tags=${tags.toString()}&`; + } + + icalUrl += `apikey=${encodeURIComponent(window.Sonarr.apiKey)}`; + + return { + iCalHttpUrl: `${window.location.protocol}//${icalUrl}`, + iCalWebCalUrl: `webcal://${icalUrl}`, + }; + }, [unmonitored, premieresOnly, asAllDay, tags]); + + return ( + <ModalContent onModalClose={onModalClose}> + <ModalHeader>{translate('CalendarFeed')}</ModalHeader> + + <ModalBody> + <Form> + <FormGroup> + <FormLabel>{translate('IncludeUnmonitored')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="unmonitored" + value={unmonitored} + helpText={translate('ICalIncludeUnmonitoredEpisodesHelpText')} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('SeasonPremieresOnly')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="premieresOnly" + value={premieresOnly} + helpText={translate('ICalSeasonPremieresOnlyHelpText')} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel> + + <FormInputGroup + type={inputTypes.CHECK} + name="asAllDay" + value={asAllDay} + helpText={translate('ICalShowAsAllDayEventsHelpText')} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup> + <FormLabel>{translate('Tags')}</FormLabel> + + <FormInputGroup + type={inputTypes.SERIES_TAG} + name="tags" + value={tags} + helpText={translate('ICalTagsSeriesHelpText')} + onChange={handleInputChange} + /> + </FormGroup> + + <FormGroup size={sizes.LARGE}> + <FormLabel>{translate('ICalFeed')}</FormLabel> + + <FormInputGroup + type={inputTypes.TEXT} + name="iCalHttpUrl" + value={iCalHttpUrl} + readOnly={true} + helpText={translate('ICalFeedHelpText')} + buttons={[ + <ClipboardButton + key="copy" + value={iCalHttpUrl} + kind={kinds.DEFAULT} + />, + + <FormInputButton + key="webcal" + kind={kinds.DEFAULT} + to={iCalWebCalUrl} + target="_blank" + noRouter={true} + > + <Icon name={icons.CALENDAR_O} /> + </FormInputButton>, + ]} + onChange={handleInputChange} + onFocus={handleLinkFocus} + /> + </FormGroup> + </Form> + </ModalBody> + + <ModalFooter> + <Button onPress={onModalClose}>{translate('Close')}</Button> + </ModalFooter> + </ModalContent> + ); +} + +export default CalendarLinkModalContent; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js b/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js deleted file mode 100644 index e10c5c3f9..000000000 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import CalendarLinkModalContent from './CalendarLinkModalContent'; - -function createMapStateToProps() { - return createSelector( - createTagsSelector(), - (tagList) => { - return { - tagList - }; - } - ); -} - -export default connect(createMapStateToProps)(CalendarLinkModalContent); diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 0881e571a..4ed86e8e6 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { FocusEvent, ReactNode } from 'react'; import Link from 'Components/Link/Link'; import { inputTypes } from 'Helpers/Props'; import { InputType } from 'Helpers/Props/inputTypes'; @@ -152,9 +152,11 @@ interface FormInputGroupProps<T> { canEdit?: boolean; includeAny?: boolean; delimiters?: string[]; + readOnly?: boolean; errors?: (ValidationMessage | ValidationError)[]; warnings?: (ValidationMessage | ValidationWarning)[]; onChange: (args: T) => void; + onFocus?: (event: FocusEvent<HTMLInputElement>) => void; } function FormInputGroup<T>(props: FormInputGroupProps<T>) { diff --git a/frontend/src/Components/Form/TextInput.tsx b/frontend/src/Components/Form/TextInput.tsx index 007651d30..647b9f2ac 100644 --- a/frontend/src/Components/Form/TextInput.tsx +++ b/frontend/src/Components/Form/TextInput.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import React, { ChangeEvent, + FocusEvent, SyntheticEvent, useCallback, useEffect, @@ -25,7 +26,7 @@ export interface TextInputProps<T> { min?: number; max?: number; onChange: (change: InputChanged<T> | FileInputChanged) => void; - onFocus?: (event: SyntheticEvent) => void; + onFocus?: (event: FocusEvent) => void; onBlur?: (event: SyntheticEvent) => void; onCopy?: (event: SyntheticEvent) => void; onSelectionChange?: (start: number | null, end: number | null) => void; @@ -94,7 +95,7 @@ function TextInput<T>({ ); const handleFocus = useCallback( - (event: SyntheticEvent) => { + (event: FocusEvent) => { onFocus?.(event); selectionChanged(); diff --git a/frontend/src/Components/Icon.tsx b/frontend/src/Components/Icon.tsx index ea5279840..a04463b51 100644 --- a/frontend/src/Components/Icon.tsx +++ b/frontend/src/Components/Icon.tsx @@ -18,7 +18,7 @@ export interface IconProps kind?: Extract<Kind, keyof typeof styles>; size?: number; isSpinning?: FontAwesomeIconProps['spin']; - title?: string | (() => string); + title?: string | (() => string) | null; } export default function Icon({ diff --git a/frontend/src/Components/Link/Button.tsx b/frontend/src/Components/Link/Button.tsx index cf2293f59..610350a8d 100644 --- a/frontend/src/Components/Link/Button.tsx +++ b/frontend/src/Components/Link/Button.tsx @@ -1,16 +1,14 @@ import classNames from 'classnames'; import React from 'react'; -import { align, kinds, sizes } from 'Helpers/Props'; +import { kinds, sizes } from 'Helpers/Props'; +import { Align } from 'Helpers/Props/align'; import { Kind } from 'Helpers/Props/kinds'; import { Size } from 'Helpers/Props/sizes'; import Link, { LinkProps } from './Link'; import styles from './Button.css'; export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> { - buttonGroupPosition?: Extract< - (typeof align.all)[number], - keyof typeof styles - >; + buttonGroupPosition?: Extract<Align, keyof typeof styles>; kind?: Extract<Kind, keyof typeof styles>; size?: Extract<Size, keyof typeof styles>; children: Required<LinkProps['children']>; diff --git a/frontend/src/Episode/Episode.ts b/frontend/src/Episode/Episode.ts index c154e0278..0a8f69419 100644 --- a/frontend/src/Episode/Episode.ts +++ b/frontend/src/Episode/Episode.ts @@ -25,7 +25,9 @@ interface Episode extends ModelBase { endTime?: string; grabDate?: string; seriesTitle?: string; + queued?: boolean; series?: Series; + finaleType?: string; } export default Episode; diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts index 01062b2a6..3b0801c2c 100644 --- a/frontend/src/Episode/useEpisode.ts +++ b/frontend/src/Episode/useEpisode.ts @@ -1,6 +1,7 @@ import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; +import Episode from './Episode'; export type EpisodeEntities = | 'calendar' @@ -20,7 +21,7 @@ function createEpisodeSelector(episodeId?: number) { function createCalendarEpisodeSelector(episodeId?: number) { return createSelector( - (state: AppState) => state.calendar.items, + (state: AppState) => state.calendar.items as Episode[], (episodes) => { return episodes.find(({ id }) => id === episodeId); } diff --git a/frontend/src/Store/Selectors/createSeriesQualityProfileSelector.ts b/frontend/src/Store/Selectors/createSeriesQualityProfileSelector.ts index 48528873a..01cbe3788 100644 --- a/frontend/src/Store/Selectors/createSeriesQualityProfileSelector.ts +++ b/frontend/src/Store/Selectors/createSeriesQualityProfileSelector.ts @@ -1,13 +1,14 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import Series from 'Series/Series'; +import QualityProfile from 'typings/QualityProfile'; import { createSeriesSelectorForHook } from './createSeriesSelector'; function createSeriesQualityProfileSelector(seriesId: number) { return createSelector( (state: AppState) => state.settings.qualityProfiles.items, createSeriesSelectorForHook(seriesId), - (qualityProfiles, series = {} as Series) => { + (qualityProfiles: QualityProfile[], series = {} as Series) => { return qualityProfiles.find( (profile) => profile.id === series.qualityProfileId ); diff --git a/frontend/src/typings/Calendar.ts b/frontend/src/typings/Calendar.ts new file mode 100644 index 000000000..8c4cc698a --- /dev/null +++ b/frontend/src/typings/Calendar.ts @@ -0,0 +1,25 @@ +import Episode from 'Episode/Episode'; + +export interface CalendarItem extends Omit<Episode, 'airDateUtc'> { + airDateUtc: string; +} + +export interface CalendarEvent extends CalendarItem { + isGroup: false; +} + +export interface CalendarEventGroup { + isGroup: true; + seriesId: number; + seasonNumber: number; + episodeIds: number[]; + events: CalendarItem[]; +} + +export type CalendarStatus = + | 'downloaded' + | 'downloading' + | 'unmonitored' + | 'onAir' + | 'missing' + | 'unaired'; diff --git a/frontend/src/typings/CalendarEventGroup.ts b/frontend/src/typings/CalendarEventGroup.ts deleted file mode 100644 index 2039f4615..000000000 --- a/frontend/src/typings/CalendarEventGroup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Episode from 'Episode/Episode'; - -export interface CalendarEvent extends Episode { - isGroup: false; -} - -interface CalendarEventGroup { - isGroup: true; - seriesId: number; - seasonNumber: number; - episodeIds: number[]; - events: Episode[]; -} - -export default CalendarEventGroup; diff --git a/frontend/src/typings/Queue.ts b/frontend/src/typings/Queue.ts index dd266d132..8b392601f 100644 --- a/frontend/src/typings/Queue.ts +++ b/frontend/src/typings/Queue.ts @@ -1,5 +1,6 @@ import ModelBase from 'App/ModelBase'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import Episode from 'Episode/Episode'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; @@ -46,6 +47,7 @@ interface Queue extends ModelBase { episodeId?: number; seasonNumber?: number; downloadClientHasPostImportCategory: boolean; + episode?: Episode; } export default Queue; diff --git a/frontend/src/typings/Settings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts index 656c4518b..1ba62187b 100644 --- a/frontend/src/typings/Settings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -4,4 +4,7 @@ export default interface UiSettings { shortDateFormat: string; longDateFormat: string; timeFormat: string; + firstDayOfWeek: number; + enableColorImpairedMode: boolean; + calendarWeekColumnHeader: string; } From c0e264cfc520ee387bfc882c95a5822c655e0d9b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 10 Dec 2024 19:22:58 -0800 Subject: [PATCH 698/762] Fixed: Series without tags bypassing tags on Download Client Closes #7474 --- .../Download/DownloadClientProvider.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 7497c6eac..7bb4c866b 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -38,6 +38,10 @@ namespace NzbDrone.Core.Download public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false, HashSet<int> tags = null) { + // Tags aren't required, but indexers with tags should not be picked unless there is at least one matching tag. + // Defaulting to an empty HashSet ensures this is always checked. + tags ??= new HashSet<int>(); + var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId)); var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList(); @@ -46,18 +50,15 @@ namespace NzbDrone.Core.Download return null; } - if (tags is { Count: > 0 }) + var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList(); + + availableProviders = matchingTagsClients.Count > 0 ? + matchingTagsClients : + availableProviders.Where(i => i.Definition.Tags.Empty()).ToList(); + + if (!availableProviders.Any()) { - var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList(); - - availableProviders = matchingTagsClients.Count > 0 ? - matchingTagsClients : - availableProviders.Where(i => i.Definition.Tags.Empty()).ToList(); - - if (!availableProviders.Any()) - { - throw new DownloadClientUnavailableException("No download client was found without tags or a matching series tag. Please check your settings."); - } + throw new DownloadClientUnavailableException("No download client was found without tags or a matching series tag. Please check your settings."); } if (indexerId > 0) From b552d4e9f7ca7388404aa0d52566010a54cb0244 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 10 Dec 2024 19:25:13 -0800 Subject: [PATCH 699/762] Fixed: Error getting processes in some cases Closes #7470 --- src/NzbDrone.Common/Processes/ProcessProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index be443e195..17d31976d 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes processInfo = new ProcessInfo(); processInfo.Id = process.Id; processInfo.Name = process.ProcessName; - processInfo.StartPath = process.MainModule.FileName; + processInfo.StartPath = process.MainModule?.FileName; if (process.Id != GetCurrentProcessId() && process.HasExited) { From cb7489ce8fe933920ea04297bd2941496a0c07c6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 10 Dec 2024 19:33:48 -0800 Subject: [PATCH 700/762] Fixed: Augmenting languages from indexer for release with stale indexer ID Closes #7476 --- .../Aggregators/AggregateLanguagesFixture.cs | 96 +++++++++++++++++-- .../Aggregators/AggregateLanguages.cs | 5 +- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs index 6a41dc76e..ac23ade33 100644 --- a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } }; Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(1)) + .Setup(v => v.Find(1)) .Returns(indexerDefinition); _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); - Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators }; Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(1)) + .Setup(v => v.Find(1)) .Returns(indexerDefinition1); Mocker.GetMock<IIndexerFactory>() @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); - Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } @@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } }; Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(1)) + .Setup(v => v.Find(1)) .Returns(indexerDefinition); _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.Unknown }, releaseTitle); @@ -164,7 +164,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); - Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } @@ -178,7 +178,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Settings = new TorrentRssIndexerSettings { } }; Mocker.GetMock<IIndexerFactory>() - .Setup(v => v.Get(1)) + .Setup(v => v.Find(1)) .Returns(indexerDefinition); _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators _remoteEpisode.Release.Title = releaseTitle; Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage }); - Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } @@ -249,5 +249,85 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(Language.Greek); } + + [Test] + public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_does_not_exist() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition1 = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer1", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + var indexerDefinition2 = new IndexerDefinition + { + Id = 2, + Name = "MyIndexer2", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } } + }; + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Find(1)) + .Returns(null as IndexerDefinition); + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.FindByName("MyIndexer1")) + .Returns(indexerDefinition1); + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.All()) + .Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 }); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.IndexerId = 10; + _remoteEpisode.Release.Indexer = "MyIndexer1"; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Once()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_not_available() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition1 = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer1", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + var indexerDefinition2 = new IndexerDefinition + { + Id = 2, + Name = "MyIndexer2", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } } + }; + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Find(1)) + .Returns(null as IndexerDefinition); + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.FindByName("MyIndexer1")) + .Returns(indexerDefinition1); + + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.All()) + .Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 }); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle); + _remoteEpisode.Release.IndexerId = 0; + _remoteEpisode.Release.Indexer = "MyIndexer1"; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Never()); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } } } diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs index 2baf81907..afa96dd1c 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -82,9 +82,10 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators if (releaseInfo is { IndexerId: > 0 }) { - indexer = _indexerFactory.Get(releaseInfo.IndexerId); + indexer = _indexerFactory.Find(releaseInfo.IndexerId); } - else if (releaseInfo.Indexer?.IsNotNullOrWhiteSpace() == true) + + if (indexer == null && releaseInfo.Indexer?.IsNotNullOrWhiteSpace() == true) { indexer = _indexerFactory.FindByName(releaseInfo.Indexer); } From 3b00112447361b19c04851a510e63f812597a043 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:36:38 +0200 Subject: [PATCH 701/762] Fixed: Refresh backup list on deletion --- src/Sonarr.Api.V3/System/Backup/BackupController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/System/Backup/BackupController.cs b/src/Sonarr.Api.V3/System/Backup/BackupController.cs index 23d499b4e..ce3eeb2ea 100644 --- a/src/Sonarr.Api.V3/System/Backup/BackupController.cs +++ b/src/Sonarr.Api.V3/System/Backup/BackupController.cs @@ -50,7 +50,7 @@ namespace Sonarr.Api.V3.System.Backup } [RestDeleteById] - public void DeleteBackup(int id) + public object DeleteBackup(int id) { var backup = GetBackup(id); @@ -67,6 +67,8 @@ namespace Sonarr.Api.V3.System.Backup } _diskProvider.DeleteFile(path); + + return new { }; } [HttpPost("restore/{id:int}")] From 5d1d44e09ef43754f91484846ca0313bd0eb0baf Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 13 Dec 2024 23:54:50 +0200 Subject: [PATCH 702/762] New: Series genres for search results --- .../AddNewSeries/AddNewSeriesSearchResult.css | 7 ++- .../AddNewSeriesSearchResult.css.d.ts | 1 + .../AddNewSeries/AddNewSeriesSearchResult.js | 19 +++++++ frontend/src/Helpers/Props/icons.ts | 2 + frontend/src/Series/Details/SeriesDetails.css | 3 +- .../src/Series/Details/SeriesDetails.css.d.ts | 1 + frontend/src/Series/Details/SeriesDetails.js | 4 +- frontend/src/Series/Details/SeriesGenres.css | 3 -- .../src/Series/Details/SeriesGenres.css.d.ts | 7 --- frontend/src/Series/Details/SeriesGenres.js | 53 ------------------- frontend/src/Series/SeriesGenres.tsx | 38 +++++++++++++ 11 files changed, 71 insertions(+), 67 deletions(-) delete mode 100644 frontend/src/Series/Details/SeriesGenres.css delete mode 100644 frontend/src/Series/Details/SeriesGenres.css.d.ts delete mode 100644 frontend/src/Series/Details/SeriesGenres.js create mode 100644 frontend/src/Series/SeriesGenres.tsx diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css index c32e6efcb..dcf3f6de3 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css @@ -70,10 +70,15 @@ } .originalLanguageName, -.network { +.network, +.genres { margin-left: 8px; } +.genres { + pointer-events: all; +} + .tvdbLink { composes: link from '~Components/Link/Link.css'; diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts index 4d51aab62..b6fcfe361 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'alreadyExistsIcon': string; 'content': string; + 'genres': string; 'icons': string; 'network': string; 'originalLanguageName': string; diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js index 9ec6cf283..8ce556456 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.js @@ -6,6 +6,7 @@ import Label from 'Components/Label'; import Link from 'Components/Link/Link'; import MetadataAttribution from 'Components/MetadataAttribution'; import { icons, kinds, sizes } from 'Helpers/Props'; +import SeriesGenres from 'Series/SeriesGenres'; import SeriesPoster from 'Series/SeriesPoster'; import translate from 'Utilities/String/translate'; import AddNewSeriesModal from './AddNewSeriesModal'; @@ -56,6 +57,7 @@ class AddNewSeriesSearchResult extends Component { year, network, originalLanguage, + genres, status, overview, statistics, @@ -181,6 +183,18 @@ class AddNewSeriesSearchResult extends Component { null } + { + genres.length > 0 ? + <Label size={sizes.LARGE}> + <Icon + name={icons.GENRE} + size={13} + /> + <SeriesGenres className={styles.genres} genres={genres} /> + </Label> : + null + } + { seasonCount ? <Label size={sizes.LARGE}> @@ -243,6 +257,7 @@ AddNewSeriesSearchResult.propTypes = { year: PropTypes.number.isRequired, network: PropTypes.string, originalLanguage: PropTypes.object, + genres: PropTypes.arrayOf(PropTypes.string), status: PropTypes.string.isRequired, overview: PropTypes.string, statistics: PropTypes.object.isRequired, @@ -254,4 +269,8 @@ AddNewSeriesSearchResult.propTypes = { isSmallScreen: PropTypes.bool.isRequired }; +AddNewSeriesSearchResult.defaultProps = { + genres: [] +}; + export default AddNewSeriesSearchResult; diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index ba6859e58..32d0ce55d 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -102,6 +102,7 @@ import { faTable as fasTable, faTags as fasTags, faTh as fasTh, + faTheaterMasks as fasTheaterMasks, faThList as fasThList, faTimes as fasTimes, faTimesCircle as fasTimesCircle, @@ -162,6 +163,7 @@ export const FLAG = fasFlag; export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; +export const GENRE = fasTheaterMasks; export const GROUP = farObjectGroup; export const HEALTH = fasMedkit; export const HEART = fasHeart; diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index 21ff2722d..fe62642c3 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -110,7 +110,8 @@ font-size: 20px; } -.runtime { +.runtime, +.genres { margin-right: 15px; } diff --git a/frontend/src/Series/Details/SeriesDetails.css.d.ts b/frontend/src/Series/Details/SeriesDetails.css.d.ts index 9dbf4d792..939838592 100644 --- a/frontend/src/Series/Details/SeriesDetails.css.d.ts +++ b/frontend/src/Series/Details/SeriesDetails.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'details': string; 'detailsLabel': string; 'fileCountMessage': string; + 'genres': string; 'header': string; 'headerContent': string; 'info': string; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 211b40dd5..d416f4792 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -27,6 +27,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import SeriesHistoryModal from 'Series/History/SeriesHistoryModal'; import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal'; +import SeriesGenres from 'Series/SeriesGenres'; import SeriesPoster from 'Series/SeriesPoster'; import { getSeriesStatusDetails } from 'Series/SeriesStatus'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; @@ -38,7 +39,6 @@ import toggleSelected from 'Utilities/Table/toggleSelected'; import SeriesAlternateTitles from './SeriesAlternateTitles'; import SeriesDetailsLinks from './SeriesDetailsLinks'; import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector'; -import SeriesGenres from './SeriesGenres'; import SeriesTagsConnector from './SeriesTagsConnector'; import styles from './SeriesDetails.css'; @@ -419,7 +419,7 @@ class SeriesDetails extends Component { null } - <SeriesGenres genres={genres} /> + <SeriesGenres className={styles.genres} genres={genres} /> <span> {runningYears} diff --git a/frontend/src/Series/Details/SeriesGenres.css b/frontend/src/Series/Details/SeriesGenres.css deleted file mode 100644 index 93a028748..000000000 --- a/frontend/src/Series/Details/SeriesGenres.css +++ /dev/null @@ -1,3 +0,0 @@ -.genres { - margin-right: 15px; -} diff --git a/frontend/src/Series/Details/SeriesGenres.css.d.ts b/frontend/src/Series/Details/SeriesGenres.css.d.ts deleted file mode 100644 index 83399e63b..000000000 --- a/frontend/src/Series/Details/SeriesGenres.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'genres': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Series/Details/SeriesGenres.js b/frontend/src/Series/Details/SeriesGenres.js deleted file mode 100644 index 7cd1e7720..000000000 --- a/frontend/src/Series/Details/SeriesGenres.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import Tooltip from 'Components/Tooltip/Tooltip'; -import { kinds, sizes, tooltipPositions } from 'Helpers/Props'; -import styles from './SeriesGenres.css'; - -function SeriesGenres({ genres }) { - const [firstGenre, ...otherGenres] = genres; - - if (otherGenres.length) { - return ( - <Tooltip - anchor={ - <span className={styles.genres}> - {firstGenre} - </span> - } - tooltip={ - <div> - { - otherGenres.map((tag) => { - return ( - <Label - key={tag} - kind={kinds.INFO} - size={sizes.LARGE} - > - {tag} - </Label> - ); - }) - } - </div> - } - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> - ); - } - - return ( - <span className={styles.genres}> - {firstGenre} - </span> - ); -} - -SeriesGenres.propTypes = { - genres: PropTypes.arrayOf(PropTypes.string).isRequired -}; - -export default SeriesGenres; diff --git a/frontend/src/Series/SeriesGenres.tsx b/frontend/src/Series/SeriesGenres.tsx new file mode 100644 index 000000000..3db1a3e47 --- /dev/null +++ b/frontend/src/Series/SeriesGenres.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Label from 'Components/Label'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import { kinds, sizes, tooltipPositions } from 'Helpers/Props'; + +interface SeriesGenresProps { + className?: string; + genres: string[]; +} + +function SeriesGenres({ className, genres }: SeriesGenresProps) { + const [firstGenre, ...otherGenres] = genres; + + if (otherGenres.length) { + return ( + <Tooltip + anchor={<span className={className}>{firstGenre}</span>} + tooltip={ + <div> + {otherGenres.map((tag) => { + return ( + <Label key={tag} kind={kinds.INFO} size={sizes.LARGE}> + {tag} + </Label> + ); + })} + </div> + } + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> + ); + } + + return <span className={className}>{firstGenre}</span>; +} + +export default SeriesGenres; From 99e25cec0fb985d5ff915fa6e281e32f569d3939 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 15 Dec 2024 21:32:01 +0000 Subject: [PATCH 703/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/tr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index b7be87d2a..c479ff2c8 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -49,7 +49,7 @@ "AddConditionError": "Yeni bir koşul eklenemiyor, lütfen tekrar deneyin.", "AddCustomFormat": "Özel Format Ekle", "AddCustomFormatError": "Yeni bir özel format eklenemiyor, lütfen tekrar deneyin.", - "AddDelayProfile": "Gecikme Profili Ekleme", + "AddDelayProfile": "Gecikme Profili Ekle", "AddNewSeries": "Yeni Dizi Ekle", "AddNewSeriesError": "Arama sonuçları yüklenemedi, lütfen tekrar deneyin.", "AddNewSeriesHelpText": "Yeni bir dizi eklemek kolaydır, eklemek istediğiniz dizinin adını yazmaya başlamanız yeterlidir.", @@ -1380,7 +1380,7 @@ "Options": "Seçenekler", "OrganizeNamingPattern": "Adlandırma düzeni: `{episodeFormat}`", "Original": "Orijinal", - "OutputPath": "Çıkış yolu", + "OutputPath": "İndirilen Yol", "OverviewOptions": "Genel Bakış Seçenekler", "PendingChangesMessage": "Kaydedilmemiş değişiklikleriniz var, bu sayfadan ayrılmak istediğinizden emin misiniz?", "Permissions": "İzinler", @@ -1425,7 +1425,7 @@ "RegularExpressionsTutorialLink": "Düzenli ifadeler hakkında daha fazla ayrıntı [burada]({url}) bulunabilir.", "RelativePath": "Göreceli yol", "ReleaseRejected": "Reddedildi", - "ReleaseTitle": "Yayin Başlığı", + "ReleaseTitle": "Yayın Başlığı", "Reload": "Tekrar yükle", "RemoveFailedDownloadsHelpText": "Başarısız indirmeleri indirme istemcisi geçmişinden kaldırın", "RemoveFromBlocklist": "Kara listeden kaldır", From 220b4bc257e8cf80f8fa62ce2bbd8943c84712ab Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:58:00 +0200 Subject: [PATCH 704/762] Fixed: Opening episode info modal on calendar event click --- frontend/src/Calendar/Events/CalendarEvent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Calendar/Events/CalendarEvent.tsx b/frontend/src/Calendar/Events/CalendarEvent.tsx index 83452a5ce..079256a0e 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.tsx +++ b/frontend/src/Calendar/Events/CalendarEvent.tsx @@ -74,12 +74,12 @@ function CalendarEvent(props: CalendarEventProps) { const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); - const handleDetailsModalClose = useCallback(() => { + const handlePress = useCallback(() => { setIsDetailsModalOpen(true); onEventModalOpenToggle(true); }, [onEventModalOpenToggle]); - const handlePress = useCallback(() => { + const handleDetailsModalClose = useCallback(() => { setIsDetailsModalOpen(false); onEventModalOpenToggle(false); }, [onEventModalOpenToggle]); From c39fb4fe6f0ed5e1dc2aa33f4455a4d0c760063b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:59:21 +0200 Subject: [PATCH 705/762] Fix typo about download clients comment --- src/NzbDrone.Core/Download/DownloadClientProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 7bb4c866b..5a82abb11 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false, HashSet<int> tags = null) { - // Tags aren't required, but indexers with tags should not be picked unless there is at least one matching tag. + // Tags aren't required, but download clients with tags should not be picked unless there is at least one matching tag. // Defaulting to an empty HashSet ensures this is always checked. tags ??= new HashSet<int>(); From 2e83d59f61957cbc2171bef097fe2410e72729ad Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:50:16 +0200 Subject: [PATCH 706/762] Set minor version for core-js in babel/preset-env --- frontend/build/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index da97f7331..0d0364950 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -187,7 +187,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: 3 + corejs: '3.39' } ] ] From bfcd017012730c97eb587ae2d2e91f72ee7a1de3 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 15 Dec 2024 08:44:18 -0800 Subject: [PATCH 707/762] Upgrade babel to 7.26.0 --- package.json | 12 +- yarn.lock | 1245 +++++++++++++++++++++++++++----------------------- 2 files changed, 668 insertions(+), 589 deletions(-) diff --git a/package.json b/package.json index db747ef6e..92a784f9f 100644 --- a/package.json +++ b/package.json @@ -84,13 +84,13 @@ "typescript": "5.7.2" }, "devDependencies": { - "@babel/core": "7.25.8", - "@babel/eslint-parser": "7.25.8", - "@babel/plugin-proposal-export-default-from": "7.25.8", + "@babel/core": "7.26.0", + "@babel/eslint-parser": "7.25.9", + "@babel/plugin-proposal-export-default-from": "7.25.9", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.25.8", - "@babel/preset-react": "7.25.7", - "@babel/preset-typescript": "7.25.7", + "@babel/preset-env": "7.26.0", + "@babel/preset-react": "7.26.3", + "@babel/preset-typescript": "7.26.0", "@types/lodash": "4.14.195", "@types/qs": "6.9.16", "@types/react-autosuggest": "10.1.11", diff --git a/yarn.lock b/yarn.lock index 23cd15e46..8dbd73b92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,7 +15,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.25.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== @@ -23,47 +23,62 @@ "@babel/highlight" "^7.25.7" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== -"@babel/core@7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6" - integrity sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg== +"@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== + +"@babel/core@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.25.7" - "@babel/generator" "^7.25.7" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helpers" "^7.25.7" - "@babel/parser" "^7.25.8" - "@babel/template" "^7.25.7" - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.8" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.8.tgz#0119dec46be547d7a339978dedb9d29e517c2443" - integrity sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg== +"@babel/eslint-parser@7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz#603c68a63078796527bc9d0833f5e52dd5f9224c" + integrity sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" - integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/types" "^7.25.7" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -75,15 +90,14 @@ dependencies: "@babel/types" "^7.25.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" - integrity sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg== +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/types" "^7.25.9" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.7": +"@babel/helper-compilation-targets@^7.22.6": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== @@ -94,20 +108,31 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" - integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-member-expression-to-functions" "^7.25.7" - "@babel/helper-optimise-call-expression" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.7": +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== @@ -116,6 +141,15 @@ regexpu-core "^6.1.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.2.0" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" @@ -127,109 +161,120 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-member-expression-to-functions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" - integrity sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA== +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-module-imports@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472" - integrity sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw== +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-module-transforms@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz#2ac9372c5e001b19bc62f1fe7d96a18cb0901d1a" - integrity sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ== +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-simple-access" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-optimise-call-expression@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz#1de1b99688e987af723eed44fa7fc0ee7b97d77a" - integrity sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng== +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== dependencies: - "@babel/types" "^7.25.7" + "@babel/types" "^7.25.9" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== -"@babel/helper-remap-async-to-generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" - integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-wrap-function" "^7.25.7" - "@babel/traverse" "^7.25.7" +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== -"@babel/helper-replace-supers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" - integrity sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw== +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.7" - "@babel/helper-optimise-call-expression" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-simple-access@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0" - integrity sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ== +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-skip-transparent-expression-wrappers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz#382831c91038b1a6d32643f5f49505b8442cb87c" - integrity sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA== +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" "@babel/helper-string-parser@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== -"@babel/helper-wrap-function@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz#9f6021dd1c4fdf4ad515c809967fc4bac9a70fe7" - integrity sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg== - dependencies: - "@babel/template" "^7.25.7" - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== -"@babel/helpers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2" - integrity sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA== +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== dependencies: - "@babel/template" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" "@babel/highlight@^7.25.7": version "7.25.7" @@ -241,58 +286,58 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.25.7", "@babel/parser@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2" - integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ== +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: - "@babel/types" "^7.25.8" + "@babel/types" "^7.26.3" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz#93969ac50ef4d68b2504b01b758af714e4cbdd64" - integrity sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz#a338d611adb9dcd599b8b1efa200c88ebeffe046" - integrity sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" - integrity sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz#3b7ea04492ded990978b6deaa1dfca120ad4455a" - integrity sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-transform-optional-chaining" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz#9622b1d597a703aa3a921e6f58c9c2d9a028d2c5" - integrity sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-proposal-export-default-from@7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.8.tgz#fa22151caa240683c3659796037237813767f348" - integrity sha512-5SLPHA/Gk7lNdaymtSVS9jH77Cs7yuHTR3dYj+9q+M7R7tNLXhNuvnmOfafRIzpWL+dtMibuu1I4ofrc768Gkw== +"@babel/plugin-proposal-export-default-from@7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.9.tgz#52702be6ef8367fc8f18b8438278332beeb8f87c" + integrity sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -306,33 +351,33 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-import-assertions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz#8ce248f9f4ed4b7ed4cb2e0eb4ed9efd9f52921f" - integrity sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ== +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-attributes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz#d78dd0499d30df19a598e63ab895e21b909bc43f" - integrity sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-jsx@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz#5352d398d11ea5e7ef330c854dea1dae0bf18165" - integrity sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw== +"@babel/plugin-syntax-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-typescript@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz#bfc05b0cc31ebd8af09964650cee723bb228108b" - integrity sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g== +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -342,498 +387,505 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz#1b9ed22e6890a0e9ff470371c73b8c749bcec386" - integrity sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg== +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-async-generator-functions@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz#3331de02f52cc1f2c75b396bec52188c85b0b1ec" - integrity sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA== +"@babel/plugin-transform-async-generator-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-async-to-generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz#a44c7323f8d4285a6c568dd43c5c361d6367ec52" - integrity sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg== +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" -"@babel/plugin-transform-block-scoped-functions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz#e0b8843d5571719a2f1bf7e284117a3379fcc17c" - integrity sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ== +"@babel/plugin-transform-block-scoped-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz#5700691dbd7abb93de300ca7be94203764fce458" + integrity sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-block-scoping@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz#6dab95e98adf780ceef1b1c3ab0e55cd20dd410a" - integrity sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow== +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-class-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" - integrity sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g== +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-class-static-block@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz#a8af22028920fe404668031eceb4c3aadccb5262" - integrity sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ== +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-classes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz#5103206cf80d02283bbbd044509ea3b65d0906bb" - integrity sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg== +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz#7f621f0aa1354b5348a935ab12e3903842466f65" - integrity sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA== +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/template" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" -"@babel/plugin-transform-destructuring@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz#f6f26a9feefb5aa41fd45b6f5838901b5333d560" - integrity sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA== +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-dotall-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz#9d775c4a3ff1aea64045300fcd4309b4a610ef02" - integrity sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ== +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-keys@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz#fbba7d1155eab76bd4f2a038cbd5d65883bd7a93" - integrity sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg== +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz#102b31608dcc22c08fbca1894e104686029dc141" - integrity sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-dynamic-import@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz#f1edbe75b248cf44c70c8ca8ed3818a668753aaa" - integrity sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A== +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-exponentiation-operator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz#5961a3a23a398faccd6cddb34a2182807d75fb5f" - integrity sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg== +"@babel/plugin-transform-exponentiation-operator@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-export-namespace-from@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz#d1988c3019a380b417e0516418b02804d3858145" - integrity sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw== +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-for-of@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz#0acfea0f27aa290818b5b48a5a44b3f03fc13669" - integrity sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw== +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz#4bdc7d42a213397905d89f02350c5267866d5755" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-function-name@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz#7e394ccea3693902a8b50ded8b6ae1fa7b8519fd" - integrity sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ== +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-json-strings@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz#6fb3ec383a2ea92652289fdba653e3f9de722694" - integrity sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA== +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz#70cbdc742f2cfdb1a63ea2cbd018d12a60b213c3" - integrity sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w== +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-logical-assignment-operators@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz#01868ff92daa9e525b4c7902aa51979082a05710" - integrity sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g== +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-member-expression-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz#0a36c3fbd450cc9e6485c507f005fa3d1bc8fca5" - integrity sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw== +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-amd@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz#bb4e543b5611f6c8c685a2fd485408713a3adf3d" - integrity sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA== +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-commonjs@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz#173f0c791bb7407c092ce6d77ee90eb3f2d1d2fd" - integrity sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg== +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-simple-access" "^7.25.7" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-systemjs@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz#8b14d319a177cc9c85ef8b0512afd429d9e2e60b" - integrity sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g== +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-modules-umd@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz#00ee7a7e124289549381bfb0e24d87fd7f848367" - integrity sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw== +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz#a2f3f6d7f38693b462542951748f0a72a34d196d" - integrity sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-new-target@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz#52b2bde523b76c548749f38dc3054f1f45e82bc9" - integrity sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA== +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-nullish-coalescing-operator@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz#befb4900c130bd52fccf2b926314557987f1b552" - integrity sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ== +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz#bcb1b0d9e948168102d5f7104375ca21c3266949" + integrity sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-numeric-separator@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz#91e370486371637bd42161052f2602c701386891" - integrity sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q== +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-object-rest-spread@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz#0904ac16bcce41df4db12d915d6780f85c7fb04b" - integrity sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g== +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-transform-parameters" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" -"@babel/plugin-transform-object-super@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz#582a9cea8cf0a1e02732be5b5a703a38dedf5661" - integrity sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA== +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" -"@babel/plugin-transform-optional-catch-binding@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz#2649b86a3bb202c6894ec81a6ddf41b94d8f3103" - integrity sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg== +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-optional-chaining@^7.25.7", "@babel/plugin-transform-optional-chaining@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz#f46283b78adcc5b6ab988a952f989e7dce70653f" - integrity sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg== +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-parameters@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" - integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-private-methods@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" - integrity sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA== +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-private-property-in-object@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz#1234f856ce85e061f9688764194e51ea7577c434" - integrity sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow== +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-property-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz#a8612b4ea4e10430f00012ecf0155662c7d6550d" - integrity sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw== +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-display-name@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz#2753e875a1b702fb1d806c4f5d4c194d64cadd88" - integrity sha512-r0QY7NVU8OnrwE+w2IWiRom0wwsTbjx4+xH2RTd7AVdof3uurXOF+/mXHQDRk+2jIvWgSaCHKMgggfvM4dyUGA== +"@babel/plugin-transform-react-display-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" + integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-jsx-development@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.7.tgz#2fbd77887b8fa2942d7cb61edf1029ea1b048554" - integrity sha512-5yd3lH1PWxzW6IZj+p+Y4OLQzz0/LzlOG8vGqonHfVR3euf1vyzyMUJk9Ac+m97BH46mFc/98t9PmYLyvgL3qg== +"@babel/plugin-transform-react-jsx-development@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" + integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== dependencies: - "@babel/plugin-transform-react-jsx" "^7.25.7" + "@babel/plugin-transform-react-jsx" "^7.25.9" -"@babel/plugin-transform-react-jsx@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz#f5e2af6020a562fe048dd343e571c4428e6c5632" - integrity sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q== +"@babel/plugin-transform-react-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" + integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-jsx" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/plugin-transform-react-pure-annotations@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.7.tgz#6d0b8dadb2d3c5cbb8ade68c5efd49470b0d65f7" - integrity sha512-6YTHJ7yjjgYqGc8S+CbEXhLICODk0Tn92j+vNJo07HFk9t3bjFgAKxPLFhHwF2NjmQVSI1zBRfBWUeVBa2osfA== +"@babel/plugin-transform-react-pure-annotations@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" + integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-regenerator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz#6eb006e6d26f627bc2f7844a9f19770721ad6f3e" - integrity sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ== +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz#dc56b25e02afaabef3ce0c5b06b0916e8523e995" - integrity sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng== +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-shorthand-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz#92690a9c671915602d91533c278cc8f6bf12275f" - integrity sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA== +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-spread@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz#df83e899a9fc66284ee601a7b738568435b92998" - integrity sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw== +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-sticky-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz#341c7002bef7f29037be7fb9684e374442dd0d17" - integrity sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw== +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-template-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz#e566c581bb16d8541dd8701093bb3457adfce16b" - integrity sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA== +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-typeof-symbol@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz#debb1287182efd20488f126be343328c679b66eb" - integrity sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA== +"@babel/plugin-transform-template-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz#6dbd4a24e8fad024df76d1fac6a03cf413f60fe1" + integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-typescript@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz#8fc7c3d28ddd36bce45b9b48594129d0e560cfbe" - integrity sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q== +"@babel/plugin-transform-typeof-symbol@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz#224ba48a92869ddbf81f9b4a5f1204bbf5a2bc4b" + integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-syntax-typescript" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-escapes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz#973592b6d13a914794e1de8cf1383e50e0f87f81" - integrity sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ== +"@babel/plugin-transform-typescript@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz#3d6add9c78735623317387ee26d5ada540eee3fd" + integrity sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" -"@babel/plugin-transform-unicode-property-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz#25349197cce964b1343f74fa7cfdf791a1b1919e" - integrity sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w== +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz#f93a93441baf61f713b6d5552aaa856bfab34809" - integrity sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g== +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-sets-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz#d1b3295d29e0f8f4df76abc909ad1ebee919560c" - integrity sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw== +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/preset-env@7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.8.tgz#dc6b719627fb29cd9cccbbbe041802fd575b524c" - integrity sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg== +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== dependencies: - "@babel/compat-data" "^7.25.8" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.7" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.7" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.7" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.0.tgz#30e5c6bc1bcc54865bff0c5a30f6d4ccdc7fa8b1" + integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== + dependencies: + "@babel/compat-data" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.25.7" - "@babel/plugin-syntax-import-attributes" "^7.25.7" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.7" - "@babel/plugin-transform-async-generator-functions" "^7.25.8" - "@babel/plugin-transform-async-to-generator" "^7.25.7" - "@babel/plugin-transform-block-scoped-functions" "^7.25.7" - "@babel/plugin-transform-block-scoping" "^7.25.7" - "@babel/plugin-transform-class-properties" "^7.25.7" - "@babel/plugin-transform-class-static-block" "^7.25.8" - "@babel/plugin-transform-classes" "^7.25.7" - "@babel/plugin-transform-computed-properties" "^7.25.7" - "@babel/plugin-transform-destructuring" "^7.25.7" - "@babel/plugin-transform-dotall-regex" "^7.25.7" - "@babel/plugin-transform-duplicate-keys" "^7.25.7" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.7" - "@babel/plugin-transform-dynamic-import" "^7.25.8" - "@babel/plugin-transform-exponentiation-operator" "^7.25.7" - "@babel/plugin-transform-export-namespace-from" "^7.25.8" - "@babel/plugin-transform-for-of" "^7.25.7" - "@babel/plugin-transform-function-name" "^7.25.7" - "@babel/plugin-transform-json-strings" "^7.25.8" - "@babel/plugin-transform-literals" "^7.25.7" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.8" - "@babel/plugin-transform-member-expression-literals" "^7.25.7" - "@babel/plugin-transform-modules-amd" "^7.25.7" - "@babel/plugin-transform-modules-commonjs" "^7.25.7" - "@babel/plugin-transform-modules-systemjs" "^7.25.7" - "@babel/plugin-transform-modules-umd" "^7.25.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.7" - "@babel/plugin-transform-new-target" "^7.25.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.8" - "@babel/plugin-transform-numeric-separator" "^7.25.8" - "@babel/plugin-transform-object-rest-spread" "^7.25.8" - "@babel/plugin-transform-object-super" "^7.25.7" - "@babel/plugin-transform-optional-catch-binding" "^7.25.8" - "@babel/plugin-transform-optional-chaining" "^7.25.8" - "@babel/plugin-transform-parameters" "^7.25.7" - "@babel/plugin-transform-private-methods" "^7.25.7" - "@babel/plugin-transform-private-property-in-object" "^7.25.8" - "@babel/plugin-transform-property-literals" "^7.25.7" - "@babel/plugin-transform-regenerator" "^7.25.7" - "@babel/plugin-transform-reserved-words" "^7.25.7" - "@babel/plugin-transform-shorthand-properties" "^7.25.7" - "@babel/plugin-transform-spread" "^7.25.7" - "@babel/plugin-transform-sticky-regex" "^7.25.7" - "@babel/plugin-transform-template-literals" "^7.25.7" - "@babel/plugin-transform-typeof-symbol" "^7.25.7" - "@babel/plugin-transform-unicode-escapes" "^7.25.7" - "@babel/plugin-transform-unicode-property-regex" "^7.25.7" - "@babel/plugin-transform-unicode-regex" "^7.25.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.7" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.25.9" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.25.9" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.25.9" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.25.9" + "@babel/plugin-transform-typeof-symbol" "^7.25.9" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.10" babel-plugin-polyfill-corejs3 "^0.10.6" @@ -850,28 +902,28 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.7.tgz#081cbe1dea363b732764d06a0fdda67ffa17735d" - integrity sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg== +"@babel/preset-react@7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - "@babel/plugin-transform-react-display-name" "^7.25.7" - "@babel/plugin-transform-react-jsx" "^7.25.7" - "@babel/plugin-transform-react-jsx-development" "^7.25.7" - "@babel/plugin-transform-react-pure-annotations" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-transform-react-display-name" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx-development" "^7.25.9" + "@babel/plugin-transform-react-pure-annotations" "^7.25.9" -"@babel/preset-typescript@7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz#43c5b68eccb856ae5b52274b77b1c3c413cde1b7" - integrity sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw== +"@babel/preset-typescript@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz#4a570f1b8d104a242d923957ffa1eaff142a106d" + integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - "@babel/plugin-syntax-jsx" "^7.25.7" - "@babel/plugin-transform-modules-commonjs" "^7.25.7" - "@babel/plugin-transform-typescript" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-typescript" "^7.25.9" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.25.7" @@ -880,29 +932,29 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769" - integrity sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA== +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== dependencies: - "@babel/code-frame" "^7.25.7" - "@babel/parser" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/traverse@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" - integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== +"@babel/traverse@^7.25.9": + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== dependencies: - "@babel/code-frame" "^7.25.7" - "@babel/generator" "^7.25.7" - "@babel/parser" "^7.25.7" - "@babel/template" "^7.25.7" - "@babel/types" "^7.25.7" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.25.7", "@babel/types@^7.25.8", "@babel/types@^7.4.4": +"@babel/types@^7.25.7", "@babel/types@^7.4.4": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== @@ -911,6 +963,14 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@csstools/css-parser-algorithms@^2.1.1": version "2.7.1" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz#6d93a8f7d8aeb7cd9ed0868f946e46f021b6aa70" @@ -5786,6 +5846,18 @@ regexpu-core@^6.1.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + regjsgen@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" @@ -5798,6 +5870,13 @@ regjsparser@^0.11.0: dependencies: jsesc "~3.0.2" +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" From 016b5718386593c030f14fcac307c93ee1ceeca6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 15 Dec 2024 08:45:49 -0800 Subject: [PATCH 708/762] Upgrade Font Awesome to 6.7.1 --- package.json | 8 ++++---- yarn.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 92a784f9f..adff1d686 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "defaults" ], "dependencies": { - "@fortawesome/fontawesome-free": "6.6.0", - "@fortawesome/fontawesome-svg-core": "6.6.0", - "@fortawesome/free-regular-svg-icons": "6.6.0", - "@fortawesome/free-solid-svg-icons": "6.6.0", + "@fortawesome/fontawesome-free": "6.7.1", + "@fortawesome/fontawesome-svg-core": "6.7.1", + "@fortawesome/free-regular-svg-icons": "6.7.1", + "@fortawesome/free-solid-svg-icons": "6.7.1", "@fortawesome/react-fontawesome": "0.2.2", "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", diff --git a/yarn.lock b/yarn.lock index 8dbd73b92..0ecc39d6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,36 +1028,36 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@fortawesome/fontawesome-common-types@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f" - integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw== +"@fortawesome/fontawesome-common-types@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz#6201640f39fdcf8e41cd9d1a92b2da3a96817fa4" + integrity sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ== -"@fortawesome/fontawesome-free@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz#0e984f0f2344ee513c185d87d77defac4c0c8224" - integrity sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow== +"@fortawesome/fontawesome-free@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz#160a48730d533ec77578ed0141661a8f0150a71d" + integrity sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA== -"@fortawesome/fontawesome-svg-core@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff" - integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg== +"@fortawesome/fontawesome-svg-core@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz#1f8ebb6f35cf02f89c110198514e848de17ac99e" + integrity sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g== dependencies: - "@fortawesome/fontawesome-common-types" "6.6.0" + "@fortawesome/fontawesome-common-types" "6.7.1" -"@fortawesome/free-regular-svg-icons@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz#fc49a947ac8dfd20403c9ea5f37f0919425bdf04" - integrity sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ== +"@fortawesome/free-regular-svg-icons@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.1.tgz#d7ec06f896ee91116a388a5a234cd26420ccdfe4" + integrity sha512-e13cp+bAx716RZOTQ59DhqikAgETA9u1qTBHO3e3jMQQ+4H/N1NC1ZVeFYt1V0m+Th68BrEL1/X6XplISutbXg== dependencies: - "@fortawesome/fontawesome-common-types" "6.6.0" + "@fortawesome/fontawesome-common-types" "6.7.1" -"@fortawesome/free-solid-svg-icons@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz#061751ca43be4c4d814f0adbda8f006164ec9f3b" - integrity sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA== +"@fortawesome/free-solid-svg-icons@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz#c1f9a6c25562a12c283e87e284f9d82a5b0dbcc0" + integrity sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg== dependencies: - "@fortawesome/fontawesome-common-types" "6.6.0" + "@fortawesome/fontawesome-common-types" "6.7.1" "@fortawesome/react-fontawesome@0.2.2": version "0.2.2" From ed10b63fa0c161cac7e0a2084e53785ab1798208 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 16 Dec 2024 16:09:27 -0800 Subject: [PATCH 709/762] Upgrade @typescript-eslint packages to 8.181.1 --- frontend/.eslintrc.js | 4 + frontend/src/App/State/MetadataAppState.ts | 2 +- frontend/src/App/State/SettingsAppState.ts | 3 +- frontend/src/App/State/WantedAppState.ts | 4 +- .../Components/Form/Tag/SeriesTagInput.tsx | 2 +- .../Components/Table/Cells/TableRowCell.tsx | 2 +- .../src/Utilities/String/getLanguageName.ts | 2 +- frontend/src/Utilities/String/translate.ts | 2 +- package.json | 4 +- yarn.lock | 170 +++++++++--------- 10 files changed, 99 insertions(+), 96 deletions(-) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 77b933a8f..e14b9125d 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -363,7 +363,11 @@ module.exports = { { args: 'after-used', argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', ignoreRestSiblings: true + } ], '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/frontend/src/App/State/MetadataAppState.ts b/frontend/src/App/State/MetadataAppState.ts index 60d5c434c..495f109d8 100644 --- a/frontend/src/App/State/MetadataAppState.ts +++ b/frontend/src/App/State/MetadataAppState.ts @@ -1,6 +1,6 @@ import { AppSectionProviderState } from 'App/State/AppSectionState'; import Metadata from 'typings/Metadata'; -interface MetadataAppState extends AppSectionProviderState<Metadata> {} +type MetadataAppState = AppSectionProviderState<Metadata>; export default MetadataAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 28d3fc098..b8e6f4954 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -37,8 +37,7 @@ export interface NamingAppState extends AppSectionItemState<NamingConfig>, AppSectionSaveState {} -export interface NamingExamplesAppState - extends AppSectionItemState<NamingExample> {} +export type NamingExamplesAppState = AppSectionItemState<NamingExample>; export interface ImportListAppState extends AppSectionState<ImportList>, diff --git a/frontend/src/App/State/WantedAppState.ts b/frontend/src/App/State/WantedAppState.ts index 18a0fbd33..b543d3879 100644 --- a/frontend/src/App/State/WantedAppState.ts +++ b/frontend/src/App/State/WantedAppState.ts @@ -1,9 +1,9 @@ import AppSectionState from 'App/State/AppSectionState'; import Episode from 'Episode/Episode'; -interface WantedCutoffUnmetAppState extends AppSectionState<Episode> {} +type WantedCutoffUnmetAppState = AppSectionState<Episode>; -interface WantedMissingAppState extends AppSectionState<Episode> {} +type WantedMissingAppState = AppSectionState<Episode>; interface WantedAppState { cutoffUnmet: WantedCutoffUnmetAppState; diff --git a/frontend/src/Components/Form/Tag/SeriesTagInput.tsx b/frontend/src/Components/Form/Tag/SeriesTagInput.tsx index 6d4beb20a..f72248cf5 100644 --- a/frontend/src/Components/Form/Tag/SeriesTagInput.tsx +++ b/frontend/src/Components/Form/Tag/SeriesTagInput.tsx @@ -23,7 +23,7 @@ const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i'); function isValidTag(tagName: string) { try { return !VALID_TAG_REGEX.test(tagName); - } catch (e) { + } catch { return false; } } diff --git a/frontend/src/Components/Table/Cells/TableRowCell.tsx b/frontend/src/Components/Table/Cells/TableRowCell.tsx index 00b6acb1d..aed693222 100644 --- a/frontend/src/Components/Table/Cells/TableRowCell.tsx +++ b/frontend/src/Components/Table/Cells/TableRowCell.tsx @@ -1,7 +1,7 @@ import React, { ComponentPropsWithoutRef } from 'react'; import styles from './TableRowCell.css'; -export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {} +export type TableRowCellProps = ComponentPropsWithoutRef<'td'>; export default function TableRowCell({ className = styles.cell, diff --git a/frontend/src/Utilities/String/getLanguageName.ts b/frontend/src/Utilities/String/getLanguageName.ts index bf1b5451e..6bbaf3252 100644 --- a/frontend/src/Utilities/String/getLanguageName.ts +++ b/frontend/src/Utilities/String/getLanguageName.ts @@ -35,7 +35,7 @@ export default function getLanguageName(code: string) { try { return languageNames.of(code) ?? code; - } catch (error) { + } catch { return code; } } diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 9a633040c..b9aa5c769 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> { translations = data.strings; resolve(true); - } catch (error) { + } catch { resolve(false); } }); diff --git a/package.json b/package.json index adff1d686..600ac5df3 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,8 @@ "@types/react-window": "1.8.8", "@types/redux-actions": "2.6.5", "@types/webpack-livereload-plugin": "2.3.6", - "@typescript-eslint/eslint-plugin": "6.21.0", - "@typescript-eslint/parser": "6.21.0", + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "babel-plugin-inline-classnames": "2.0.1", diff --git a/yarn.lock b/yarn.lock index 0ecc39d6e..15f6e4931 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1003,7 +1003,12 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint-community/regexpp@^4.6.1": version "4.11.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== @@ -1325,7 +1330,7 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== -"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1497,11 +1502,6 @@ resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.5.tgz#3728477ada6c4bc0fe54ffae11def23a65c0714b" integrity sha512-RgXOigay5cNweP+xH1ru+Vaaj1xXYLpWIfSVO8cSA8Ii2xvR+HRfWYdLe1UVOA8X0kIklalGOa0DTDyld0obkg== -"@types/semver@^7.5.0": - version "7.5.8" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" - integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== - "@types/source-list-map@*": version "0.1.6" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.6.tgz#164e169dd061795b50b83c19e4d3be09f8d3a454" @@ -1547,91 +1547,86 @@ anymatch "^3.0.0" source-map "^0.6.0" -"@typescript-eslint/eslint-plugin@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" - integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== +"@typescript-eslint/eslint-plugin@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz#992e5ac1553ce20d0d46aa6eccd79dc36dedc805" + integrity sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ== dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/type-utils" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/type-utils" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" -"@typescript-eslint/parser@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.1.tgz#c258bae062778b7696793bc492249027a39dfb95" + integrity sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/scope-manager@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz#52cedc3a8178d7464a70beffed3203678648e55b" + integrity sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" -"@typescript-eslint/type-utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" - integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== +"@typescript-eslint/type-utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz#10f41285475c0bdee452b79ff7223f0e43a7781e" + integrity sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ== dependencies: - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils" "8.18.1" debug "^4.3.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/types@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.1.tgz#d7f4f94d0bba9ebd088de840266fcd45408a8fff" + integrity sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw== -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz#2a86cd64b211a742f78dfa7e6f4860413475367e" + integrity sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== +"@typescript-eslint/utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.1.tgz#c4199ea23fc823c736e2c96fd07b1f7235fa92d5" + integrity sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/visitor-keys@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz#344b4f6bc83f104f514676facf3129260df7610a" + integrity sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ== dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "8.18.1" + eslint-visitor-keys "^4.2.0" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -3202,6 +3197,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" @@ -3309,7 +3309,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -3848,7 +3848,7 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -4683,13 +4683,6 @@ mini-css-extract-plugin@2.9.1: schema-utils "^4.0.0" tapable "^2.2.1" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" @@ -4711,6 +4704,13 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimatch@~3.0.4: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -6099,7 +6099,7 @@ semver@^6.0.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.4: +semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.6.0: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -6654,10 +6654,10 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -ts-api-utils@^1.0.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== +ts-api-utils@^1.3.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-loader@9.5.1: version "9.5.1" From edfc12e27a00fd927df0de6ccb3961efe8f5dc3b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 16 Dec 2024 15:52:09 -0800 Subject: [PATCH 710/762] Fixed: Loading calendar on older browsers --- frontend/src/polyfills.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/polyfills.js b/frontend/src/polyfills.js index ece7aea85..04de2c85c 100644 --- a/frontend/src/polyfills.js +++ b/frontend/src/polyfills.js @@ -8,6 +8,7 @@ window.console.debug = window.console.debug || function() {}; window.console.warn = window.console.warn || function() {}; window.console.assert = window.console.assert || function() {}; +// TODO: Remove in v5, well suppoprted in browsers if (!String.prototype.startsWith) { Object.defineProperty(String.prototype, 'startsWith', { enumerable: false, @@ -20,6 +21,7 @@ if (!String.prototype.startsWith) { }); } +// TODO: Remove in v5, well suppoprted in browsers if (!String.prototype.endsWith) { Object.defineProperty(String.prototype, 'endsWith', { enumerable: false, @@ -34,8 +36,14 @@ if (!String.prototype.endsWith) { }); } +// TODO: Remove in v5, use `includes` instead if (!('contains' in String.prototype)) { String.prototype.contains = function(str, startIndex) { return String.prototype.indexOf.call(this, str, startIndex) !== -1; }; } + +// For Firefox ESR 115 support +if (!Object.groupBy) { + import('core-js/actual/object/group-by'); +} From 983b079c8257790b52d7f63b2aa8051e88c7ceb2 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 21 Dec 2024 01:13:30 +0100 Subject: [PATCH 711/762] Fix: Adding a new root folder from edit series modal Closes #7497 --- frontend/src/Components/Form/Select/RootFolderSelectInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx index 68b3d3da2..8b278ded7 100644 --- a/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx +++ b/frontend/src/Components/Form/Select/RootFolderSelectInput.tsx @@ -134,7 +134,7 @@ function RootFolderSelectInput({ const handleNewRootFolderSelect = useCallback( ({ value: newValue }: InputChanged<string>) => { setNewRootFolderPath(newValue); - dispatch(addRootFolder(newValue)); + dispatch(addRootFolder({ path: newValue })); }, [setNewRootFolderPath, dispatch] ); From 82c526e15c2eb3b2d80f372b821caf275f524dc6 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 19 Dec 2024 05:32:08 +0000 Subject: [PATCH 712/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/tr.json | 54 ++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index c479ff2c8..19b097da0 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -129,9 +129,9 @@ "CloneCondition": "Klon Durumu", "CountIndexersSelected": "{count} dizinleyici 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", + "AutoRedownloadFailed": "Başarısız İndirmeleri Yenile", + "AutoRedownloadFailedFromInteractiveSearch": "Etkileşimli Arama'dan Başarısız İndirmeleri Yenile", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etkileşimli aramadan başarısız bir sürüm alındığında otomatik olarak farklı bir sürümü arayın ve indirmeye çalışın", "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", @@ -227,8 +227,8 @@ "DownloadClientFreeboxSettingsApiUrl": "API URL'si", "DownloadClientFreeboxSettingsAppId": "Uygulama kimliği", "DownloadClientFreeboxNotLoggedIn": "Giriş yapmadınız", - "DownloadClientFreeboxSettingsAppToken": "Uygulama Jetonu", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')", + "DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama token'ı (ör. 'app_token')", "Apply": "Uygula", "DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}", "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')", @@ -263,7 +263,7 @@ "DownloadClientSettingsAddPaused": "Duraklatılana Ekle", "DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın", "DownloadClientSettingsInitialState": "Başlangıç Durumu", - "DownloadClientSettingsPostImportCategoryHelpText": "{appName}'in indirmeyi içe aktardıktan sonra ayarlayacağı kategori. {appName}, tohumlama tamamlansa bile bu kategorideki torrentleri kaldırmaz. Aynı kategoriyi korumak için boş bırakın.", + "DownloadClientSettingsPostImportCategoryHelpText": "{appName}'in indirmeyi içe aktardıktan sonra ayarlayacağı kategori. {appName}, seed tamamlanmış olsa bile bu kategorideki torrentleri kaldırmayacaktır. Aynı kategoriyi korumak için boş bırakın.", "DownloadClientSettingsUseSslHelpText": "{clientName} ile bağlantı kurulurken güvenli bağlantıyı kullan", "DownloadClientSettingsUrlBaseHelpText": "{clientName} URL'sine {url} gibi bir önek ekler", "DownloadClientValidationApiKeyRequired": "API Anahtarı Gerekli", @@ -337,7 +337,7 @@ "Imported": "İçe aktarıldı", "NotificationsAppriseSettingsTagsHelpText": "İsteğe bağlı olarak yalnızca uygun şekilde etiketlenenleri bilgilendirin.", "NotificationsDiscordSettingsAvatar": "Avatar", - "NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Jetonu", + "NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Token'ı", "ImportScriptPath": "Komut Dosyası Yolunu İçe Aktar", "History": "Geçmiş", "EditSelectedImportLists": "Seçilen İçe Aktarma Listelerini Düzenle", @@ -373,8 +373,8 @@ "NotificationsKodiSettingsDisplayTimeHelpText": "Bildirimin ne kadar süreyle görüntüleneceği (Saniye cinsinden)", "NotificationsMailgunSettingsUseEuEndpoint": "AB Uç Noktasını Kullan", "NotificationsMailgunSettingsSenderDomain": "Gönderen Alanı", - "NotificationsNtfySettingsAccessToken": "Erişim Jetonu", - "NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı jeton tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir", + "NotificationsNtfySettingsAccessToken": "Erişim Token'ı", + "NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı token tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir", "NotificationsNtfySettingsPasswordHelpText": "İsteğe bağlı şifre", "NotificationsNtfySettingsTagsEmojisHelpText": "Kullanılacak etiketlerin veya emojilerin isteğe bağlı listesi", "NotificationsNtfySettingsTopics": "Konular", @@ -480,7 +480,7 @@ "NotificationsEmailSettingsCcAddress": "CC Adres(ler)i", "NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i", "NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplığı Güncelleyin", - "NotificationsGotifySettingsAppToken": "Uygulama Jetonu", + "NotificationsGotifySettingsAppToken": "Uygulama Token'ı", "NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği", "Test": "Test Et", @@ -508,7 +508,7 @@ "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", "LastDuration": "Yürütme Süresi", "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", - "NotificationsPlexSettingsAuthToken": "Kimlik Doğrulama Jetonu", + "NotificationsPlexSettingsAuthToken": "Kimlik Doğrulama Token'ı", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Alıcının Grup Kimliği / Telefon Numarası", "NotificationsSignalSettingsUsernameHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan kullanıcı adı", "NotificationsTraktSettingsAuthenticateWithTrakt": "Trakt ile kimlik doğrulama", @@ -564,7 +564,7 @@ "NotificationsNtfyValidationAuthorizationRequired": "Yetkilendirme gerekli", "NotificationsPushBulletSettingSenderId": "Gönderen ID", "NotificationsPushBulletSettingsChannelTags": "Kanal Etiketleri", - "NotificationsPushBulletSettingsAccessToken": "Erişim Jetonu", + "NotificationsPushBulletSettingsAccessToken": "Erişim Token'ı", "NotificationsPushBulletSettingsChannelTagsHelpText": "Bildirimlerin gönderileceği Kanal Etiketleri Listesi", "NotificationsPushBulletSettingsDeviceIdsHelpText": "Cihaz kimliklerinin listesi (tüm cihazlara göndermek için boş bırakın)", "NotificationsPushcutSettingsNotificationName": "Bildirim Adı", @@ -583,14 +583,14 @@ "NotificationsSlackSettingsIcon": "Simge", "NotificationsSlackSettingsIconHelpText": "Slack'e gönderilen mesajlar için kullanılan simgeyi değiştirin (Emoji veya URL)", "NotificationsSynologyValidationInvalidOs": "Bir Synology olmalı", - "NotificationsTelegramSettingsBotToken": "Bot Jetonu", + "NotificationsTelegramSettingsBotToken": "Bot Token'ı", "NotificationsTelegramSettingsChatIdHelpText": "Mesaj almak için botla bir konuşma başlatmanız veya onu grubunuza eklemeniz gerekir", "NotificationsTelegramSettingsTopicId": "Konu Kimliği", "NotificationsTraktSettingsAuthUser": "Yetkilendirilmiş Kullanıcı", "NotificationsTwitterSettingsConsumerKey": "Kullanıcı anahtarı", "NotificationsTwitterSettingsConnectToTwitter": "Twitter / X'e bağlanın", - "NotificationsValidationInvalidAccessToken": "Erişim Jetonu geçersiz", - "NotificationsValidationInvalidAuthenticationToken": "Kimlik Doğrulama Jetonu geçersiz", + "NotificationsValidationInvalidAccessToken": "Erişim Token'ı geçersiz", + "NotificationsValidationInvalidAuthenticationToken": "Kimlik Doğrulama Token'ı geçersiz", "OverrideAndAddToDownloadQueue": "Geçersiz kıl ve indirme kuyruğuna ekle", "Parse": "Ayrıştır", "PackageVersion": "Paket Versiyonu", @@ -605,8 +605,8 @@ "StopSelecting": "Düzenlemeden Çık", "TableOptionsButton": "Tablo Seçenekleri Butonu", "TheLogLevelDefault": "Log seviyesi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/ayarlar/genel) bölümünden değiştirilebilir", - "NotificationsTwitterSettingsAccessToken": "Erişim Jetonu", - "AutoRedownloadFailedHelpText": "Otomatik olarak farklı bir Yayın arayın ve indirmeye çalışın", + "NotificationsTwitterSettingsAccessToken": "Erişim Token'ı", + "AutoRedownloadFailedHelpText": "Farklı bir sürümü otomatik olarak ara ve indirmeyi dene", "Queue": "Kuyruk", "RemoveFromQueue": "Kuyruktan kaldır", "TorrentDelayTime": "Torrent Gecikmesi: {torrentDelay}", @@ -706,7 +706,7 @@ "QueueIsEmpty": "Kuyruk boş", "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizinleyicinin ayarlanması, bu profilin yalnızca söz konusu dizinleyicinin yayınlarına uygulanmasına neden olur.", "ResetDefinitionTitlesHelpText": "Değerlerin yanı sıra tanım başlıklarını da sıfırlayın", - "SecretToken": "Gizlilik Jetonu", + "SecretToken": "Gizlilik Token'ı", "SetReleaseGroupModalTitle": "{modalTitle} - Yayımlama Grubunu Ayarla", "SslPort": "SSL Bağlantı Noktası", "True": "Aktif", @@ -723,7 +723,7 @@ "ReleaseProfileIndexerHelpText": "Profilin hangi dizinleyiciye uygulanacağını belirtin", "TablePageSize": "Sayfa Boyutu", "NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil", - "NotificationsTwitterSettingsAccessTokenSecret": "Erişim Jetonu Gizliliği", + "NotificationsTwitterSettingsAccessTokenSecret": "Erişim Token Gizliliği", "NotificationsSimplepushSettingsKey": "Anahtar", "NotificationsPushBulletSettingsDeviceIds": "Cihaz Kimlikleri", "NotificationsSendGridSettingsApiKeyHelpText": "SendGrid tarafından oluşturulan API Anahtar", @@ -756,9 +756,9 @@ "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Bu, dosyaları taşımak yerine {appName}'e Kopyalama veya Sabit Bağlantı kurma talimatını verecektir (ayarlara/sistem yapılandırmasına bağlı olarak)", "Interval": "Periyot", "NotificationsTelegramSettingsSendSilentlyHelpText": "Mesajı sessizce gönderir. Kullanıcılar sessiz bir bildirim alacak", - "NotificationsTraktSettingsAccessToken": "Erişim Jetonu", + "NotificationsTraktSettingsAccessToken": "Erişim Token'ı", "NotificationsTraktSettingsExpires": "Süresi doluyor", - "NotificationsTraktSettingsRefreshToken": "Jetonu Yenile", + "NotificationsTraktSettingsRefreshToken": "Token'ı Yenile", "NotificationsTwitterSettingsConsumerKeyHelpText": "Twitter uygulamasından kullanıcı anahtarı", "NotificationsTelegramSettingsChatId": "Sohbet Kimliği", "NotificationsTwitterSettingsConsumerSecret": "Kullanıcı Gizliliği", @@ -992,7 +992,7 @@ "MetadataSettings": "Meta Veri Ayarları", "Negated": "Reddedildi", "FileManagement": "Dosya Yönetimi", - "FirstDayOfWeek": "Haftanın ilk günü", + "FirstDayOfWeek": "Haftanın İlk Günü", "Fixed": "Düzeltilen", "InstallLatest": "En Sonu Yükle", "RemotePathMappings": "Uzak Yol Eşlemeleri", @@ -1269,7 +1269,7 @@ "DeleteSelectedCustomFormats": "Özel Formatı Sil", "DeleteSelectedCustomFormatsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?", "DeleteSelectedImportListExclusionsMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", - "DestinationRelativePath": "Hedef Göreli Yol", + "DestinationRelativePath": "Hedef Göreceli Yol", "DiskSpace": "Disk Alanı", "DoNotUpgradeAutomatically": "Otomatik Olarak Yükseltme", "Donations": "Bağış", @@ -1464,8 +1464,8 @@ "Settings": "Ayarlar", "ShowAdvanced": "Gelişmiş'i Göster", "ShowQualityProfileHelpText": "Poster altında kalite profilini göster", - "ShowRelativeDates": "Göreli Tarihleri Göster", - "ShowRelativeDatesHelpText": "Göreli (Bugün / Dün / vb.) Veya mutlak tarihleri göster", + "ShowRelativeDates": "İlgili Tarihleri Göster", + "ShowRelativeDatesHelpText": "Göreceli (Bugün/Dün/vb.) veya mutlak tarihleri göster", "ShowSearch": "Aramayı Göster", "ShowSearchHelpText": "Fareyle üzerine gelindiğinde arama düğmesini göster", "ShowTitle": "Başlığı göster", @@ -1477,10 +1477,10 @@ "Socks5": "Socks5 (TOR Desteği)", "Source": "Kaynak", "SourcePath": "Kaynak Yolu", - "SourceRelativePath": "Kaynak Göreli Yol", + "SourceRelativePath": "Kaynak Göreceli Yol", "SourceTitle": "Kaynak başlığı", "StartProcessing": "İşlemeye Başla", - "Style": "Tarz", + "Style": "Stil", "Sunday": "Pazar", "SupportedDownloadClients": "{appName}, Newznab standardını kullanan herhangi bir indirme istemcisinin yanı sıra aşağıda listelenen diğer indirme istemcilerini de destekler.", "SupportedDownloadClientsMoreInfo": "Bireysel indirme istemcileri hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.", From 9a69222c9a1c42c2571f21e2d4a2e02b90216248 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 17 Dec 2024 17:05:39 -0800 Subject: [PATCH 713/762] Fixed: Prevent exception when grabbing unparsable release Closes #7494 --- .../DataAugmentation/Scene/SceneMappingService.cs | 5 +++++ src/NzbDrone.Core/Parser/Parser.cs | 5 +++++ src/Sonarr.Api.V3/Indexers/ReleaseController.cs | 6 +++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index 10aac05ec..b9e568163 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -94,6 +94,11 @@ namespace NzbDrone.Core.DataAugmentation.Scene public SceneMapping FindSceneMapping(string seriesTitle, string releaseTitle, int sceneSeasonNumber) { + if (seriesTitle.IsNullOrWhiteSpace()) + { + return null; + } + var mappings = FindMappings(seriesTitle, releaseTitle); if (mappings == null) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 836e52ca1..2b603052e 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -836,6 +836,11 @@ namespace NzbDrone.Core.Parser public static string CleanSeriesTitle(this string title) { + if (title.IsNullOrWhiteSpace()) + { + return title; + } + // If Title only contains numbers return it as is. if (long.TryParse(title, out _)) { diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs index 6de121914..12da7f9ae 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs @@ -127,7 +127,7 @@ namespace Sonarr.Api.V3.Indexers if (episodes.Empty()) { - throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse episodes in the release"); + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse episodes in the release, will need to be manually provided"); } remoteEpisode.Series = series; @@ -135,7 +135,7 @@ namespace Sonarr.Api.V3.Indexers } else { - throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to find matching series and episodes"); + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to find matching series and episodes, will need to be manually provided"); } } else if (remoteEpisode.Episodes.Empty()) @@ -154,7 +154,7 @@ namespace Sonarr.Api.V3.Indexers if (remoteEpisode.Episodes.Empty()) { - throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse episodes in the release"); + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse episodes in the release, will need to be manually provided"); } await _downloadService.DownloadReport(remoteEpisode, release.DownloadClientId); From 608f67a074b2053a9e2581d688854d1675f08104 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:35:09 +0200 Subject: [PATCH 714/762] Bump MailKit to 4.8.0 and Microsoft.Data.SqlClient to 2.1.7 --- src/NzbDrone.Core/Sonarr.Core.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index b62133278..c89a512df 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -6,8 +6,9 @@ <PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Diacritical.Net" Version="1.0.4" /> <PackageReference Include="Equ" Version="2.3.0" /> - <PackageReference Include="MailKit" Version="3.6.0" /> + <PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" /> + <PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.7" /> <PackageReference Include="Polly" Version="8.3.1" /> <PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" /> <PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" /> From ab49268bac95949fd0b083e0c4357dd918de43f1 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:15:26 +0200 Subject: [PATCH 715/762] Bump NLog, IPAddressRange, Polly, ImageSharp, Npgsql, System.Memory, Ical.Net and Lib.Harmony --- src/NzbDrone.Common/Sonarr.Common.csproj | 8 ++++---- src/NzbDrone.Core/Sonarr.Core.csproj | 10 +++++----- src/NzbDrone.Host/Sonarr.Host.csproj | 2 +- src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj | 2 +- src/NzbDrone.Update/Sonarr.Update.csproj | 2 +- src/NzbDrone.Windows/Sonarr.Windows.csproj | 2 +- src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 4 ++-- src/Sonarr.Http/Sonarr.Http.csproj | 2 +- src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index e7acd472a..16fe75f18 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -5,14 +5,14 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> - <PackageReference Include="IPAddressRange" Version="6.0.0" /> + <PackageReference Include="IPAddressRange" Version="6.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="5.3.2" /> - <PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" /> + <PackageReference Include="NLog" Version="5.3.4" /> + <PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.2" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> - <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" /> <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="System.Text.Json" Version="6.0.10" /> diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index c89a512df..2b2ec9042 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -9,10 +9,10 @@ <PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.7" /> - <PackageReference Include="Polly" Version="8.3.1" /> + <PackageReference Include="Polly" Version="8.5.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="System.Memory" Version="4.6.0" /> <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" /> @@ -20,13 +20,13 @@ <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.1.5" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog" Version="5.3.4" /> <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.10" /> - <PackageReference Include="Npgsql" Version="7.0.7" /> + <PackageReference Include="Npgsql" Version="7.0.9" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index c2b82e95c..322e345c5 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.21" /> - <PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> diff --git a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj index 4155803b2..bc1e7a7dc 100644 --- a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj @@ -6,7 +6,7 @@ <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="5.3.2" /> + <PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="RestSharp" Version="106.15.0" /> </ItemGroup> diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index 4d07b3d1a..9d7c5c05f 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -6,7 +6,7 @@ <ItemGroup> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> - <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog" Version="5.3.4" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Windows/Sonarr.Windows.csproj b/src/NzbDrone.Windows/Sonarr.Windows.csproj index e534a2b3f..19f586803 100644 --- a/src/NzbDrone.Windows/Sonarr.Windows.csproj +++ b/src/NzbDrone.Windows/Sonarr.Windows.csproj @@ -4,7 +4,7 @@ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> - <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" /> </ItemGroup> <ItemGroup> diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index ac6900d33..62aba2b87 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -4,8 +4,8 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="Ical.Net" Version="4.2.0" /> - <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="Ical.Net" Version="4.3.1" /> + <PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" /> </ItemGroup> <ItemGroup> diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index 290357a96..9bacc9f25 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -5,7 +5,7 @@ <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="ImpromptuInterface" Version="7.0.1" /> - <PackageReference Include="NLog" Version="5.3.2" /> + <PackageReference Include="NLog" Version="5.3.4" /> </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..3800542e1 100644 --- a/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj +++ b/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj @@ -3,6 +3,6 @@ <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="Lib.Harmony" Version="2.0.1" /> + <PackageReference Include="Lib.Harmony" Version="2.3.3" /> </ItemGroup> </Project> From ce7d8a175e1afdf9a1a71aec1e6259c398c6201a Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 22 Dec 2024 09:32:03 +0000 Subject: [PATCH 716/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Tommy Au <smarttommyau@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_TW/ Translation: Servarr/Sonarr --- .../Localization/Core/zh_TW.json | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index 2d69ba69b..5313dcd31 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -18,7 +18,7 @@ "AddListExclusion": "新增排除清單", "Add": "新增", "About": "關於", - "Actions": "執行", + "Actions": "動作", "AddAutoTagError": "無法加入新的自動標籤,請重新嘗試。", "AddAutoTag": "新增自動標籤", "AddCondition": "新增條件", @@ -36,5 +36,21 @@ "AddCustomFilter": "新增自定義過濾器", "UnselectAll": "取消全選", "Any": "任何", - "UpdateAvailableHealthCheckMessage": "可用的新版本: {version}" + "UpdateAvailableHealthCheckMessage": "可用的新版本: {version}", + "AutoRedownloadFailed": "失敗時重新下載", + "AutoRedownloadFailedFromInteractiveSearch": "失敗時重新下載來自手動搜索的資源", + "AuthenticationRequired": "需要驗證", + "AppDataDirectory": "AppData 路徑", + "AuthenticationMethod": "驗證方式", + "AuthenticationRequiredHelpText": "更改需要進行驗證的請求。除非你了解其中的風險,否則請勿修改。", + "AuthenticationRequiredPasswordHelpTextWarning": "請輸入新密碼", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "確認新密碼", + "AuthenticationMethodHelpTextWarning": "請選擇一個有效的驗證方式", + "AuthenticationRequiredUsernameHelpTextWarning": "請輸入新用戶名", + "AudioLanguages": "音頻語言", + "Activity": "‎活動‎", + "AddNewRestriction": "加入新的限制", + "AuthenticationRequiredWarning": "為防止未經認證的遠程訪問,{appName} 現需要啟用身份認證。您可以選擇禁用本地地址的身份認證。", + "AddRemotePathMappingError": "無法加入新的遠程路徑對應,請重試。", + "AnalyseVideoFilesHelpText": "從文件中提取影像資訊,如解析度、運行環境和編解碼器資訊。這需要 {appName} 在掃描期間讀取文件並可能導致高磁盤或網絡佔用。" } From f7b54f9d6b88330e04f127b6ee2ed9ee19404ec9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:52:27 +0200 Subject: [PATCH 717/762] Fixed: Prevent exception for seed configuration provider with invalid indexer ID --- .../IndexerTests/SeedConfigProviderFixture.cs | 27 +++++++++++++++++-- .../Indexers/CachedIndexerSettingsProvider.cs | 18 ++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs index f01d50d55..0dc71467e 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.IndexerTests var result = Subject.GetSeedConfiguration(new RemoteEpisode { - Release = new ReleaseInfo() + Release = new ReleaseInfo { DownloadProtocol = DownloadProtocol.Torrent, IndexerId = 0 @@ -32,6 +32,29 @@ namespace NzbDrone.Core.Test.IndexerTests result.Should().BeNull(); } + [Test] + public void should_not_return_config_for_invalid_indexer() + { + Mocker.GetMock<ICachedIndexerSettingsProvider>() + .Setup(v => v.GetSettings(It.IsAny<int>())) + .Returns<CachedIndexerSettings>(null); + + var result = Subject.GetSeedConfiguration(new RemoteEpisode + { + Release = new ReleaseInfo + { + DownloadProtocol = DownloadProtocol.Torrent, + IndexerId = 1 + }, + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + FullSeason = true + } + }); + + result.Should().BeNull(); + } + [Test] public void should_return_season_time_for_season_packs() { @@ -48,7 +71,7 @@ namespace NzbDrone.Core.Test.IndexerTests var result = Subject.GetSeedConfiguration(new RemoteEpisode { - Release = new ReleaseInfo() + Release = new ReleaseInfo { DownloadProtocol = DownloadProtocol.Torrent, IndexerId = 1 diff --git a/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs b/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs index f5cb3064c..3f95ca0e4 100644 --- a/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs +++ b/src/NzbDrone.Core/Indexers/CachedIndexerSettingsProvider.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider.Events; @@ -12,8 +14,10 @@ public interface ICachedIndexerSettingsProvider CachedIndexerSettings GetSettings(int indexerId); } -public class CachedIndexerSettingsProvider : ICachedIndexerSettingsProvider, IHandle<ProviderUpdatedEvent<IIndexer>> +public class CachedIndexerSettingsProvider : ICachedIndexerSettingsProvider, IHandle<ProviderUpdatedEvent<IIndexer>>, IHandle<ProviderDeletedEvent<IIndexer>> { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(CachedIndexerSettingsProvider)); + private readonly IIndexerFactory _indexerFactory; private readonly ICached<CachedIndexerSettings> _cache; @@ -35,11 +39,12 @@ public class CachedIndexerSettingsProvider : ICachedIndexerSettingsProvider, IHa private CachedIndexerSettings FetchIndexerSettings(int indexerId) { - var indexer = _indexerFactory.Get(indexerId); - var indexerSettings = indexer.Settings as IIndexerSettings; + var indexer = _indexerFactory.Find(indexerId); - if (indexerSettings == null) + if (indexer?.Settings is not IIndexerSettings indexerSettings) { + Logger.Trace("Could not load settings for indexer ID: {0}", indexerId); + return null; } @@ -60,6 +65,11 @@ public class CachedIndexerSettingsProvider : ICachedIndexerSettingsProvider, IHa { _cache.Clear(); } + + public void Handle(ProviderDeletedEvent<IIndexer> message) + { + _cache.Clear(); + } } public class CachedIndexerSettings From 1c30ecd66dd0fd1dafaf9ab0e41a11a54eaac132 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 21 Dec 2024 15:12:20 -0800 Subject: [PATCH 718/762] Fixed: Series updated during Import List Sync not reflected in the UI Closes #7511 --- .../Tv/Events/SeriesBulkEditedEvent.cs | 15 +++++++++++++++ src/NzbDrone.Core/Tv/SeriesService.cs | 3 +++ src/Sonarr.Api.V3/Series/SeriesController.cs | 10 ++++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/NzbDrone.Core/Tv/Events/SeriesBulkEditedEvent.cs diff --git a/src/NzbDrone.Core/Tv/Events/SeriesBulkEditedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesBulkEditedEvent.cs new file mode 100644 index 000000000..7fbf23e48 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/SeriesBulkEditedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class SeriesBulkEditedEvent : IEvent + { + public List<Series> Series { get; private set; } + + public SeriesBulkEditedEvent(List<Series> series) + { + Series = series; + } + } +} diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index d34c63be4..29cb6fac5 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -251,6 +251,7 @@ namespace NzbDrone.Core.Tv _seriesRepository.UpdateMany(series); _logger.Debug("{0} series updated", series.Count); + _eventAggregator.PublishEvent(new SeriesBulkEditedEvent(series)); return series; } @@ -298,6 +299,8 @@ namespace NzbDrone.Core.Tv return true; } + _logger.Debug("Tags not updated for '{0}'", series.Title); + return false; } } diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 7122af7e9..73c95aafd 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -34,6 +34,7 @@ namespace Sonarr.Api.V3.Series IHandle<SeriesEditedEvent>, IHandle<SeriesDeletedEvent>, IHandle<SeriesRenamedEvent>, + IHandle<SeriesBulkEditedEvent>, IHandle<MediaCoversUpdatedEvent> { private readonly ISeriesService _seriesService; @@ -338,6 +339,15 @@ namespace Sonarr.Api.V3.Series BroadcastResourceChange(ModelAction.Updated, message.Series.Id); } + [NonAction] + public void Handle(SeriesBulkEditedEvent message) + { + foreach (var series in message.Series) + { + BroadcastResourceChange(ModelAction.Updated, series.ToResource()); + } + } + [NonAction] public void Handle(MediaCoversUpdatedEvent message) { From 4b143687365fb07498df4919af38773f9fbe6561 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 25 Dec 2024 09:32:06 +0000 Subject: [PATCH 719/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: 1 <1228553526@qq.com> Co-authored-by: Fixer <ygj59783@zslsz.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Tommy Au <smarttommyau@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: marapavelka <mara.pavelka@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ar/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_TW/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/cs.json | 69 +++++----- src/NzbDrone.Core/Localization/Core/fi.json | 122 +++++++++++------- src/NzbDrone.Core/Localization/Core/it.json | 2 +- src/NzbDrone.Core/Localization/Core/ro.json | 4 +- .../Localization/Core/zh_TW.json | 17 ++- 5 files changed, 129 insertions(+), 85 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index fe6aafb59..ee77064e8 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -7,19 +7,19 @@ "AnalyseVideoFiles": "Analyzovat video soubory", "AnalyseVideoFilesHelpText": "Extrahujte ze souborů informace o videu, jako je rozlišení, doba běhu a informace o kodeku. To vyžaduje, aby {appName} četl části souboru, což může způsobit vysokou aktivitu disku nebo sítě během skenování.", "ApplicationURL": "URL aplikace", - "ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základní adresy URL", + "ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základu URL", "AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k {appName}u", - "AuthenticationRequired": "Vyžadované ověření", - "AuthenticationRequiredHelpText": "Změnit, pro které požadavky je vyžadováno ověření. Pokud nerozumíte rizikům, neměňte je.", - "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní {appName} povolení ověření. Ověřování z místních adres můžete volitelně zakázat.", + "AuthenticationRequired": "Vyžadováno ověření", + "AuthenticationRequiredHelpText": "Změnit, pro které požadavky je vyžadováno ověření. Neměňte, pokud nerozumíte rizikům.", + "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní {appName}, aby bylo povoleno ověřování. Volitelně můžete zakázat ověřování z místních adres.", "AutoRedownloadFailedHelpText": "Automatické vyhledání a pokus o stažení jiného vydání", "AutoTaggingLoadError": "Nepodařilo se načíst automatické značky", "AutomaticAdd": "Přidat automaticky", "BackupIntervalHelpText": "Interval mezi automatickými zálohami", "BackupRetentionHelpText": "Automatické zálohy starší než doba uchovávání budou automaticky vyčištěny", "BackupsLoadError": "Nelze načíst zálohy", - "BranchUpdate": "Větev, která se použije k aktualizaci {appName}u", - "BranchUpdateMechanism": "Větev používaná externím aktualizačním mechanismem", + "BranchUpdate": "Větev použitá k aktualizaci {appName}u", + "BranchUpdateMechanism": "Větev použitá externím aktualizačním mechanismem", "BrowserReloadRequired": "Vyžaduje se opětovné načtení prohlížeče", "BuiltIn": "Vestavěný", "BypassDelayIfHighestQualityHelpText": "Obejít zpoždění, když má vydání nejvyšší povolenou kvalitu v profilu kvality s preferovaným protokolem", @@ -58,12 +58,12 @@ "Automatic": "Automatický", "AutoTaggingNegateHelpText": "Pokud je zaškrtnuto, pravidlo automatického označování se nepoužije, pokud odpovídá této podmínce {implementationName}.", "BindAddress": "Vázat adresu", - "BindAddressHelpText": "Platná IP adresa, localhost nebo '*' pro všechna rozhraní", + "BindAddressHelpText": "Platná IP adresa, localhost nebo ‚*‘ pro všechna rozhraní", "BlocklistLoadError": "Nelze načíst černou listinu", "BypassDelayIfHighestQuality": "Obejít v případě nejvyšší kvality", "BypassDelayIfAboveCustomFormatScoreHelpText": "Povolit obcházení, pokud má vydání vyšší skóre, než je nakonfigurované minimální skóre vlastního formátu", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimální skóre vlastního formátu", - "CertificateValidation": "Ověření certifikátu", + "CertificateValidation": "Ověřování certifikátu", "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ů.", @@ -86,19 +86,19 @@ "AbsoluteEpisodeNumbers": "Úplné číslo dílu(ů)", "AddRootFolder": "Přidat kořenový adresář", "Backups": "Zálohy", - "Clear": "Vyčistit", - "BeforeUpdate": "Před zálohováním", + "Clear": "Vymazat", + "BeforeUpdate": "Před aktualizací", "CloneAutoTag": "Klonovat automatické značky", "Conditions": "Podmínky", - "CancelPendingTask": "Opravdu chcete zrušit tento nevyřízený úkol?", + "CancelPendingTask": "Opravdu chcete zrušit tento úkol čekající na vyřízení?", "Apply": "Použít", - "AddingTag": "Přidání značky", - "ApplyTags": "Použít značky", + "AddingTag": "Přidávání štítku", + "ApplyTags": "Použít štítky", "AutoAdd": "Přidat automaticky", "Cancel": "Zrušit", "ApplyTagsHelpTextHowToApplyDownloadClients": "Jak použít značky na vybrané klienty pro stahování", "ApplyTagsHelpTextHowToApplyImportLists": "Jak použít značky na vybrané seznamy k importu", - "ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané indexery", + "ApplyTagsHelpTextHowToApplyIndexers": "Jak použít štítky na vybrané indexery", "AudioInfo": "Audio informace", "Age": "Stáří", "AudioLanguages": "Jazyky zvuku", @@ -114,22 +114,22 @@ "AllResultsAreHiddenByTheAppliedFilter": "Všechny výsledky jsou schovány použitým filtrem", "AddANewPath": "Přidat novou cestu", "AddCustomFormatError": "Nebylo možné přidat nový vlastní formát, prosím zkuste to později.", - "AddDownloadClientImplementation": "Přidat klienta pro stahování - {implementationName}", + "AddDownloadClientImplementation": "Přidat klienta pro stahování – {implementationName}", "AddImportListExclusionError": "Nebylo možné přidat nové importované položky, prosím zkuste to později.", - "ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané značky", - "ApplyTagsHelpTextAdd": "Přidat: Přidá značky k již existujícímu seznamu", - "ApplyTagsHelpTextReplace": "Nahradit: Nahradit značky zadanými značkami (prázdné pole vymaže všechny značky)", + "ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané štítky", + "ApplyTagsHelpTextAdd": "Přidat: Přidat štítky do existujícího seznamu štítků", + "ApplyTagsHelpTextReplace": "Nahradit: Nahradit štítky zadanými štítky (prázdné pole vymaže všechny štítky)", "Backup": "Záloha", "Activity": "Aktivita", "Blocklist": "Blocklist", "AddNew": "Přidat nové", "About": "O aplikaci", "Actions": "Akce", - "AptUpdater": "K instalaci aktualizace použijte apt", - "BackupNow": "Ihned zálohovat", + "AptUpdater": "K instalaci aktualizace používat apt", + "BackupNow": "Zálohovat nyní", "AppDataDirectory": "Adresář AppData", "ApplyTagsHelpTextHowToApplySeries": "Jak použít značky na vybrané seriály", - "BackupFolderHelpText": "Relativní cesty se budou nacházet v adresáři AppData systému {appName}", + "BackupFolderHelpText": "Relativní cesty budou v adresáři AppData {appName}u", "BlocklistReleases": "Blocklist pro vydání", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Epizoda se stahuje", @@ -150,7 +150,7 @@ "ClickToChangeSeries": "Kliknutím změníte seriál", "AddNotificationError": "Nebylo možné přidat nové oznámení, prosím zkuste to později.", "AddConditionImplementation": "Přidat podmínku - {implementationName}", - "AddConnectionImplementation": "Přidat spojení - {implementationName}", + "AddConnectionImplementation": "Přidat spojení – {implementationName}", "AddCustomFilter": "Přidat vlastní filtr", "AddDownloadClient": "Přidat klienta pro stahování", "AddDelayProfile": "Přidat profil zpoždění", @@ -159,7 +159,7 @@ "AddImportListExclusion": "Přidat výjimku ze seznamu k importu", "AddImportList": "Přidat importované položky", "AddImportListImplementation": "Přidat seznam k importu - {implementationName}", - "AddIndexerImplementation": "Přidat indexer - {implementationName}", + "AddIndexerImplementation": "Přidat indexer – {implementationName}", "AddQualityProfileError": "Nebylo možné přidat nový profil kvality, prosím zkuste to později.", "AddToDownloadQueue": "Přidat stahování do fronty", "Airs": "Vysíláno", @@ -170,7 +170,7 @@ "CalendarOptions": "Možnosti kalendáře", "AllFiles": "Všechny soubory", "Analytics": "Analýzy", - "AnalyticsEnabledHelpText": "Odesílání anonymních informací o používání a chybách na servery společnosti {appName}. To zahrnuje informace o vašem prohlížeči, o tom, které stránky webového rozhraní {appName} používáte, hlášení chyb a také informace o verzi operačního systému a běhového prostředí. Tyto informace použijeme k určení priorit funkcí a oprav chyb.", + "AnalyticsEnabledHelpText": "Odesílejte anonymní informace o použití a chybách na servery {appName}u. To zahrnuje informace o vašem prohlížeči, které stránky webového rozhraní {appName}u používáte, hlášení chyb a také verzi operačního systému a běhového prostředí. Tyto informace použijeme k určení priorit funkcí a oprav chyb.", "Anime": "Anime", "AnimeEpisodeFormat": "Formát epizod pro Anime", "AnimeEpisodeTypeDescription": "Epizody vydané s použitím absolutního čísla epizody", @@ -182,7 +182,7 @@ "ChooseImportMode": "Vyberte mód importu", "ClickToChangeEpisode": "Kliknutím změníte epizodu", "ClickToChangeLanguage": "Kliknutím změníte jazyk", - "AutomaticSearch": "Vyhledat automaticky", + "AutomaticSearch": "Automatické vyhledávání", "AutomaticUpdatesDisabledDocker": "Automatické aktualizace nejsou při použití aktualizačního mechanismu Docker přímo podporovány. Obraz kontejneru je nutné aktualizovat mimo {appName} nebo použít skript", "Branch": "Větev", "BypassDelayIfAboveCustomFormatScore": "Obejít, pokud je vyšší než skóre vlastního formátu", @@ -202,9 +202,9 @@ "CloneProfile": "Klonovat profil", "CollapseMultipleEpisodes": "Sbalení více epizod", "CollapseMultipleEpisodesHelpText": "Sbalení více epizod vysílaných ve stejný den", - "ConnectionLost": "Spojení ztraceno", + "ConnectionLost": "Ztráta spojení", "ConnectionLostReconnect": "{appName} se pokusí připojit automaticky, nebo můžete kliknout na tlačítko znovunačtení níže.", - "ConnectionLostToBackend": "{appName} ztratil spojení s backendem a pro obnovení funkčnosti bude třebaho znovu načíst.", + "ConnectionLostToBackend": "{appName} ztratil spojení s backendem a pro obnovení funkčnosti bude potřeba ho znovu načíst.", "Continuing": "Pokračující", "CountSeasons": "{count} Řad", "CustomFormat": "Vlastní formát", @@ -229,7 +229,7 @@ "CurrentlyInstalled": "Aktuálně nainstalováno", "CountImportListsSelected": "{count} vybraných seznamů pro import", "CustomFormatScore": "Skóre vlastního formátu", - "CountDownloadClientsSelected": "{count} vybraných klientů ke stahování", + "CountDownloadClientsSelected": "{count} vybraných klientů pro stahování", "CouldNotFindResults": "Nepodařilo se najít žádné výsledky pro '{term}'", "CustomFormatsSettings": "Nastavení vlastních formátů", "CopyToClipboard": "Zkopírovat do schránky", @@ -266,7 +266,7 @@ "Dash": "Pomlčka", "Database": "Databáze", "Date": "Datum", - "Dates": "Termíny", + "Dates": "Data", "DefaultCase": "Výchozí případ", "DailyEpisodeTypeFormat": "Datum ({format})", "Default": "Výchozí", @@ -303,10 +303,10 @@ "FormatAgeHour": "hodina", "FormatAgeHours": "hodin", "AuthenticationMethod": "Metoda ověřování", - "AuthenticationMethodHelpTextWarning": "Prosím vyberte platnou metodu ověřování", - "AuthenticationRequiredPasswordHelpTextWarning": "Vložte nové heslo", + "AuthenticationMethodHelpTextWarning": "Vyberte platnou metodu ověřování", + "AuthenticationRequiredPasswordHelpTextWarning": "Zadejte nové heslo", "EditSelectedIndexers": "Upravit vybrané indexery", - "AuthenticationRequiredUsernameHelpTextWarning": "Vložte nové uživatelské jméno", + "AuthenticationRequiredUsernameHelpTextWarning": "Zadejte nové uživatelské jméno", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrďte nové heslo", "AutoRedownloadFailedFromInteractiveSearchHelpText": "Automaticky vyhledat a pokusit se o stažení jiného vydání, pokud bylo neúspěšné vydání zachyceno z interaktivního vyhledávání", "AutoRedownloadFailed": "Opětovné stažení se nezdařilo", @@ -322,7 +322,7 @@ "ConnectionSettingsUrlBaseHelpText": "Přidá předponu do {connectionName} url, jako např. {url}", "CustomFormatsSpecificationRegularExpressionHelpText": "Vlastní formát RegEx nerozlišuje velká a malá písmena", "CustomFormatsSpecificationFlag": "Vlajka", - "BlackholeFolderHelpText": "Složka do které {appName} uloží {extension} soubor", + "BlackholeFolderHelpText": "Složka, do které {appName} uloží soubor {extension}", "BlackholeWatchFolder": "Složka sledování", "Category": "Kategorie", "BlocklistAndSearch": "Seznam blokovaných a vyhledávání", @@ -330,5 +330,6 @@ "BlocklistReleaseHelpText": "Zabránit {appName} v opětovném sebrání tohoto vydání pomocí RSS nebo automatického vyhledávání", "BlocklistMultipleOnlyHint": "Blokovat a nehledat náhradu", "CustomFormatsSettingsTriggerInfo": "Vlastní formát se použije na vydání nebo soubor, pokud odpovídá alespoň jednomu z různých typů zvolených podmínek.", - "ChangeCategory": "Změnit kategorii" + "ChangeCategory": "Změnit kategorii", + "CustomFilter": "Vlastní filtr" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index a0105cf6d..549c55cf6 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1,14 +1,14 @@ { - "RecycleBinUnableToWriteHealthCheckMessage": "Määritettyyn roskakorikansioon ei voida tallentaa: {path}. Varmista että sijainti on olemassa ja että sovelluksen suorittavalla käyttäjällä on siihen kirjoitusoikeus.", + "RecycleBinUnableToWriteHealthCheckMessage": "Roskakoriksi määritettyyn sijaintiin ei voida tallentaa: {path}. Varmista, että se on olemassa ja että {appName}in suorittavalla käyttäjällä on kirjoitusoikeus kansioon.", "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ää", "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.", + "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalihaulle ei ole määritetty tietolähteitä, eikä {appName} sen vuoksi löydä sillä tuloksia.", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", - "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "Jaksojen tuonti epäonnistui. Katso tarkemmat tiedot lokista.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} ei voinut tuoda jaksoja. Katso tarkemmat tiedot lokista.", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattihakua varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName}in automaattihaku ei löydä tuloksia.", "AgeWhenGrabbed": "Ikä (kaappaushetkellä)", @@ -46,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. ", @@ -55,7 +55,7 @@ "EnableColorImpairedMode": "Heikentyneen värinäön tila", "EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihauille, jotka suoritetaan käyttöliittymästä tai {appName}in toimesta.", "InvalidUILanguage": "Käytöliittymän kielivalinta on virheellinen. Korjaa se ja tallenna asetukset.", - "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt todennuksen käyttöönoton. Todennus voidaan poistaa käytöstä paikallisille osoitteille.", + "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt tunnistautumisen käyttöönoton. Paikallisilta osoitteilta se voidaan valinnaisesti poistaa käytöstä.", "IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä lataustyökalu.", "ProfilesSettingsSummary": "Laatu-, kieli-, viive- ja julkaisuprofiilit.", "ConnectionLostReconnect": "{appName} pyrkii ajoittain muodostamaan yhteyden automaattisesti tai voit painaa alta \"Lataa uudelleen\".", @@ -132,7 +132,7 @@ "AfterManualRefresh": "Manuaalisen päivityksen jälkeen", "AddRemotePathMappingError": "Etäsijainnin kohdistuksen lisäys epäonnistui. Yritä uudelleen.", "ApplicationURL": "Sovelluksen URL", - "AuthBasic": "Perus (ponnahdusikkuna)", + "AuthBasic": "Perus (selaimen ponnahdus)", "AuthForm": "Lomake (kirjautumissivu)", "Backup": "Varmuuskopiointi", "AutomaticSearch": "Automaattihaku", @@ -193,7 +193,7 @@ "LibraryImportTipsQualityInEpisodeFilename": "Varmista, että tuotavien tiedostojen nimissä mainitaan laatutiedot, kuten esim. \"episode.s02e15.bluray.mkv\".", "IndexerIPTorrentsSettingsFeedUrl": "Syötteen URL-osoite", "IndexerSettingsAnimeStandardFormatSearch": "Animen vakiomuotohaku", - "IndexerSettingsApiPathHelpText": "Polku API:in (yleensä {url}).", + "IndexerSettingsApiPathHelpText": "Polku rajapintaan (yleensä {url}).", "IndexerSettingsApiPath": "API:n polku", "IndexerSettingsCategories": "Kategoriat", "IndexerSettingsSeedRatio": "Jakosuhde", @@ -230,7 +230,7 @@ "NoIndexersFound": "Tietolähteitä ei löytynyt", "NamingSettingsLoadError": "Virhe ladattaessa nimeämisasetuksia", "OnSeriesAdd": "Kun sarja lisätään", - "ParseModalHelpText": "Syötä julkaisunimike yllä olevaan kenttään.", + "ParseModalHelpText": "Syötä julkaisun nimi yllä olevaan kenttään.", "Path": "Tiedostosijainti", "PreviousAiring": "Edellinen esitys", "NoIssuesWithYourConfiguration": "Kokoonpanossasi ei ole ongelmia.", @@ -245,7 +245,7 @@ "RemoveFromBlocklist": "Poista estolistalta", "RemoveQueueItem": "Poistetaan - {sourceTitle}", "RemoveFromQueue": "Poista jonosta", - "RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, käytetään nykyistä tiedostonimeä.", + "RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, {appName} käyttää nykyistä tiedostonimeä.", "RestoreBackup": "Palauta varmuuskopio", "RestrictionsLoadError": "Virhe ladattaessa rajoituksia", "SceneInfo": "Kohtaustiedot", @@ -323,7 +323,7 @@ "SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä tietolähteistä painamalla niiden ohessa olevia lisätietopainikkeita.", "Status": "Tila", "SupportedListsSeries": "{appName} tukee useita listoja, joilta sarjoja voidaan tuoda tietokantaan.", - "SystemTimeHealthCheckMessage": "Järjestelmän ajassa on ainakin vuorokauden heitto eivätkä ajoitetut tehtävät tämän vuoksi toimi oikein ennen kuin se on korjattu.", + "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.", "TagsLoadError": "Virhe ladattaessa tunnisteita", "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tomorrow": "Huomenna", @@ -383,7 +383,7 @@ "ApplyTagsHelpTextReplace": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi", "CustomFormatScore": "Mukautetun muodon pisteytys", "SeriesMatchType": "Sarjan kohdistustyyppi", - "RemotePathMappingLocalPathHelpText": "Polku, jonka kautta etäsijaintia tulee käyttää paikallisesti.", + "RemotePathMappingLocalPathHelpText": "Sijainti, jonka kautta {appName}in tulee käyttää etäsijaintia paikallisesti.", "SonarrTags": "{appName}in tunnisteet", "CalendarLoadError": "Kalenterin lataus epäonnistui.", "BeforeUpdate": "Ennen päivitystä", @@ -414,7 +414,7 @@ "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Lataa tiedostot järjestyksessä (qBittorrent 4.1.0+).", "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd:n develop-versio, oletettavasti vähintään versio 3.0.0.", "Daily": "Päivittäinen", - "CutoffUnmetNoItems": "Katkaisutasoa saavuttamattomia kohteita ei ole.", + "CutoffUnmetNoItems": "Katkaisutasoa saavuttamattomia kohteita ei ole", "DailyEpisodeFormat": "Päivittäisjaksojen kaava", "DelayProfileProtocol": "Protokolla: {preferredProtocol}", "Day": "Päivä", @@ -462,7 +462,7 @@ "ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein", "ListExclusionsLoadError": "Virhe ladattaessa listapoikkeuksia", "ManualImportItemsLoadError": "Virhe ladattaessa manuaalisen tuonnin kohteita.", - "MediaManagementSettingsSummary": "Tiedostojen nimeämisen, hallinnan ja juurikansioiden asetukset.", + "MediaManagementSettingsSummary": "Tiedostojen nimeämis- ja hallinta-asetukset, sekä kirjaston juurikansiot.", "Message": "Viesti", "MetadataSettings": "Metatietoasetukset", "MetadataLoadError": "Virhe ladattaessa metatietoja", @@ -549,7 +549,7 @@ "ShowMonitored": "Näytä valvontatila", "ShowEpisodeInformationHelpText": "Näytä jakson nimi ja numero.", "ShowAdvanced": "Näytä lisäasetukset", - "ShowMonitoredHelpText": "Näytä valvonnan tila julisteen alla.", + "ShowMonitoredHelpText": "Näytä valvontatila julisteen alla.", "ShowNetwork": "Näytä kanava/tuottaja", "ShowPath": "Näytä tiedostosijainti", "SkipFreeSpaceCheck": "Ohita vapaan tilan tarkastus", @@ -600,8 +600,8 @@ "DownloadClientQbittorrentTorrentStateError": "qBittorrent ilmoittaa virheestä", "RemotePath": "Etäsijainti", "DownloadClientQbittorrentValidationQueueingNotEnabled": "Jonotus ei ole käytössä", - "DownloadClientValidationErrorVersion": "{clientName} version tulee olla vähintään {requiredVersion}. Ilmoitettu versio on {reportedVersion}.", - "DownloadClientVuzeValidationErrorVersion": "Protokollan versiota ei tueta. Käytä vähintään Vuze-versiota 5.0.0.0 sekä Vuze Web Remote -lisäosaa.", + "DownloadClientValidationErrorVersion": "Lataustyökalun {clientName} version tulee olla vähintään {requiredVersion}. Ilmoitettu versio on {reportedVersion}.", + "DownloadClientVuzeValidationErrorVersion": "Protokollaversiota ei tueta. Käytä vähintään Vuzen versiota 5.0.0.0 Vuze Web Remote -lisäosan kanssa.", "DownloadStationStatusExtracting": "Puretaan: {progress} %", "EditDelayProfile": "Muokkaa viiveprofiilia", "EditConnectionImplementation": "Muokataan kytköstä - {implementationName}", @@ -630,7 +630,7 @@ "CalendarFeed": "{appName}in kalenterisyöte", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Jaksoa ladataan", - "ListOptionsLoadError": "Virhe ladattaessa tuontilista-asetuksia", + "ListOptionsLoadError": "Lista-asetuksia ei voida ladata.", "RemoveCompleted": "Poisto on suoritettu", "ICalShowAsAllDayEvents": "Näytä koko päivän tapahtumina", "FailedToLoadTagsFromApi": "Tunnisteiden lataus rajapinnasta epäonnistui", @@ -713,16 +713,16 @@ "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", "TimeLeft": "Aikaa jäljellä", "Time": "Aika", - "UpdateAvailableHealthCheckMessage": "Uusi päivitys on saatavilla", + "UpdateAvailableHealthCheckMessage": "Uusi päivitys on saatavilla: {version}", "SupportedDownloadClientsMoreInfo": "Saat tietoja yksittäisistä lataustyökaluista painamalla niiden ohessa olevia lisätietopainikkeita.", - "SupportedDownloadClients": "Monet torrent- ja Usenet-lataajat ovat tuettuja.", + "SupportedDownloadClients": "{appName} tukee monia torrent- ja Usenet-lataajia.", "SupportedImportListsMoreInfo": "Saat tietoja yksittäisistä tuontilistoista painamalla niiden ohessa olevia lisätietopainikkeita.", "System": "Järjestelmä", "TotalFileSize": "Kokonaistiedostokoko", "UseSsl": "Käytä SSL-salausta", "Yes": "Kyllä", "AddNewSeries": "Lisää uusi sarja", - "MissingLoadError": "Virhe ladattaessa puuttuvia kohteita", + "MissingLoadError": "Virhe ladattaessa puuttuvia kohteita.", "AuthenticationRequiredUsernameHelpTextWarning": "Syötä uusi käyttäjätunnus", "BlocklistLoadError": "Virhe ladattaessa estolistaa", "Database": "Tietokanta", @@ -732,7 +732,7 @@ "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kohdistukset ja säiliön tallennusmedian asetukset.", "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", - "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", + "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. {appName} käyttää edelleen kaikkia käytössä olevia tietolähteitä RSS-synkronointiin ja hakuun.", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista]({url})", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakosuhderajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", @@ -814,7 +814,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", @@ -832,8 +832,8 @@ "CutoffUnmetLoadError": "Virhe ladattaessa katkaisutasoa saavuttamattomia kohteita", "CountSeriesSelected": "{count} sarjaa on valittu", "CopyUsingHardlinksSeriesHelpText": "Hardlink-kytkösten avulla {appName} voi tuoda jaettavat torrentit ilman niiden täyttä kopiointia ja levytilan kaksinkertaista varausta. Tämä toimii vain lähde- ja kohdesijaintien ollessa samalla tallennusmedialla.", - "CutoffUnmet": "Katkaisutasoa ei savutettu", - "CurrentlyInstalled": "Nykyinen asennettu versio", + "CutoffUnmet": "Katkaisutasoa ei saavutettu", + "CurrentlyInstalled": "Tällä hetkellä asennettu versio", "DeleteSelectedDownloadClientsMessageText": "Haluatko varmasti poistaa {count} valit(n/tua) lataustyökalu(n/a)?", "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa lataustyökalun \"{name}\"?", "DeleteSelectedDownloadClients": "Poista lataustyökalu(t)", @@ -857,7 +857,7 @@ "DeletedReasonEpisodeMissingFromDisk": "{appName} ei löytänyt tiedostoa levyltä, joten sen kytkös tietokonnassa olevaan jaksoon purettiin.", "Details": "Tiedot", "DownloadClient": "Lataustyökalu", - "DisabledForLocalAddresses": "Ei käytössä paikallisille osoitteille", + "DisabledForLocalAddresses": "Ei käytössä paikallisissa osoitteissa", "DownloadClientDelugeValidationLabelPluginFailure": "Label-tunnisteen määritys epäonnistui.", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorioiden käyttö edellyttää, että {clientName}n Label-tunnistelisäosa on käytössä.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisää median ominaisuuksia tunnisteina. Vihjeet ovat esimerkkejä.", @@ -917,17 +917,17 @@ "LibraryImport": "Kirjastoon tuonti", "Logout": "Kirjaudu ulos", "IndexerSettings": "Tietolähdeasetukset", - "IncludeHealthWarnings": "Sisällytä terveysvaroitukset", + "IncludeHealthWarnings": "Sisällytä kuntovaroitukset", "ListsLoadError": "Virhe ladattaessa listoja", "IndexerValidationUnableToConnect": "Tietolähdettä ei tavoiteta: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", - "MassSearchCancelWarning": "Tätä ei ole mahdollista pysäyttää kuin käynnistämällä {appName}ia uudelleen tai poistamalla kaikki tietolähteet käytöstä.", + "MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.", "MetadataSourceSettingsSeriesSummary": "Tietoja siitä, mistä {appName} saa sarjojen ja jaksojen tiedot.", "NoHistory": "Historiaa ei ole", "MustContainHelpText": "Julkaisun on sisällettävä ainakin yksi näistä termeistä (kirjainkoolla ei ole merkitystä).", "NoEpisodesFoundForSelectedSeason": "Valitulle tuotantokaudelle ei löytynyt jaksoja.", "MonitorFutureEpisodesDescription": "Valvo jaksoja, joita ei ole vielä esitetty.", - "MissingNoItems": "Puuttuvia kohteita ei ole.", + "MissingNoItems": "Puuttuvia kohteita ei ole", "Mode": "Tila", "NextExecution": "Seuraava suoritus", "PreviewRename": "Nimeämisen esikatselu", @@ -1004,7 +1004,7 @@ "RemoveSelected": "Poista valitut", "RemoveSelectedBlocklistMessageText": "Haluatko varmasti poistaa valitut kohteet estolistalta?", "RemoveSelectedItemsQueueMessageText": "Haluatko varmasti poistaa jonosta {selectedCount} kohdetta?", - "ReplaceIllegalCharactersHelpText": "Korvaa laittomat merkit vaihtoehtoisella merkinnällä. Jos ei valittu, ne poistetaan.", + "ReplaceIllegalCharactersHelpText": "Korvaa laittomat merkit vaihtoehtoisella merkinnällä. Jos ei valittu, {appName} poistaa ne.", "ResetQualityDefinitions": "Palauta laatumääritykset", "RestartRequiredToApplyChanges": "{appName} on käynnistettävä uudelleen muutosten käyttöönottamiseksi. Haluatko tehdä sen nyt?", "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", @@ -1016,7 +1016,7 @@ "Year": "Vuosi", "WeekColumnHeader": "Viikkosarakkeen otsikko", "UpdateStartupNotWritableHealthCheckMessage": "Päivitystä ei voida asentaa, koska käyttäjällä \"{userName}\" ei ole kirjoitusoikeutta käynnistyskansioon \"{startupFolder}\".", - "UpgradeUntilEpisodeHelpText": "Kun tämä laatutaso saavutetaan, {appName} ei enää lataa jakoja.", + "UpgradeUntilEpisodeHelpText": "Kun tämä laatutaso on saavutettu, ei {appName} enää kaappaa jaksoja.", "No": "Ei", "CustomFilters": "Omat suodattimet", "CopyUsingHardlinksHelpTextWarning": "Tiedostojen käsittelystä johtuvat lukitukset saattavat joskus estää jaettavien tiedostojen uudelleennimeämisen. Voit keskeyttää jakamisen väliaikaisesti ja käyttää {appName}in nimeämistoimintoa.", @@ -1043,7 +1043,7 @@ "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muta, jos et ymmärrä riskejä.", "Certification": "Varmennus", "ChangeFileDate": "Muuta tiedoston päiväys", - "DelayingDownloadUntil": "Lataus on siirretty alkamaan {date} klo {time}.", + "DelayingDownloadUntil": "Lataus on lykätty alkamaan {date} klo {time}", "DeleteImportListExclusion": "Poista tuontilistapoikkeus", "Enabled": "Käytössä", "Age": "Ikä", @@ -1171,7 +1171,7 @@ "DownloadPropersAndRepacks": "Proper- ja repack-julkaisut", "DownloadPropersAndRepacksHelpText": "Määrittää päivitetäänkö tiedostot automaattisesti Proper- ja Repack-julkaisuihin (kunnollinen/uudelleenpaketoitu).", "SingleEpisode": "Yksittäinen jakso", - "SmartReplaceHint": "Yhdysmerkki tai välilyönti nimen perusteella", + "SmartReplaceHint": "\"Yhdysmerkki\" tai \"Välilyönti Yhdysmerkki\" nimen perusteella.", "FormatAgeHour": "tunti", "FormatAgeMinute": "minuutti", "InteractiveImportNoSeries": "Jokaisen valitun tiedoston sarja on määritettävä.", @@ -1193,7 +1193,7 @@ "AddListExclusion": "Lisää listapoikkeus", "AddedDate": "Lisätty: {date}", "Anime": "Anime", - "Any": "Mikä vain", + "Any": "Mikä tahansa", "ClickToChangeSeason": "Vaihda tuotantokautta painamalla tästä", "CountSelectedFile": "{selectedCount} tiedosto on valittu", "SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava", @@ -1217,7 +1217,7 @@ "DelayProfiles": "Viiveprofiilit", "DeleteRemotePathMappingMessageText": "Haluatko varmasti poistaa tämän etäsijainnin kohdistuksen?", "Deleted": "Poistettu", - "DeletedReasonManual": "Tiedosto poistettiin käyttöliittymän kautta", + "DeletedReasonManual": "Tiedosto poistettiin {appName}illa, joko manuaalisesti tai rajapinnan välityksellä jonkin muun työkalun pyynnöstä.", "DestinationPath": "Kohdesijainti", "DestinationRelativePath": "Kohde suhteessa polkuun", "Disabled": "Ei käytössä", @@ -1262,12 +1262,12 @@ "BypassProxyForLocalAddresses": "Ohjaa paikalliset osoitteet välityspalvelimen ohi", "DeleteSelectedEpisodeFilesHelpText": "Haluatko varmasti poistaa valitut jaksotiedostot?", "ReplaceWithDash": "Korvaa yhdysmerkillä", - "ConnectSettingsSummary": "Ilmoitukset, kuten viestintä mediapalvelimille ja soittimille, sekä omat komentosarjat.", + "ConnectSettingsSummary": "Ilmoitukset, yhteydet mediapalvelimiin ja soittimiin, sekä mukautetut komentosarjat.", "DockerUpdater": "Hanki päivitys päivittämällä Docker-säiliö", "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", "ManualGrab": "Manuaalinen kaappaus", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "Valinnainen jaettu kansio latauksille. Jätä tyhjäksi käyttääksesi Download Stationin oletussijaintia.", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Vaihtoehtoinen jaettu kansio latauksille. Käytä Download Stationin oletussijaintia jättämällä tyhjäksi.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", "Table": "Taulukko", @@ -1428,7 +1428,7 @@ "Retention": "Säilytys", "ShortDateFormat": "Lyhyen päiväyksen esitys", "Unknown": "Tuntematon", - "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Mikään tietolähde ei ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.", "OrganizeSelectedSeriesModalAlert": "Vinkki: Esikatsellaksesi nimeämistä, paina \"Peruuta\" ja valitse jokin sarjanimike ja paina tätä kuvaketta:", "Socks5": "SOCKS5 (TOR-tuki)", "EditMetadata": "Muokkaa metatietoa {metadataType}", @@ -1492,7 +1492,7 @@ "Rejections": "Hylkäykset", "NoImportListsFound": "Tuotilistoja ei löytynyt", "OnManualInteractionRequired": "Kun tarvitaan manuaalisia toimenpiteitä", - "RetryingDownloadOn": "Latausta yritetään uudelleen {date} klo {time}.", + "RetryingDownloadOn": "Yritetään latausta uudelleen {date} klo {time}", "BlocklistAndSearch": "Estolista ja haku", "BlocklistAndSearchHint": "Etsi korvaavaa kohdetta kun kohde lisätään estolistalle.", "BlocklistOnlyHint": "Lisää estolistalle etsimättä korvaavaa kohdetta.", @@ -1517,7 +1517,7 @@ "NotificationsJoinSettingsDeviceIds": "Laite-ID:t", "NotificationsJoinSettingsApiKeyHelpText": "Join-tilisi asetuksista löytyvä rajapinnan (API) avain (paina Join API -painiketta).", "NotificationsJoinSettingsDeviceNames": "Laitenimet", - "NotificationsKodiSettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö Kodin kirjasto tuonnin tai uudelleennimeämisen yhteydessä.", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö Kodin kirjasto tuonnin ja uudelleennimeämisen yhteydessä.", "NotificationsMailgunSettingsApiKeyHelpText": "MailGunissa luotu rajapinnan (API) avain.", "NotificationsMailgunSettingsUseEuEndpoint": "Käytä EU-päätepistettä", "NotificationsNtfySettingsAccessToken": "Käyttötunniste", @@ -1584,7 +1584,7 @@ "NotificationsPushcutSettingsTimeSensitiveHelpText": "Merkitsee ilmoituksen kiireelliseksi (\"Time Sensitive\").", "NotificationsPushcutSettingsNotificationNameHelpText": "Ilmoituksen nimi Pushcut-sovelluksen ilmoitusvälilehdeltä.", "NotificationsPushoverSettingsSound": "Ääni", - "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "{appName}-sijainti, jonka perusteella sarjojen sijainteja muutetaan kun {serviceName} näkemä kirjastosijainti poikkeaa {appName}in sijainnista (vaatii \"Päivitä kirjasto\" -asetuksen).", "NotificationsPushoverSettingsExpire": "Erääntyminen", "NotificationsSendGridSettingsApiKeyHelpText": "SendGridin luoma rajapinnan (API) avain.", "NotificationsSimplepushSettingsEventHelpText": "Mukauta push-ilmoitusten toimintaa.", @@ -1603,7 +1603,7 @@ "Or": "tai", "UpdateFiltered": "Päivitä suodatetut", "DownloadClientPriorityHelpText": "Lautaustyökalujen painotus, 1– 50 (korkein-alin). Oletusarvo on 1 ja tasaveroiset erotetaan Round-Robin-tekniikalla.", - "NotificationsEmbySettingsSendNotificationsHelpText": "Ohjeista palvelinta välittämään ilmoitukset sen määritettyihin kohteisiin.", + "NotificationsEmbySettingsSendNotificationsHelpText": "Ohjeista Embyä lähettämään ilmoitukset sen määritettyihin kohteisiin. Ei toimi Jellyfinin kanssa.", "NotificationsEmbySettingsSendNotifications": "Lähetä ilmoitukset", "NotificationsDiscordSettingsOnGrabFields": "Kaappausilmoitusten tietueet", "NotificationsGotifySettingsAppTokenHelpText": "Gotifyn luoma sovellustunniste.", @@ -1614,7 +1614,7 @@ "NotificationsKodiSettingsDisplayTime": "Näytä aika", "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", "NotificationsSettingsUseSslHelpText": "Muodosta yhteys sovellukseen {serviceName} SSL-protokollan välityksellä.", - "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}-sijainti, jonka perusteella sarjojen sijainteja muutetaan kun {serviceName} näkemä kirjastosijainti poikkeaa {appName}in sijainnista (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.", "NotificationsTraktSettingsAccessToken": "Käyttötunniste", @@ -1622,7 +1622,7 @@ "NotificationsValidationUnableToSendTestMessage": "Testiviestin lähetys ei onnistu: {exceptionMessage}", "NotificationsValidationUnableToSendTestMessageApiResponse": "Testiviestin lähetys ei onnistu. API vastasi: {error}", "NotificationsEmailSettingsUseEncryption": "Käytä salausta", - "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimikkeen ja näyttämään sen tiedot.", + "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimen ja näyttämään sen tiedot.", "ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.", "NotificationsTwitterSettingsConsumerKeyHelpText": "Kuluttajan avain (consumer key) X (Twitter) -sovelluksesta.", "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", @@ -1721,14 +1721,14 @@ "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Yli 14 päivää sitten julkaistujen jaksojen kaappauksille käytettävä painotus.", "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Trakt-lajityyppien slug-arvoilla (pilkuin eroteltuna). Koskee vain suosituimpia listoja.", "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse sarjatyyppi, jonka haluat tuoda.", - "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja [UKK:sta]({url}).", + "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja UKK:sta ({url}).", "MetadataSettingsSeriesMetadata": "Sarjojen metatiedot", "MetadataSettingsSeriesMetadataUrl": "Sarjojen metatietojen URL", "MetadataSettingsSeriesMetadataEpisodeGuide": "Sarjojen metatietojen jakso-opas", "SomeResultsAreHiddenByTheAppliedFilter": "Aktiivinen suodatin piilottaa joitakin tuloksia.", "ChangeCategoryHint": "Vaihtaa latauksen kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", "ChangeCategoryMultipleHint": "Vaihtaa latausten kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", - "DownloadClientAriaSettingsDirectoryHelpText": "Valinnainen latuasten tallennussijainti. Käytä Aria2-oletusta jättämällä tyhjäksi.", + "DownloadClientAriaSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Aria2:n oletusta jättämällä tyhjäksi.", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrentien jonotus ei ole käytössä qBittorent-asetuksissasi. Ota se käyttöön tai valitse painotukseksi \"Viimeiseksi\".", "DownloadClientSettingsCategorySubFolderHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa. Tämä luo latauskansioon [kategoria]-alikansion.", "ListQualityProfileHelpText": "Laatuprofiili, joka listalta lisätyille kohteille asetetaan.", @@ -1784,7 +1784,7 @@ "DownloadClientValidationTestNzbs": "NZB-listausten nouto epäonnistui: {exceptionMessage}.", "DownloadClientValidationUnableToConnect": "Lataustyökalua {clientName} ei tavoitettu", "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latauskansio. Käytä Transmissionin oletusta jättämällä tyhjäksi.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.", "AddDelayProfileError": "Virhe lisättäessä viiveporofiilia. Yritä uudelleen.", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", @@ -1805,6 +1805,32 @@ "CustomFormatsSpecificationFlag": "Lippu", "SelectIndexerFlags": "Valitse tietolähteen liput", "SetIndexerFlagsModalTitle": "{modalTitle} - Aseta tietolähteen liput", - "CustomFilter": "Oma suodatin", - "Label": "Nimi" + "CustomFilter": "Mukautettu suodatin", + "Label": "Nimi", + "ShowTagsHelpText": "Näytä tunnisteet julisteen alla.", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Kun tämä mukautetun muodon pisteytys on saavutettu, ei {appName} enää kaappaa jaksoja.", + "IndexerFlags": "Tietolähteen liput", + "AutoTaggingSpecificationTag": "Tunniste", + "ImportListsSimklSettingsUserListTypeCompleted": "Katseltu", + "UrlBaseHelpText": "Käänteisen välityspalvelimen tukea varten. Oletusarvo on tyhjä.", + "IndexerSettingsMultiLanguageRelease": "Useat kielet", + "ReleaseProfileIndexerHelpTextWarning": "Jos julkaisuprofiilille määritetään tietty tietolähde, koskee se vain kyseisen tietolähteen julkaisuja.", + "ConnectionSettingsUrlBaseHelpText": "Lisää palvelimen {connectionName} URL-osoitteeseen etuliite, kuten \"{url}\".", + "Script": "Skripti", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Vaihtoehtoinen sijainti, johon valmistuneet lataukset siirretään. Käytä Delugen oletusta jättämällä tyhjäksi.", + "DownloadClientDelugeSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Delugen oletusta jättämällä tyhjäksi.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä rTorrentin oletusta jättämällä tyhjäksi.", + "DeleteSelected": "Poista valitut", + "NotificationsPlexSettingsServer": "Palvelin", + "Completed": "Katseltu", + "DeleteSelectedImportListExclusionsMessageText": "Haluatko varmasti poistaa valitut tuontilistapoikkeukset?", + "PublishedDate": "Julkaisupäivä", + "DeleteSelectedCustomFormatsMessageText": "Haluatko varmasti poistaa valitut {count} mukautettua muotoa?", + "DownloadClientValidationGroupMissingDetail": "Syötettyä ryhmää ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.", + "ImportListsAniListSettingsImportCancelled": "Tuonti peruttiin", + "ImportListsAniListSettingsImportCancelledHelpText": "Media: sarja on lopetettu", + "FolderNameTokens": "Kansionimimuuttujat", + "Delay": "Viive", + "DeleteSelectedCustomFormats": "Poista mukautetut muodot", + "DownloadClientUnavailable": "Lataustyökalu ei ole käytettävissä" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index f35182a06..a5f033412 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -250,7 +250,7 @@ "AutoRedownloadFailed": "Download fallito", "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", "Cutoff": "Taglio", - "AddListExclusion": "Aggiungi Lista esclusioni", + "AddListExclusion": "Aggiungi elenco esclusioni", "DownloadClientValidationApiKeyRequired": "API Key Richiesta", "Donate": "Dona", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nessuna destinazione predefinita", diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 7ea8e1ef1..ca88a4036 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -209,5 +209,7 @@ "CloneIndexer": "Clonează Indexer", "CloneProfile": "Clonează Profil", "DownloadClientUnavailable": "Client de descărcare indisponibil", - "Clone": "Clonează" + "Clone": "Clonează", + "DownloadClientSettingsOlderPriority": "Prioritate mai vechi", + "DownloadClientSettingsRecentPriority": "Prioritate recente" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index 5313dcd31..bccc347ed 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -52,5 +52,20 @@ "AddNewRestriction": "加入新的限制", "AuthenticationRequiredWarning": "為防止未經認證的遠程訪問,{appName} 現需要啟用身份認證。您可以選擇禁用本地地址的身份認證。", "AddRemotePathMappingError": "無法加入新的遠程路徑對應,請重試。", - "AnalyseVideoFilesHelpText": "從文件中提取影像資訊,如解析度、運行環境和編解碼器資訊。這需要 {appName} 在掃描期間讀取文件並可能導致高磁盤或網絡佔用。" + "AnalyseVideoFilesHelpText": "從文件中提取影像資訊,如解析度、運行環境和編解碼器資訊。這需要 {appName} 在掃描期間讀取文件並可能導致高磁盤或網絡佔用。", + "AddDelayProfileError": "無法加入新的延遲配置,請重新嘗試。", + "AddImportListExclusionError": "無法加入新的導入列表排除,請重試。", + "AddIndexerError": "無法加入新的索引器,請重試。", + "AddList": "加入清單", + "AddListError": "無法加入新的清單,請重試。", + "AddANewPath": "新增新的路徑", + "AddCustomFormat": "加入自訂格式", + "AddCustomFormatError": "無法加入自訂格式,請重試。", + "AddDownloadClient": "加入下載用戶端", + "AddDownloadClientError": "無法加入下載用戶端,請重試。", + "AddExclusion": "加入排除", + "AbsoluteEpisodeNumbers": "絕對集數", + "AbsoluteEpisodeNumber": "絕對集數", + "AddListExclusionError": "無法加入新的清單排除,請重試。", + "AddNew": "加入新的" } From 514c04935f78011556d9affe87ed2bb85bae497a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:33:08 +0200 Subject: [PATCH 720/762] Fixed: Advanced settings for Metadata consumers --- .../Settings/Metadata/Metadata/EditMetadataModal.tsx | 11 +++++++++-- frontend/src/Settings/Metadata/Metadata/Metadata.tsx | 1 - 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx index 6dd30ca78..9731a39ab 100644 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx @@ -1,5 +1,6 @@ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; import Modal from 'Components/Modal/Modal'; import { sizes } from 'Helpers/Props'; import { clearPendingChanges } from 'Store/Actions/baseActions'; @@ -7,7 +8,8 @@ import EditMetadataModalContent, { EditMetadataModalContentProps, } from './EditMetadataModalContent'; -interface EditMetadataModalProps extends EditMetadataModalContentProps { +interface EditMetadataModalProps + extends Omit<EditMetadataModalContentProps, 'advancedSettings'> { isOpen: boolean; } @@ -18,6 +20,10 @@ function EditMetadataModal({ }: EditMetadataModalProps) { const dispatch = useDispatch(); + const advancedSettings = useSelector( + (state: AppState) => state.settings.advancedSettings + ); + const handleModalClose = useCallback(() => { dispatch(clearPendingChanges({ section: 'metadata' })); onModalClose(); @@ -27,6 +33,7 @@ function EditMetadataModal({ <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}> <EditMetadataModalContent {...otherProps} + advancedSettings={advancedSettings} onModalClose={handleModalClose} /> </Modal> diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.tsx b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx index 52797218d..bb988d0d9 100644 --- a/frontend/src/Settings/Metadata/Metadata/Metadata.tsx +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx @@ -95,7 +95,6 @@ function Metadata({ id, name, enable, fields }: MetadataProps) { ) : null} <EditMetadataModal - advancedSettings={false} id={id} isOpen={isEditMetadataModalOpen} onModalClose={handleModalClose} From c885fb81f9fe2395984ded8c0b275f9c675915cc Mon Sep 17 00:00:00 2001 From: Harry Pollard <harry@meharryp.xyz> Date: Thu, 26 Dec 2024 19:40:35 +0000 Subject: [PATCH 721/762] Fixed: Searching by title not using all titles --- src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index fddd5168e..43a9516ee 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -454,7 +454,7 @@ namespace NzbDrone.Core.Indexers.Newznab searchCriteria, $"&season={NewznabifySeasonNumber(searchCriteria.SeasonNumber)}"); - var queryTitles = TextSearchEngine == "raw" ? searchCriteria.SceneTitles : searchCriteria.CleanSceneTitles; + var queryTitles = TextSearchEngine == "raw" ? searchCriteria.AllSceneTitles : searchCriteria.CleanSceneTitles; foreach (var queryTitle in queryTitles) { @@ -582,7 +582,7 @@ namespace NzbDrone.Core.Indexers.Newznab } else if (SupportsTvQuerySearch) { - var queryTitles = TvTextSearchEngine == "raw" ? searchCriteria.SceneTitles : searchCriteria.CleanSceneTitles; + var queryTitles = TvTextSearchEngine == "raw" ? searchCriteria.AllSceneTitles : searchCriteria.CleanSceneTitles; foreach (var queryTitle in queryTitles) { chain.Add(GetPagedRequests(MaxPages, From fae24e98fb9230c2f3701caef457332952c6723f Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 26 Dec 2024 12:37:45 -0800 Subject: [PATCH 722/762] Don't send session information to Sentry Closes #7518 --- src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 0c8ed2f78..e92f8b208 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -110,7 +110,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry o.Environment = BuildInfo.Branch; // Crash free run statistics (sends a ping for healthy and for crashes sessions) - o.AutoSessionTracking = true; + o.AutoSessionTracking = false; // Caches files in the event device is offline // Sentry creates a 'sentry' sub directory, no need to concat here From ef358e6f24a7b57707da80b74df8d9ef4b344da1 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 29 Dec 2024 17:32:02 +0000 Subject: [PATCH 723/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Alexander Balya <alexander.balya@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 898 +++++++++++++------- src/NzbDrone.Core/Localization/Core/ru.json | 2 +- src/NzbDrone.Core/Localization/Core/tr.json | 128 +-- 3 files changed, 670 insertions(+), 358 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 549c55cf6..f7fbc2413 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -3,20 +3,20 @@ "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ää", - "DownloadClientSortingHealthCheckMessage": "Lataustyökalun \"{downloadClientName}\" {sortingMode} on kytketty käyttöön {appName}in kategorialle ja tuontiongelmien välttämiseksi se tulisi poistaa käytöstä.", + "DownloadClientSortingHealthCheckMessage": "Latauspalvelun \"{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ä {appName} sen vuoksi löydä sillä tuloksia.", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} ei voinut tuoda jaksoja. Katso tarkemmat tiedot lokista.", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattihakua varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName}in automaattihaku ei löydä tuloksia.", "AgeWhenGrabbed": "Ikä (kaappaushetkellä)", "GrabId": "Kaappauksen tunniste", "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", - "BrowserReloadRequired": "Käyttöönotto vaatii selaimen sivupäivityksen.", + "BrowserReloadRequired": "Vaatii selaimen sivupäivityksen (F5).", "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.", + "RemotePathMappingHostHelpText": "Sama osoite, joka on määritetty etälatauspalvelulle.", "AudioLanguages": "Äänen kielet", "Grabbed": "Kaapattu", "GrabSelected": "Kaappaa valitut", @@ -33,7 +33,7 @@ "MultiLanguages": "Monikielinen", "Permissions": "Käyttöoikeudet", "RestartRequiredWindowsService": "Jotta palvelu käynnistyisi automaattisesti, voi suorittavasta käyttäjästä riippuen olla tarpeellista suorittaa {appName} kerran järjestelmänvalvojan oikeuksilla.", - "SelectLanguageModalTitle": "{modalTitle} - Valitse kieli", + "SelectLanguageModalTitle": "{modalTitle} – Valitse kieli", "SelectLanguage": "Valitse kieli", "SelectLanguages": "Valitse kielet", "ShowRelativeDatesHelpText": "Korvaa absoluuttiset päiväykset suhteellisilla päiväyksillä (tänään/eilen/yms.).", @@ -56,26 +56,26 @@ "EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihauille, jotka suoritetaan käyttöliittymästä tai {appName}in toimesta.", "InvalidUILanguage": "Käytöliittymän kielivalinta on virheellinen. Korjaa se ja tallenna asetukset.", "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt tunnistautumisen käyttöönoton. Paikallisilta osoitteilta se voidaan valinnaisesti poistaa käytöstä.", - "IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä lataustyökalu.", + "IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä latauspalvelu.", "ProfilesSettingsSummary": "Laatu-, kieli-, viive- ja julkaisuprofiilit.", "ConnectionLostReconnect": "{appName} pyrkii ajoittain muodostamaan yhteyden automaattisesti tai voit painaa alta \"Lataa uudelleen\".", - "LanguagesLoadError": "Virhe ladattaessa kieliä", + "LanguagesLoadError": "Kielien lataus epäonnistui", "OverrideGrabNoLanguage": "Ainakin yksi kieli on valittava.", - "ChmodFolderHelpTextWarning": "Tämä toimii vain, jos käyttäjä suorittaa {appName}in tiedoston omistajana. Parempi vaihtoehto on varmistaa, että lataustyökalu asettaa oikeudet oikein.", + "ChmodFolderHelpTextWarning": "Toimii vain, jos käyttäjä suorittaa {appName}in tiedoston omistajana. Parempi vaihtoehto on varmistaa, että latauspalvelu asettaa oikeudet oikein.", "InteractiveImportNoLanguage": "Jokaisen valitun tiedoston kieli on määritettävä.", "MediaInfoFootNote": "MediaInfo Full, AudioLanguages ja SubtitleLanguages tukevat \":EN+FI\"-tyylisiä jälkiliitteitä, joiden avulla tiedostonimeen voidaan lisätä videon sisältämiä kieliä. \"-\" ohittaa tietyt kielet (esim. \"-EN\") ja \"+\"-pääte (esim. \":FI+\") tuottaa ohitettavista kielistä riippuen \"[FI]\", \"[FI+--]\" tai \"[--]\". Esimerkiksi \"{MediaInfo Full:FI+EN}\".", "Host": "Osoite", "AddAutoTagError": "Virhe lisättäessä automaattimerkintää. Yritä uudelleen.", - "AbsoluteEpisodeNumber": "Täysi jakson numero", + "AbsoluteEpisodeNumber": "Absoluuttinen jaksonumero", "AddConditionError": "Virhe lisättäessä ehtoa. Yritä uudelleen.", "AddCustomFormat": "Lisää mukautettu muoto", "AddCustomFormatError": "Virhe lisättäessä mukautettua muotoa. Yritä uudelleen.", - "AddConnection": "Lisää yhteys", + "AddConnection": "Lisää ilmoituspavelu", "AddDelayProfile": "Lisää viiveprofiili", - "AddDownloadClient": "Lisää lataustyökalu", + "AddDownloadClient": "Lisää latauspalvelu", "AddImportList": "Lisää tuontilista", "AddImportListExclusion": "Lisää tuontilistapoikkeus", - "AddDownloadClientError": "Virhe lisättäessä lataustyökalua. Yritä uudelleen.", + "AddDownloadClientError": "Latauspalvelun lisääminen epäonnistui. Yritä uudelleen.", "AddExclusion": "Lisää poikkeussääntö", "AddIndexerError": "Virhe lisättäessä tietolähdettä. Yritä uudelleen.", "AddList": "Lisää lista", @@ -83,7 +83,7 @@ "AddCondition": "Lisää ehto", "AddAutoTag": "Lisää automaattinen tunniste", "UpdateMechanismHelpText": "Käytä {appName}in sisäänrakennettua päivitystoimintoa tai komentosarjaa.", - "AbsoluteEpisodeNumbers": "Täysi jakson numero(t)", + "AbsoluteEpisodeNumbers": "Absoluuttiset jaksonumerot", "Add": "Lisää", "AddNew": "Lisää uusi", "Activity": "Tapahtumat", @@ -91,25 +91,25 @@ "Actions": "Toiminnot", "Absolute": "Ehdoton", "AddANewPath": "Lisää uusi polku", - "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", - "AddDownloadClientImplementation": "Lisäätään lataustyökalua - {implementationName}", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", + "AddDownloadClientImplementation": "Lisätään latauspalvelua – {implementationName}", "AddImportListExclusionError": "Virhe lisättäessä tuontilistapokkeusta. Yritä uudelleen.", - "AddIndexerImplementation": "Lisätään tietolähdettä - {implementationName}", + "AddIndexerImplementation": "Lisätään tietolähdettä – {implementationName}", "CalendarOptions": "Kalenterin asetukset", "BlocklistReleases": "Lisää julkaisut estolistalle", "BlocklistRelease": "Lisää julkaisu estolistalle", "DeleteConditionMessageText": "Haluatko varmasti poistaa ehdon \"{name}\"?", "ImportMechanismHandlingDisabledHealthCheckMessage": "Käytä valmistuneiden latausten käsittelyä", "Remove": "Poista", - "RemoveFromDownloadClient": "Poista lataustyökalusta", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Viestintä lataustyökalun \"{downloadClientName}\" kanssa ei onnistu. {errorMessage}", + "RemoveFromDownloadClient": "Poista latauspalvelusta", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Viestintä latauspalvelun \"{downloadClientName}\" kanssa epäonnistui. {errorMessage}", "AnimeEpisodeFormat": "Animejaksojen kaava", - "CheckDownloadClientForDetails": "katso lisätietoja lataustyökalusta", + "CheckDownloadClientForDetails": "katso lisätietoja latauspalvelusta", "Donations": "Lahjoitukset", - "DownloadClientsSettingsSummary": "Lataustyökalut, latausten käsittely ja etäsijaintien kohdistukset.", + "DownloadClientsSettingsSummary": "Latauspalvelut, latausten käsittely ja etäsijaintien kohdistukset.", "EpisodeFileRenamed": "Jaksotiedosto nimettiin uudelleen", "EpisodeImported": "Jakso tuotiin", - "EpisodeImportedTooltip": "Jakso ladattiin ja poimittiin lataustyökalulta", + "EpisodeImportedTooltip": "Jakso ladattiin ja poimittiin latauspalvelulta.", "ImportErrors": "Tuotinvirheet", "ImportedTo": "Tuontikohde", "ImportSeries": "Tuo sarja", @@ -122,7 +122,7 @@ "Imported": "Tuotu", "AddListError": "Virhe lisättäessä listaa. Yritä uudelleen.", "AddListExclusionError": "Virhe lisättäessä listapoikkeusta. Yritä uudelleen.", - "AddNotificationError": "Kytköksen lisäys epäonnistui. Yritä uudelleen.", + "AddNotificationError": "Ilmoituspalvelun lisääminen epäonnistui. Yritä uudelleen.", "AddQualityProfile": "Lisää laatuprofiili", "AddNewRestriction": "Lisää uusi rajoitus", "All": "Kaikki", @@ -137,25 +137,25 @@ "Backup": "Varmuuskopiointi", "AutomaticSearch": "Automaattihaku", "BackupRetentionHelpText": "Säilytysjaksoa vanhemmat varmuuskopiot siivotaan automaattisesti.", - "BackupsLoadError": "Virhe ladattaessa varmuuskopioita", + "BackupsLoadError": "Varmuuskopioinnin lataus epäonnistui", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Mukautetun muodon vähimmäispisteytys, jolla ensisijaisen protokollan viiveen ohitus sallitaan.", "BypassDelayIfHighestQuality": "Ohita, jos korkein laatu", - "CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?", + "CancelPendingTask": "Haluatko varmasti perua odottavan tehtävän?", "Clear": "Tyhjennä", - "CollectionsLoadError": "Virhe ladattaessa kokoelmia", + "CollectionsLoadError": "Kokoelmien lataus epäonnistui", "CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot", "CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.", - "CustomFormatsLoadError": "Virhe ladattaessa mukautettuja muotoja", + "CustomFormatsLoadError": "Mukautettujen muotojen lataus epäonnistui", "Debug": "Vianselvitys", "DeleteDelayProfileMessageText": "Haluatko varmasti poistaa viiveprofiilin?", "DeleteImportListMessageText": "Haluatko varmasti poistaa listan \"{name}\"?", "DeleteImportListExclusionMessageText": "Haluatko varmasti poistaa tuontilistapoikkeuksen?", "DeleteQualityProfile": "Poista laatuprofiili", "DeleteTagMessageText": "Haluatko varmasti poistaa tunnisteen \"{label}\"?", - "DownloadClientRootFolderHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa lataukset juurikansioon \"{rootFolderPath}\", mutta ne tulisi tallentaa muualle.", + "DownloadClientRootFolderHealthCheckMessage": "Latauspalvelu {downloadClientName} tallentaa lataukset juurikansioon \"{rootFolderPath}\", mutta niitä ei tulisi tallentaa sinne.", "EditImportListExclusion": "Muokkaa tuontilistapoikkeusta", - "EnableMediaInfoHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn ja/tai verkon kuormitusta tarkistusten aikana.", - "HistoryLoadError": "Virhe ladattaessa historiaa", + "EnableMediaInfoHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn tai verkon kuormitusta tarkistusten aikana.", + "HistoryLoadError": "Historian lataus epäonnistui", "Import": "Tuo", "DownloadClientQbittorrentSettingsContentLayout": "Sisällön rakenne", "MoreInfo": "Lisätietoja", @@ -163,14 +163,14 @@ "OnGrab": "Kun julkaisu kaapataan", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} ei voinut lisätä tunnistetta qBittorrentiin.", "SeriesFolderFormat": "Sarjakansioiden kaava", - "TagDetails": "Tunnisteen \"{label}\" tiedot", - "DownloadClientStatusSingleClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", + "TagDetails": "Tunnisteen tiedot – {label}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Latauspalveluita ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", "DownloadClientValidationCategoryMissing": "Kategoriaa ei ole olemassa", - "EditSelectedDownloadClients": "Muokkaa valittuja lataustyökaluja", + "EditSelectedDownloadClients": "Muokkaa valittuja latauspalveluita", "EpisodeProgress": "Jaksotilanne", "FilterSeriesPlaceholder": "Suodata sarjoja", "GeneralSettingsSummary": "Portti, SSL-salaus, käyttäjätunnus ja salasana, välityspalvelin, analytiikka ja päivitykset.", - "GeneralSettingsLoadError": "Virhe ladattaessa yleisiä asetuksia", + "GeneralSettingsLoadError": "Yleisasetusten lataus epäonnistui", "ImportCountSeries": "Tuo {selectedCount} sarjaa", "ICalTagsSeriesHelpText": "Vain vähintään yhdellä täsmäävällä tunnisteella merkityt sarjat sisällytetään syötteeseen.", "IndexerHDBitsSettingsMediumsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", @@ -179,37 +179,37 @@ "IndexerSettingsAllowZeroSize": "Salli nollakoko", "IndexerSettingsAdditionalParametersNyaa": "Muut parametrit", "IndexerSettingsAnimeCategories": "Animekategoriat", - "IndexerSettingsApiUrl": "Rajapinnan URL-osoite", + "IndexerSettingsApiUrl": "Rajapinnan URL", "IndexerSettingsCookie": "Eväste", "IndexerSettingsPasskey": "Suojausavain", "IndexerTagSeriesHelpText": "Tietolähdettä käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", "IndexerValidationQuerySeasonEpisodesNotSupported": "Tietolähde ei tue nykyistä kyselyä. Tarkista tukeeko se kategorioita ja kausien/jaksojen etsintää.", "InfoUrl": "Tietojen URL", "InstanceName": "Instanssin nimi", - "InteractiveImportLoadError": "Virhe ladattaessa manuaalisen tuonnin kohteita.", + "InteractiveImportLoadError": "Manuaalituonnin kohteiden lataus epäonnistui", "InteractiveSearchResultsSeriesFailedErrorMessage": "Haku epäonnistui: {message}. Elokuvan tietojen päivittäminen ja kaikkien tarvittavien tietojen olemassaolon varmistus voi auttaa ennen uutta hakuyritystä.", "Large": "Suuri", "LibraryImportSeriesHeader": "Tuo sinulla jo olevat sarjat", "LibraryImportTipsQualityInEpisodeFilename": "Varmista, että tuotavien tiedostojen nimissä mainitaan laatutiedot, kuten esim. \"episode.s02e15.bluray.mkv\".", - "IndexerIPTorrentsSettingsFeedUrl": "Syötteen URL-osoite", + "IndexerIPTorrentsSettingsFeedUrl": "Syötteen URL", "IndexerSettingsAnimeStandardFormatSearch": "Animen vakiomuotohaku", - "IndexerSettingsApiPathHelpText": "Polku rajapintaan (yleensä {url}).", - "IndexerSettingsApiPath": "API:n polku", + "IndexerSettingsApiPathHelpText": "Rajapinnan sijainti, yleensä \"{url}\".", + "IndexerSettingsApiPath": "Rajapinnan sijainti", "IndexerSettingsCategories": "Kategoriat", "IndexerSettingsSeedRatio": "Jakosuhde", - "IndexerSettingsWebsiteUrl": "Verkkosivuston URL-osoite", + "IndexerSettingsWebsiteUrl": "Verkkosivuston URL", "IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", - "IndexersLoadError": "Virhe ladattaessa tietolähteitä", + "IndexersLoadError": "Tietolähteiden lataus epäonnistui", "IndexersSettingsSummary": "Tietolähteet ja niiden asetukset.", "Indexers": "Tietolähteet", "KeyboardShortcutsFocusSearchBox": "Kohdista hakukenttä", "LastExecution": "Edellinen suoritus", "LogLevel": "Lokikirjauksen laajuus", "LogFilesLocation": "Lokitiedostojen tallennussijainti: {location}", - "Medium": "Keskikoko", - "MediaManagementSettingsLoadError": "Virhe ladattaessa mediatiedostojen hallinta-asetuksia", + "Medium": "Keskikokoinen", + "MediaManagementSettingsLoadError": "Mediatiedostojen hallinta-asetusten lataus epäonnistui", "MaximumSize": "Enimmäiskoko", - "MaximumSizeHelpText": "Kaapattavien julkaisujen enimmäiskoko megatavuina. Arvo \"0\" (nolla) poistaa rajoituksen.", + "MaximumSizeHelpText": "Kaapattavien julkaisujen enimmäiskoko megatavuina. Poista rajoitus asettamalla arvoksi 0.", "MinimumFreeSpaceHelpText": "Estä tuonti, jos sen jälkeinen vapaa levytila olisi tässä määritettyä pienempi.", "MinutesSixty": "60 minuuttia: {sixty}", "MediaInfo": "Median tiedot", @@ -226,9 +226,9 @@ "MoreDetails": "Lisätietoja", "MonitoredStatus": "Valvottu/tila", "NegateHelpText": "Jos käytössä, ei mukautettua muotoa sovelleta tämän \"{implementationName}\" -ehdon täsmätessä.", - "NoDownloadClientsFound": "Lataustyökaluja ei löytynyt", + "NoDownloadClientsFound": "Latauspalveluita ei löytynyt", "NoIndexersFound": "Tietolähteitä ei löytynyt", - "NamingSettingsLoadError": "Virhe ladattaessa nimeämisasetuksia", + "NamingSettingsLoadError": "Nimeämisasetusten lataus epäonnistui", "OnSeriesAdd": "Kun sarja lisätään", "ParseModalHelpText": "Syötä julkaisun nimi yllä olevaan kenttään.", "Path": "Tiedostosijainti", @@ -237,20 +237,20 @@ "NoLogFiles": "Lokitiedostoja ei ole", "RefreshSeries": "Päivitä sarja", "ReleaseSceneIndicatorUnknownSeries": "Tuntematon jakso tai sarja.", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", "RemoveCompletedDownloads": "Poista valmistuneet lataukset", "RemotePathMappingsLoadError": "Etäsijaintien kohdistusten lataus epäonnistui", "RemoveFailedDownloads": "Poista epäonnistuneet lataukset", "RemoveFailed": "Poisto epäonnistui", "RemoveFromBlocklist": "Poista estolistalta", - "RemoveQueueItem": "Poistetaan - {sourceTitle}", + "RemoveQueueItem": "Poistetaan – {sourceTitle}", "RemoveFromQueue": "Poista jonosta", "RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, {appName} käyttää nykyistä tiedostonimeä.", "RestoreBackup": "Palauta varmuuskopio", - "RestrictionsLoadError": "Virhe ladattaessa rajoituksia", + "RestrictionsLoadError": "Rajoitusten lataus epäonnistui", "SceneInfo": "Kohtaustiedot", "SceneInformation": "Kohtaustiedot", - "SelectFolderModalTitle": "{modalTitle} - Valitse kansio(t)", + "SelectFolderModalTitle": "{modalTitle} – Valitse kansio", "SelectQuality": "Valitse laatu", "SeriesFolderFormatHelpText": "Käytetään kun lisätään uusi sarja tai siirretään sarjoja sarjaeditorin avulla.", "SeriesIndexFooterMissingMonitored": "Jaksoja puuttuu (sarjaa valvotaan)", @@ -271,17 +271,17 @@ "OnLatestVersion": "Uusin {appName}-versio on jo asennettu", "OnSeriesDelete": "Kun sarja poistetaan", "PrioritySettings": "Painotus: {priority}", - "QualitiesLoadError": "Virhe ladattaessa laatuja", + "QualitiesLoadError": "Laatujen lataus epäonnistui", "QualityProfiles": "Laatuprofiilit", "QualityProfileInUseSeriesListCollection": "Sarjaan, listaan tai kokoelmaan liitettyä laatuprofiilia ei ole mahdollista poistaa.", "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", "RecentChanges": "Uusimmat muutokset", - "ReleaseProfileTagSeriesHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", + "ReleaseProfileTagSeriesHelpText": "Julkaisuprofiileja sovelletaan sarjoihin, jotka on merkitty ainakin yhdellä täsmäävällä tunnisteella. Käytä kaikille sarjoille jättämällä tyhjäksi.", "ReleaseTitle": "Julkaisun nimike", "Reload": "Lataa uudelleen", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etsijaintien kohdistukset ja lataustyökalun asetukset.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Etälatauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etsijaintien kohdistukset ja latauspalvelun asetukset.", "RemoveTagsAutomatically": "Poista tunnisteet automaattisesti", - "RemotePathMappingsInfo": "Etäsijaintien kohdistuksia tarvitaan harvoin ja jos {appName} ja lataustyökalu suoritetaan samassa järjestelmässä, on parempi käyttää paikallisia polkuja. Lue lisää [wikistä]({wikiLink}).", + "RemotePathMappingsInfo": "Etäsijaintien kohdistuksia tarvitaan harvoin ja jos {appName} ja latauspalvelu suoritetaan samassa järjestelmässä, on parempi käyttää paikallisia sijainteja. Lue lisää [wikistä]({wikiLink}).", "RemovedSeriesMultipleRemovedHealthCheckMessage": "Sarjat {series} poistettiin TheTVDB:stä.", "RemovedFromTaskQueue": "Poistettu tehtäväjonosta", "RestartNow": "Käynnistä uudelleen nyt", @@ -291,7 +291,7 @@ "RootFolder": "Juurikansio", "RootFolderSelectFreeSpace": "{freeSpace} vapaana", "SearchForCutoffUnmetEpisodes": "Etsi kaikkia katkaisutasoa saavuttamattomia jaksoja", - "SearchForCutoffUnmetEpisodesConfirmationCount": "Haluatko varmasti etsiä kaikkia {totalRecords} katkaisutasoa saavuttamattomia jaksoja?", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Haluatko varmasti etsiä kaikkia {totalRecords} jaksoa, joiden katkaisutasoa ei ole saavutettu?", "SeriesDetailsOneEpisodeFile": "1 jaksotiedosto", "SeriesDetailsRuntime": "{runtime} minuuttia", "SelectAll": "Valitse kaikki", @@ -302,10 +302,10 @@ "SeriesID": "Sarjan ID", "SeriesIsMonitored": "Sarjaa valvotaan", "SeriesTitle": "Sarjan nimi", - "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Kokonaismäärä: {totalEpisodeCount}, Ladataan: {downloadingCount})", + "SeriesProgressBarText": "{episodeFileCount}/{episodeCount} (kaikkiaan: {totalEpisodeCount}, latauksessa: {downloadingCount})", "SeriesTitleToExcludeHelpText": "Ohitettavan sarjan nimi.", "SeriesType": "Sarjan tyyppi", - "SetReleaseGroupModalTitle": "{modalTitle} - Aseta julkaisuryhmä", + "SetReleaseGroupModalTitle": "{modalTitle} – Aseta julkaisuryhmä", "ShowSearch": "Näytä haku", "ShowQualityProfile": "Näytä laatuprofiili", "ShowSeriesTitleHelpText": "Näytä sarjan nimi julisteen alla.", @@ -314,7 +314,7 @@ "SourceTitle": "Lähteen nimike", "SourceRelativePath": "Lähteen suhteellinen sijainti", "Special": "Erikoisjakso", - "Source": "Lähdekoodi", + "Source": "Lähde", "SkipRedownload": "Ohita uudelleenlataus", "SpecialEpisode": "Erikoisjakso", "StandardEpisodeTypeFormat": "Kausien ja jaksojen numerointi ({format})", @@ -322,9 +322,9 @@ "Started": "Alkoi", "SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä tietolähteistä painamalla niiden ohessa olevia lisätietopainikkeita.", "Status": "Tila", - "SupportedListsSeries": "{appName} tukee useita listoja, joilta sarjoja voidaan tuoda tietokantaan.", + "SupportedListsSeries": "{appName} tukee useita listoja, joiden avulla sarjoja voidaan tuoda tietokantaan.", "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.", - "TagsLoadError": "Virhe ladattaessa tunnisteita", + "TagsLoadError": "Tunnisteiden lataus epäonnistui", "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tomorrow": "Huomenna", "TestParsing": "Testaa jäsennystä", @@ -335,7 +335,7 @@ "TvdbIdExcludeHelpText": "Ohitettavan sarjan TheTVDB ID.", "Type": "Tyyppi", "TypeOfList": "{typeOfList}-lista", - "Twitter": "X (Twitter)", + "Twitter": "X (ent. Twitter)", "UiSettingsSummary": "Kalenterin, päiväyksen ja kellonajan, sekä kielen ja heikentyneelle värinäölle sopivan tilan asetukset.", "UnmappedFolders": "Kohdistamattomat kansiot", "UnmonitorSpecialEpisodes": "Älä valvo erikoisjaksoja", @@ -352,7 +352,7 @@ "FailedToFetchUpdates": "Päivitysten nouto epäonnistui", "HasMissingSeason": "Kausi(a) puuttuu", "ImportLists": "Tuontilistat", - "IndexerStatusUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi: {indexerNames}", + "IndexerStatusUnavailableHealthCheckMessage": "Tietolähteet eivät ole virheiden vuoksi käytettävissä: {indexerNames}", "Info": "Informatiivinen", "NextAiring": "Seuraava esitys", "ApplyTags": "Tunnistetoimenpide", @@ -360,24 +360,24 @@ "CountIndexersSelected": "{count} tietolähde(ttä) on valittu", "SetTags": "Tunnisteiden määritys", "Monitored": "Valvonta", - "ApplyTagsHelpTextHowToApplyDownloadClients": "Tunnisteiden käyttö valituissa lataustyökaluissa", - "ApplyTagsHelpTextHowToApplyImportLists": "Tunnisteiden käyttö valituissa tuontilistoissa", - "ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituissa sarjoissa", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Tunnisteiden käyttö valituille latauspalveluille", + "ApplyTagsHelpTextHowToApplyImportLists": "Tunnisteiden käyttö valituille tuontilistoille", + "ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituille sarjoille", "LogFiles": "Lokitiedostot", "None": "Ei mitään", "RemoveSelectedItems": "Poista valitut kohteet", "Settings": "Asetukset", "AllResultsAreHiddenByTheAppliedFilter": "Aktiivinen suodatin piilottaa kaikki tulokset.", - "DefaultNameCopiedProfile": "{name} - Kopioi", - "DefaultNameCopiedSpecification": "{name} - Kopioi", + "DefaultNameCopiedProfile": "{name} (kopio)", + "DefaultNameCopiedSpecification": "{name} (kopio)", "Downloading": "Ladataan", "ProgressBarProgress": "Tilapalkissa {progress} %", "Queue": "Jono", - "RemotePathMappingRemotePathHelpText": "Lataustyökalun käyttämän kansion juurisijainti.", + "RemotePathMappingRemotePathHelpText": "Latauspalvelun käyttämän kansion juurisijainti.", "Restart": "Käynnistä uudelleen", "SizeLimit": "Kokorajoitus", "TestAllIndexers": "Tietolähteiden testaus", - "UnableToLoadBackups": "Varmuuskopioiden lataus epäonnistui", + "UnableToLoadBackups": "Varmuuskopioinnin lataus epäonnistui", "UseSeasonFolderHelpText": "Lajittele jaksot tuotantokausikohtaisiin kansioihin.", "ApplyTagsHelpTextRemove": "- \"Poista\" tyhjentää syötetyt tunnisteet", "ApplyTagsHelpTextReplace": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi", @@ -385,16 +385,16 @@ "SeriesMatchType": "Sarjan kohdistustyyppi", "RemotePathMappingLocalPathHelpText": "Sijainti, jonka kautta {appName}in tulee käyttää etäsijaintia paikallisesti.", "SonarrTags": "{appName}in tunnisteet", - "CalendarLoadError": "Kalenterin lataus epäonnistui.", + "CalendarLoadError": "Kalenterin lataus epäonnistui", "BeforeUpdate": "Ennen päivitystä", "Backups": "Varmuuskopiot", "BackupNow": "Varmuuskopioi nyt", "AppDataDirectory": "AppData-kansio", "ClearBlocklistMessageText": "Haluatko varmasti tyhjentää kaikki estolistan kohteet?", - "CountDownloadClientsSelected": "{count} lataustyökalu(a) on valittu", + "CountDownloadClientsSelected": "{count} latauspalvelu(a) on valittu", "DelayMinutes": "{delay} minuuttia", - "DelayProfilesLoadError": "Virhe ladattaessa viiveprofiileja", - "DeleteDownloadClient": "Poista lataustyökalu", + "DelayProfilesLoadError": "Viiveprofiilien lataus epäonnistui", + "DeleteDownloadClient": "Poista latauspalvelu", "DeleteBackupMessageText": "Haluatko varmasti poistaa varmuuskopion \"{name}\"?", "DeleteIndexerMessageText": "Haluatko varmasti poistaa tietolähteen '{name}'?", "DeleteRootFolderMessageText": "Haluatko varmasti poistaa juurikansion \"{path}\"?", @@ -408,26 +408,26 @@ "DownloadClientFloodSettingsPostImportTags": "Tuonnin jälkeiset tunnisteet", "DownloadClientDownloadStationValidationSharedFolderMissing": "Jaettua kansiota ei ole olemassa", "DownloadClientFreeboxSettingsAppId": "Sovellustunniste", - "DownloadClientFreeboxSettingsApiUrl": "Rajapinnan URL-osoite", + "DownloadClientFreeboxSettingsApiUrl": "Rajapinnan URL", "DownloadClientFreeboxSettingsAppToken": "Sovellustietue", - "DownloadClientFreeboxUnableToReachFreebox": "Freebox-rajapintaa ei tavoiteta. Tarkista \"Osoite\"-, \"Portti\"- ja \"Käytä SSL-salausta\"-asetukset. Virhe: {exceptionMessage}.", + "DownloadClientFreeboxUnableToReachFreebox": "Freebox-rajapintaan ei voida muodostaa yhteyttä. Tarkista \"Osoite\"-, \"Portti\"- ja \"Käytä SSL-salausta\"-asetukset. Virhe: {exceptionMessage}.", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Lataa tiedostot järjestyksessä (qBittorrent 4.1.0+).", - "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd:n develop-versio, oletettavasti vähintään versio 3.0.0.", + "DownloadClientSabnzbdValidationDevelopVersion": "SABnzb:n develop-versio, oletettavasti vähintään versio 3.0.0.", "Daily": "Päivittäinen", "CutoffUnmetNoItems": "Katkaisutasoa saavuttamattomia kohteita ei ole", "DailyEpisodeFormat": "Päivittäisjaksojen kaava", "DelayProfileProtocol": "Protokolla: {preferredProtocol}", "Day": "Päivä", "DownloadClientSabnzbdValidationUnknownVersion": "Tuntematon versio: {rawVersion}", - "DownloadClientSettingsAddPaused": "Lisää pysäytettynä", - "DownloadClientStatusAllClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä", + "DownloadClientSettingsAddPaused": "Lisää keskeytettynä", + "DownloadClientStatusAllClientHealthCheckMessage": "Latauspalveluita ei ole ongelmien vuoksi käytettävissä", "DownloadClientUTorrentTorrentStateError": "uTorrent ilmoittaa virheestä", "DownloadClientValidationUnableToConnectDetail": "Tarkista osoite ja portti.", "DeleteEpisodesFilesHelpText": "Poista jaksotiedostot ja sarjan kansio.", "EditCustomFormat": "Muokkaa mukautettua muotoa", - "EditConditionImplementation": "Muokataan ehtoa - {implementationName}", - "EditDownloadClientImplementation": "Muokataan lataustyökalua - {implementationName}", - "EditImportListImplementation": "Muokataan tuontilistaa - {implementationName}", + "EditConditionImplementation": "Muokataan ehtoa – {implementationName}", + "EditDownloadClientImplementation": "Muokataan latauspalvelua – {implementationName}", + "EditImportListImplementation": "Muokataan tuontilistaa – {implementationName}", "EndedOnly": "Vain päättyneet", "EnableInteractiveSearchHelpTextWarning": "Tämä tietolähde ei tue hakua.", "Episode": "Jakso", @@ -453,19 +453,19 @@ "IRCLinkText": "#sonarr Liberassa", "IRC": "IRC", "IconForFinales": "Päätösjaksokuvake", - "IndexerSettingsSeedTimeHelpText": "Aika, joka torrentia tulee jakaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi.", + "IndexerSettingsSeedTimeHelpText": "Aika, joka torrentia tulee jakaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi.", "IndexerSettingsSeedTime": "Jakoaika", - "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.", + "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.", "IndexerStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi", - "LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä lataustyökalun latausten tuontiin. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.", + "LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä latausten tuontiin latauspalvelulta. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.", "LibraryImportTips": "Muutama vinkki, joilla homma sujuu:", "ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein", - "ListExclusionsLoadError": "Virhe ladattaessa listapoikkeuksia", - "ManualImportItemsLoadError": "Virhe ladattaessa manuaalisen tuonnin kohteita.", + "ListExclusionsLoadError": "Listapoikkeusten lataus epäonnistui", + "ManualImportItemsLoadError": "Manuaalituonnin kohteiden lataus epäonnistui", "MediaManagementSettingsSummary": "Tiedostojen nimeämis- ja hallinta-asetukset, sekä kirjaston juurikansiot.", "Message": "Viesti", "MetadataSettings": "Metatietoasetukset", - "MetadataLoadError": "Virhe ladattaessa metatietoja", + "MetadataLoadError": "Metatietojen lataus epäonnistui", "MetadataProvidedBy": "Metatiedot toimittaa {provider}", "MinimumCustomFormatScoreHelpText": "Mukautetun muodon vähimmäispisteytys, jolla lataus sallitaan.", "MinimumFreeSpace": "Vapaan tilan vähimmäismäärä", @@ -484,36 +484,36 @@ "Name": "Nimi", "NamingSettings": "Nimeämisasetukset", "NoEpisodeHistory": "Jaksohistoriaa ei ole", - "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, joiden yhteiskoko on {size}.", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, kooltaan yhteensä {size}.", "DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?", "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.", - "DeleteSeriesModalHeader": "Poistetaan - {title}", + "DeleteSeriesModalHeader": "Poistetaan – {title}", "DeletedSeriesDescription": "Sarja poistettiin TheTVDB:stä.", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", - "NotificationStatusSingleClientHealthCheckMessage": "Ilmoitukset eivät ole ongelmien vuoksi käytettävissä: {notificationNames}", - "NotificationsLoadError": "Virhe ladattaessa kytköksiä", + "NotificationStatusSingleClientHealthCheckMessage": "Ilmoituspalvelut eivät ole ongelmien vuoksi käytettävissä: {notificationNames}.", + "NotificationsLoadError": "Ilmoituspalveluiden lataus epäonnistui", "Options": "Asetukset", "OptionalName": "Valinnainen nimi", - "OverviewOptions": "Yleiskatsauksen asetukset", + "OverviewOptions": "Tiivistelmänäkymän asetukset", "PackageVersionInfo": "{packageVersion} julkaisijalta {packageAuthor}", "ParseModalErrorParsing": "Virhe jäsennettäessä. Yritä uudelleen.", - "PendingDownloadClientUnavailable": "Odottaa - Lataustyökalu ei ole käytettävissä", + "PendingDownloadClientUnavailable": "Odottaa – Latauspalvelu ei ole käytettävissä", "Queued": "Lisätty jonoon", "RefreshAndScanTooltip": "Päivitä tiedot ja tarkista levy", "RefreshAndScan": "Päivitä ja tarkista", "Refresh": "Päivitä", - "ReleaseProfilesLoadError": "Virhe ladattaessa julkaisuprofiileita", - "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.", + "ReleaseProfilesLoadError": "Julkaisuprofiilien lataus epäonnistui", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Etälatauspalvelu {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 palveluun {clientName}.", "DownloadClientDelugeValidationLabelPluginInactive": "Label-tunnistelisäosa ei ole käytössä.", - "AddConditionImplementation": "Lisätään ehtoa - {implementationName}", + "AddConditionImplementation": "Lisätään ehtoa – {implementationName}", "AddCustomFilter": "Lisää oma suodatin", - "AddConnectionImplementation": "Lisätään kytköstä - {implementationName}", - "RemoveCompletedDownloadsHelpText": "Poista tuodut lataukset lataustyökalun historiasta", + "AddConnectionImplementation": "Lisätään ilmoituspavelua – {implementationName}", + "RemoveCompletedDownloadsHelpText": "Poista tuodut lataukset latauspalvelun historiasta", "RemoveFilter": "Poista suodatin", "RemoveRootFolder": "Poista juurikansio", "RenameEpisodes": "Nimeä jaksot uudelleen", - "RenameFiles": "Nimeä tiedostot", + "RenameFiles": "Nimeä tiedostot uudelleen", "RemoveTagsAutomaticallyHelpText": "Poista tunnisteet automaattisesti, jos ehdot eivät täyty.", "RemovedSeriesSingleRemovedHealthCheckMessage": "Sarja {series} poistettiin TheTVDB:stä.", "RescanAfterRefreshHelpTextWarning": "{appName} ei tunnista tiedostomuutoksia automaattisesti, jos asetuksena ei ole \"Aina\".", @@ -525,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 in uudelleenkäynnistyksen.", + "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii sovelluksen uudelleenkäynnistyksen.", "Runtime": "Kesto", "Season": "Kausi", "SeasonFolder": "Kausikohtaiset kansiot", @@ -537,13 +537,13 @@ "SeasonPassTruncated": "Vain uusimmat 25 kautta näytetään. Muut ovat nähtävissä sarjan tiedoista.", "SeasonPremieresOnly": "Vain kausien pilottijaksot", "SeasonPremiere": "Kauden pilottijakso", - "SelectDownloadClientModalTitle": "{modalTitle} - Valitse lataustyökalu", - "SelectEpisodesModalTitle": "{modalTitle} - Valitse jakso(t)", + "SelectDownloadClientModalTitle": "{modalTitle} – Valitse latauspalvelu", + "SelectEpisodesModalTitle": "{modalTitle} – Valitse jakso(t)", "SelectEpisodes": "Valitse jakso(t)", "Series": "Sarjat", "SeriesCannotBeFound": "Valitettavasti etsimääsi sarjaa ei löydy.", "SeriesDetailsNoEpisodeFiles": "Jaksotiedostoja ei ole", - "SeriesIndexFooterDownloading": "Ladataan (yksi tai useita jaksoja)", + "SeriesIndexFooterDownloading": "Ladataan (yhtä tai useampaa jaksoa)", "SeriesTypesHelpText": "Sajatyyppiä käytetään uudelleennimeämiseen, jäsennykseen ja hakuun.", "SeriesTypes": "Sarjatyypit", "ShowMonitored": "Näytä valvontatila", @@ -559,7 +559,7 @@ "Tags": "Tunnisteet", "ToggleUnmonitoredToMonitored": "Ei valvota (aloita painamalla)", "TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).", - "UiSettingsLoadError": "Virhe ladattaesssa käyttöliittymän asetuksia", + "UiSettingsLoadError": "Käyttöliittymäasetusten lataus epäonnistui", "UnableToUpdateSonarrDirectly": "{appName}ia ei voida päivittää suoraan,", "UnmonitoredOnly": "Vain valvomattomat", "UnmonitorDeletedEpisodes": "Lopeta poistettujen jaksojen valvonta", @@ -568,69 +568,69 @@ "UpdateAll": "Päivitä kaikki", "UpcomingSeriesDescription": "Sarja on julkistettu, mutta tarkka esitysaika ei ole vielä tiedossa.", "UnselectAll": "Tyhjennä valinnat", - "UpdateMonitoring": "Päivitä valvontatila", + "UpdateMonitoring": "Vaihda valvontatilaa", "UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,", "UpgradeUntilThisQualityIsMetOrExceeded": "Päivitä kunnes tämä laatu on savutettu tai ylitetty", "Updates": "Päivitykset", "UpdateSelected": "Päivitä valitut", "UseSeasonFolder": "Käytä kausikansiota", - "AddedToDownloadQueue": "Lisätty latausjonoon", - "AddImportListImplementation": "Lisätään tuontilistaa - {implementationName}", + "AddedToDownloadQueue": "Lisättiin latausjonoon", + "AddImportListImplementation": "Lisätään tuontilistaa – {implementationName}", "AddToDownloadQueue": "Lisää latausjonoon", "VersionNumber": "Versio {version}", "VisitTheWikiForMoreDetails": "Lisätietoja löytyy wikistä: ", "Week": "Viikko", "Wanted": "Halutut", - "Warn": "Varoitus", + "Warn": "Varoita", "AirsDateAtTimeOn": "{date} klo {time} kanavalla {networkLabel}", "AirsTbaOn": "TBA kanavalla {networkLabel}", "AirsTimeOn": "{time} kanavalla {networkLabel}", "DownloadClientDownloadStationValidationFolderMissing": "Kansiota ei ole olemassa", "DownloadClientDownloadStationValidationNoDefaultDestination": "Oletussijaintia ei ole", "DownloadClientFloodSettingsAdditionalTags": "Lisätunnisteet", - "DownloadClientFloodSettingsPostImportTagsHelpText": "Sisällyttää tunnisteet kun lataus on tuotu.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Lisää tunnisteet kun lataus on tuotu.", "DownloadClientFloodSettingsStartOnAdd": "Käynnistä lisättäessä", "ClickToChangeQuality": "Vaihda laatua painamalla tästä", "EpisodeDownloaded": "Jakso on ladattu", "InteractiveImportNoQuality": "Jokaisen valitun tiedoston laatu on määritettävä.", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.", - "NotificationStatusAllClientHealthCheckMessage": "Mikään ilmoituspavelu ei ole ongelmien vuoksi käytettävissä.", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGetin version 16.0.", + "NotificationStatusAllClientHealthCheckMessage": "Ilmoituspalvelut eivät ole ongelmien vuoksi käytettävissä.", "DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent lataa metatietoja", "DownloadClientQbittorrentTorrentStateError": "qBittorrent ilmoittaa virheestä", "RemotePath": "Etäsijainti", "DownloadClientQbittorrentValidationQueueingNotEnabled": "Jonotus ei ole käytössä", - "DownloadClientValidationErrorVersion": "Lataustyökalun {clientName} version tulee olla vähintään {requiredVersion}. Ilmoitettu versio on {reportedVersion}.", + "DownloadClientValidationErrorVersion": "Latauspalvelun {clientName} version tulee olla vähintään {requiredVersion}. Ilmoitettu versio on {reportedVersion}.", "DownloadClientVuzeValidationErrorVersion": "Protokollaversiota ei tueta. Käytä vähintään Vuzen versiota 5.0.0.0 Vuze Web Remote -lisäosan kanssa.", "DownloadStationStatusExtracting": "Puretaan: {progress} %", "EditDelayProfile": "Muokkaa viiveprofiilia", - "EditConnectionImplementation": "Muokataan kytköstä - {implementationName}", + "EditConnectionImplementation": "Muokataan ilmoituspalvelua – {implementationName}", "EditGroups": "Muokkaa ryhmiä", - "EditIndexerImplementation": "Muokataan tietolähdettä - {implementationName}", + "EditIndexerImplementation": "Muokataan tietolähdettä – {implementationName}", "EditListExclusion": "Muokkaa poikkeussääntöä", - "EditSeriesModalHeader": "Muokataan - {title}", + "EditSeriesModalHeader": "Muokataan – {title}", "EnableInteractiveSearch": "Käytä manuaalihakuun", "EnableRssHelpText": "Käytetään {appName}in etsiessä julkaisuja ajoitetusti RSS-synkronoinnilla.", - "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", + "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvalvojan oikeuksilla.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeInfo": "Jakson tiedot", - "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja", + "EpisodeFilesLoadError": "Jaksotiedostojen lataus epäonnistui", "EpisodeIsNotMonitored": "Jaksoa ei valvota", "EpisodeIsDownloading": "Jaksoa ladataan", "EpisodeMissingFromDisk": "Jaksoa ei ole levyllä", - "EpisodeSearchResultsLoadError": "Virhe ladattaessa tämän jaksohaun tuloksia. Yritä myöhemmin uudelleen.", + "EpisodeSearchResultsLoadError": "Tämän jaksohaun tulosten lataus epäonnistui. Yritä myöhemmin uudelleen.", "EpisodeTitle": "Jakson nimi", - "EpisodeTitleRequired": "Jakson nimi vaaditaan", + "EpisodeTitleRequired": "Jakson nimi on pakollinen.", "Episodes": "Jaksot", "ErrorLoadingContents": "Virhe ladattaessa sisältöjä", - "EpisodesLoadError": "Virhe ladattaessa jaksoja", + "EpisodesLoadError": "Jaksojen lataus epäonnistui", "ErrorLoadingContent": "Virhe ladattaessa tätä sisältöä", "FailedToLoadCustomFiltersFromApi": "Suodatinmukautusten lataus rajapinnasta epäonnistui", "FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus rajapinnasta epäonnistui", "CalendarFeed": "{appName}in kalenterisyöte", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Jaksoa ladataan", - "ListOptionsLoadError": "Lista-asetuksia ei voida ladata.", + "ListOptionsLoadError": "Lista-asetusten lataus epäonnistui", "RemoveCompleted": "Poisto on suoritettu", "ICalShowAsAllDayEvents": "Näytä koko päivän tapahtumina", "FailedToLoadTagsFromApi": "Tunnisteiden lataus rajapinnasta epäonnistui", @@ -643,7 +643,7 @@ "Health": "Terveys", "HistoryModalHeaderSeason": "Historia {season}", "HistorySeason": "Tarkastele tämän kauden historiaa.", - "HideEpisodes": "Piilota jaksoja", + "HideEpisodes": "Piilota jaksot", "History": "Historia", "HourShorthand": "t", "ICalFeed": "iCal-syöte", @@ -659,7 +659,7 @@ "TablePageSizeMinimum": "Sivukohtaisen kohdemäärän on oltava vähintään {minimumValue}.", "YesCancel": "Kyllä, peru", "ImportListSearchForMissingEpisodesHelpText": "{appName} aloittaa automaattisesti puuttuvien jaksojen etsinnän kun sarja lisätään.", - "InteractiveSearchModalHeaderSeason": "Manuaalihaku - {season}", + "InteractiveSearchModalHeaderSeason": "Manuaalihaku – {season}", "InteractiveImportNoImportMode": "Tuontitila on valittava.", "InteractiveSearch": "Etsi manuaalisesti", "InteractiveSearchModalHeader": "Manuaalihaku", @@ -672,7 +672,7 @@ "PreferredProtocol": "Ensisijainen protokolla", "RootFolderMultipleMissingHealthCheckMessage": "Useita juurikansioita puuttuu: {rootFolderPaths}", "RootFolderMissingHealthCheckMessage": "Juurikansio puuttuu: {rootFolderPath}", - "OrganizeSelectedSeriesModalHeader": "Järjestele valittu sarja", + "OrganizeSelectedSeriesModalHeader": "Järjestele valitut sarjat", "OrganizeRenamingDisabled": "Uudelleennimeäminen ei ole käytössä, eikä uudelleennimettävää ole", "OrganizeSelectedSeriesModalConfirmation": "Haluatko varmasti järjestellä kaikkien {count} valitun sarjan tiedostot?", "OutputPath": "Tallennussijainti", @@ -682,7 +682,7 @@ "PortNumber": "Portin numero", "QualitySettings": "Laatuasetukset", "QuickSearch": "Pikahaku", - "QualityProfilesLoadError": "Virhe ladattaessa laatuprofiileja", + "QualityProfilesLoadError": "Laatuprofiilien lataus epäonnistui", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} jaksotiedostoa", "SeriesEditor": "Sarjaeditori", "SeriesIndexFooterMissingUnmonitored": "Jaksoja puuttuu (sarjaa ei valvota)", @@ -700,21 +700,21 @@ "FileBrowserPlaceholderText": "Kirjoita sijainti tai selaa se alta", "FeatureRequests": "Kehitysehdotukset", "IndexerPriority": "Tietolähteiden painotus", - "IndexerOptionsLoadError": "Tietolähdeasetusten lataus ei onnistu", + "IndexerOptionsLoadError": "Tietolähdeasetusten lataus epäonnistui", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", "RemotePathMappings": "Etäsijaintien kohdistukset", - "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.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Etälatauspalvelu {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", "SelectSeries": "Valitse sarja", - "SelectSeasonModalTitle": "{modalTitle} - Valitse kausi", + "SelectSeasonModalTitle": "{modalTitle} – Valitse kausi", "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", "TimeLeft": "Aikaa jäljellä", "Time": "Aika", "UpdateAvailableHealthCheckMessage": "Uusi päivitys on saatavilla: {version}", - "SupportedDownloadClientsMoreInfo": "Saat tietoja yksittäisistä lataustyökaluista painamalla niiden ohessa olevia lisätietopainikkeita.", + "SupportedDownloadClientsMoreInfo": "Saat lisätietoja yksittäisistä latauspalveluista painamalla niiden ohessa olevia lisätietopainikkeita.", "SupportedDownloadClients": "{appName} tukee monia torrent- ja Usenet-lataajia.", "SupportedImportListsMoreInfo": "Saat tietoja yksittäisistä tuontilistoista painamalla niiden ohessa olevia lisätietopainikkeita.", "System": "Järjestelmä", @@ -724,32 +724,32 @@ "AddNewSeries": "Lisää uusi sarja", "MissingLoadError": "Virhe ladattaessa puuttuvia kohteita.", "AuthenticationRequiredUsernameHelpTextWarning": "Syötä uusi käyttäjätunnus", - "BlocklistLoadError": "Virhe ladattaessa estolistaa", + "BlocklistLoadError": "Estolistan lataus epäonnistui", "Database": "Tietokanta", "LastWriteTime": "Edellinen tallennus", - "ChownGroupHelpTextWarning": "Toimii vain, jos {appName}in suorittava käyttäjä on tiedoston omistaja. On parempi varmistaa, että lataustyökalu käyttää samaa ryhmää kuin {appName}.", - "IndexerSettingsSeasonPackSeedTimeHelpText": "Aika, joka tuotantokausipaketin sisältävää torrentia tulee jakaa ennen sen pysäytystä. Käytä lataustyökalun oletusta jättämällä tyhjäksi.", + "ChownGroupHelpTextWarning": "Toimii vain, jos {appName}in suorittava käyttäjä on tiedoston omistaja. On parempi varmistaa, että latauspalvelu käyttää samaa ryhmää kuin {appName}.", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Aika, joka tuotantokausipaketin sisältävää torrentia tulee jakaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi.", "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kohdistukset ja säiliön tallennusmedian asetukset.", - "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei löydy Docker-säiliöstä. Tarkista etäsijaintien kohdistukset ja säiliön tallennusmedian asetukset.", + "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain latauspalvelun tukiessa tiedostoon tallennettuja magnet-linkkejä).", "IndexerPriorityHelpText": "Tietolähteen painotus, 1– 50 (korkein-alin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen kaappauspäätökseen. {appName} käyttää edelleen kaikkia käytössä olevia tietolähteitä RSS-synkronointiin ja hakuun.", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Sarjojen ja jaksojen tiedot tarjoaa TheTVDB.com. [Harkitse palvelun tukemista]({url})", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ei voi suorittaa valmistuneiden latausten hallintaa määritetyllä tavalla. Voit korjata tämän vaihtamalla qBittorentin asetusten \"BitTorrent\"-osion \"Jakosuhderajoitukset\"-osion toiminnoksi pysäytyksen poiston sijaan.", "FullColorEventsHelpText": "Vaihtoehtoinen tyyli, jossa koko tapahtuma väritetään tilavärillä pelkän vasemman laidan sijaan. Ei vaikuta agendan esitykseen.", "Yesterday": "Eilen", "SeriesMonitoring": "Sarjan valvonta", - "ApplyTagsHelpTextHowToApplyIndexers": "Tunnisteiden käyttö valituissa tietolähteissä", + "ApplyTagsHelpTextHowToApplyIndexers": "Tunnisteiden käyttö valituille tietolähteille", "AutoTaggingRequiredHelpText": "Tämän \"{implementationName}\" -ehdon on täsmättävä automaattimerkinnän säännön käyttämiseksi. Muutoin yksittäinen \"{implementationName}\" -vastaavuus riittää.", - "LibraryImportTipsSeriesUseRootFolder": "Osoita {appName} kaikki sarjat sisältävään kansioon, ei sarjakohtaiseen kansioon. Esim. \"`{goodFolderExample}`\" eikä \"`{badFolderExample}`\". Lisäksi jokaisen sarjan on oltava kirjasto-/juurkansion alla omissa kansioissa.", + "LibraryImportTipsSeriesUseRootFolder": "Osoita {appName}ille kansio, joka sisältää kaikki tuotavat sarjat, ei vain yksittäistä elokuvaa (esim. \"{goodFolderExample}\" ei \"{badFolderExample}\"). Lisäksi jokaisen sarjan on oltava omissa kansioissaan juuri-/kirjastokansion alla.", "SeriesDetailsGoTo": "Avaa {title}", "SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.", "WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?", - "SeriesLoadError": "Virhe ladattaessa sarjoja", + "SeriesLoadError": "Sarjojen lataus epäonnistui", "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.", - "DownloadClientOptionsLoadError": "Virhe ladattaessa lataustyökaluasetuksia", + "DownloadClientOptionsLoadError": "Latauspalveluasetusten lataus epäonnistui", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).", - "IndexerValidationUnableToConnectHttpError": "Tietolähdettä ei tavoiteta. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", + "IndexerValidationUnableToConnectHttpError": "Tietolähteeseen ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", "BypassDelayIfHighestQualityHelpText": "Ohitusviive kun julkaisun laatu vastaa laatuprofiilin korkeinta käytössä olevaa laatua halutulla protokollalla.", "IndexerHDBitsSettingsCategoriesHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", "IndexerHDBitsSettingsCategories": "Kategoriat", @@ -767,8 +767,8 @@ "NoHistoryFound": "Historiaa ei löytynyt", "NoEpisodesInThisSeason": "Kaudelle ei ole jaksoja", "NoBackupsAreAvailable": "Varmuuskopioita ei ole käytettävissä", - "OrganizeNothingToRename": "Valmis! Tuöni on tehty, eikä nimettäviä tiedostoja ole.", - "OrganizeModalHeaderSeason": "Järjestellään ja uudelleennimetään - {season}", + "OrganizeNothingToRename": "Valmis! Toiminto on suoritettu, eikä uudelleennimettäviä tiedostoja ole.", + "OrganizeModalHeaderSeason": "Järjestellään ja uudelleennimetään – {season}", "OverrideGrabNoEpisode": "Ainakin yksi jakso on valittava.", "Port": "Portti", "ProxyType": "Välityspalvelimen tyyppi", @@ -777,12 +777,12 @@ "RssSync": "Synkronoi RSS", "Search": "Haku", "AddSeriesWithTitle": "Lisää {title}", - "AddNewSeriesHelpText": "Sarjojen lisääminen on helppoa. Aloita vain haluamasi sarjan nimen kirjoittaminen.", + "AddNewSeriesHelpText": "Uuden sarjan lisääminen on helppoa. Aloita vain haluamasi sarjan nimen kirjoittaminen.", "AddNewSeriesRootFolderHelpText": "\"{folder}\" -alikansio luodaan automaattisesti.", "AddNewSeriesSearchForMissingEpisodes": "Käynnistä puuttuvien jaksojen etsintä", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Käynnistä katkaisutasoa saavuttamattomien jaksojen etsintä", "AddNewSeriesError": "Hakutulosten lataus epäonnistui. Yritä uudelleen.", - "AnalyseVideoFilesHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn ja/tai verkon kuormitusta tarkistusten aikana.", + "AnalyseVideoFilesHelpText": "Pura videotiedostoista resoluution, keston ja koodekkien kaltaisia tietoja. Tätä varten {appName}in on luettava osia tiedostoista, joka saattaa kasvattaa levyn tai verkon kuormitusta tarkistusten aikana.", "ShowUnknownSeriesItemsHelpText": "Näytä jonossa kohteet, joiden sarja ei ole tiedossa. Tämä voi sisältää poistettuja sarjoja, elokuvia tai mitä tahansa muuta {appName}in kategoriasta.", "ShowSearchHelpText": "Näytä hakupainike osoitettaessa.", "Size": "Koko", @@ -793,9 +793,9 @@ "DeleteSeriesFolderConfirmation": "Sarjan kansio \"{path}\" ja kaikki sen sisältö poistetaan.", "Theme": "Teema", "DeleteSeriesFolderCountConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa?", - "DownloadClientSettings": "Lataustyökalujen asetukset", + "DownloadClientSettings": "Latauspalveluasetukset", "FailedToLoadSeriesFromApi": "Sarjan lataus rajapinnasta epäonnistui", - "OverrideGrabModalTitle": "Ohita ja kaappaa - {title}", + "OverrideGrabModalTitle": "Ohitetaan ja kaapataan – {title}", "ShowEpisodeInformation": "Näytä jaksojen tiedot", "ShowEpisodes": "Näytä jaksot", "ShowDateAdded": "Näytä lisäyspäivä", @@ -808,7 +808,7 @@ "AlternateTitles": "Vaihtoehtoiset nimet", "ApplicationUrlHelpText": "Tämän sovelluksen ulkoinen URL-osoite, johon sisältyy http(s)://, portti ja URL-perusta.", "ApplyChanges": "Toteuta muutokset", - "AutoTaggingLoadError": "Virhe ladattaessa automaattimerkintää", + "AutoTaggingLoadError": "Automaattimerkinnän lataus epäonnistui", "BackupIntervalHelpText": "Tietokannan ja asetusten automaattisen varmuuskopioinnin ajoitus.", "BackupFolderHelpText": "Suhteelliset tiedostosijainnit ovat {appName}in AppData-kansiossa.", "AnimeEpisodeTypeFormat": "Absoluuttinen jaksonumerointi ({format})", @@ -834,9 +834,9 @@ "CopyUsingHardlinksSeriesHelpText": "Hardlink-kytkösten avulla {appName} voi tuoda jaettavat torrentit ilman niiden täyttä kopiointia ja levytilan kaksinkertaista varausta. Tämä toimii vain lähde- ja kohdesijaintien ollessa samalla tallennusmedialla.", "CutoffUnmet": "Katkaisutasoa ei saavutettu", "CurrentlyInstalled": "Tällä hetkellä asennettu versio", - "DeleteSelectedDownloadClientsMessageText": "Haluatko varmasti poistaa {count} valit(n/tua) lataustyökalu(n/a)?", - "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa lataustyökalun \"{name}\"?", - "DeleteSelectedDownloadClients": "Poista lataustyökalu(t)", + "DeleteSelectedDownloadClientsMessageText": "Haluatko varmasti poistaa {count} valittua latauspalvelua?", + "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa latauspalvelun \"{name}\"?", + "DeleteSelectedDownloadClients": "Poista valitut latauspalvelu(t)", "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) tietoläh(teen/dettä)?", "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{name}\"?", "DeleteRemotePathMapping": "Poista etäsijainnin kohdistus", @@ -856,39 +856,39 @@ "DeleteBackup": "Poista varmuuskopio", "DeletedReasonEpisodeMissingFromDisk": "{appName} ei löytänyt tiedostoa levyltä, joten sen kytkös tietokonnassa olevaan jaksoon purettiin.", "Details": "Tiedot", - "DownloadClient": "Lataustyökalu", + "DownloadClient": "Latauspalvelu", "DisabledForLocalAddresses": "Ei käytössä paikallisissa osoitteissa", "DownloadClientDelugeValidationLabelPluginFailure": "Label-tunnisteen määritys epäonnistui.", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorioiden käyttö edellyttää, että {clientName}n Label-tunnistelisäosa on käytössä.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisää median ominaisuuksia tunnisteina. Vihjeet ovat esimerkkejä.", - "DownloadClientFloodSettingsTagsHelpText": "Latauksen alkuperäiset tunnisteet. Jotta se voidaa tunnistaa, on latauksella oltava sen alkuperäiset tunnisteet. Tämä välttää ristiriidat muiden latausten kanssa.", + "DownloadClientFloodSettingsTagsHelpText": "Latauksen alkuperäiset tunnisteet, jotka tarvitaan sen tunnistamiseen. Tämä välttää ristiriidat muiden latausten kanssa.", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Ensimmäinen ja viimeinen ensin", "Donate": "Lahjoita", "DiskSpace": "Levytila", "DownloadClientDelugeTorrentStateError": "Deluge ilmoittaa virhettä", "DownloadClientFreeboxApiError": "Freebox-rajapinta palautti virheen: {errorDescription}", - "DownloadClientFreeboxAuthenticationError": "Freebox API -todennus epäonnistui. Syy: {errorDescription}.", + "DownloadClientFreeboxAuthenticationError": "Freebox API -todennus epäonnistui. {errorDescription}.", "Download": "Lataa", "DownloadClientQbittorrentSettingsUseSslHelpText": "Käytä suojattua yhteyttä. Katso qBittorentin asetusten \"Selainkäyttö\"-osion \"Käytä HTTPS:ää HTTP:n sijaan\" -asetus.", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Aloita lataamalla ensimmäinen ja viimeinen osa (qBittorrent 4.1.0+).", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Lataa ensimmäinen ja viimeinen osa ensin (qBittorrent 4.1.0+).", "DownloadClientRTorrentSettingsAddStopped": "Lisää pysäytettynä", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" on määritetty poistamaan valmistuneet lataukset, jonka seuraksena ne saatetaan poistaa ennen kuin {appName} ehtii tuoda niitä.", - "DownloadClientQbittorrentTorrentStatePathError": "Tuonti ei onnistu. Tiedostosijainti vastaa lataustyökalun perussijaintia. Ehkä \"Säilytä ylätason kansio\" ei ole käytössä tälle torrentille tai \"Torrentin sisällön asettelu\" -asetuksena EI OLE \"Alkuperäinen\" tai \"Luo alikansio\"?", - "DownloadClientSeriesTagHelpText": "Lataustyökalua käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Latauspalvelu {downloadClientName} on määritetty poistamaan valmistuneet lataukset, jonka seuraksena ne saatetaan poistaa ennen kuin {appName} ehtii tuoda niitä.", + "DownloadClientQbittorrentTorrentStatePathError": "Tuonti ei onnistu. Tiedostosijainti vastaa latauspalvelun latauskansiota. Ehkä \"Säilytä ylätason kansio\" ei ole käytössä tälle torrentille tai \"Torrentin sisällön asettelu\" -asetuksena EI OLE \"Alkuperäinen\" tai \"Luo alikansio\"?", + "DownloadClientSeriesTagHelpText": "Latauspalvelua käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", "DownloadClientValidationGroupMissing": "Ryhmää ei ole olemassa", - "DownloadClients": "Lataustyökalut", + "DownloadClients": "Latauspalvelut", "Edit": "Muokkaa", "EditAutoTag": "Muokkaa automaattimerkintää", "EditQualityProfile": "Muokkaa laatuprofiilia", - "DownloadClientsLoadError": "Virhe ladattaessa lataustyökaluja", + "DownloadClientsLoadError": "Latauspalveluiden lataus epäonnistui", "DownloadFailed": "Lataus epäonnistui", - "EnableCompletedDownloadHandlingHelpText": "Tuo valmistuneet lataukset lataustyökalusta automaattisesti.", + "EnableCompletedDownloadHandlingHelpText": "Tuo valmistuneet lataukset latauspalvelusta automaattisesti.", "EditSeries": "Muokkaa sarjaa", "EpisodeGrabbedTooltip": "Jakso kaapattiin lähteestä {indexer} ja välitettiin lataajalle {downloadClient}", "EnableAutomaticSearch": "Käytä automaattihakua", - "EndedSeriesDescription": "Uusia jaksoja tai kausia ei ole odotettavissa.", + "EndedSeriesDescription": "Uusia jaksoja tai kausia ei tiettävästi ole tulossa", "EditSelectedSeries": "Muokkaa valittuja sarjoja", - "EpisodeHistoryLoadError": "Virhe ladattaessa jaksohistoriaa", + "EpisodeHistoryLoadError": "Jaksohistorian lataus epäonnistui", "Ended": "Päättynyt", "ExistingSeries": "Olemassa olevat sarjat", "FreeSpace": "Vapaa tila", @@ -897,7 +897,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 \"{title}\"?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnistanut julkaisun sarjaa ja jaksoa, eikä sen vuoksi voi tuoda sitä automaattisesti. Haluatko kaapata julkaisun \"{title}\"?", "Forums": "Keskustelualue", "ErrorLoadingPage": "Virhe ladattaessa sivua", "FormatRuntimeHours": "{hours} t", @@ -905,21 +905,21 @@ "ICalLink": "iCal-linkki", "ICalIncludeUnmonitoredEpisodesHelpText": "Sisällytä valvomattomat jaksot iCal-syötteeseen.", "HideAdvanced": "Piilota lisäasetukset", - "ManageDownloadClients": "Hallitse lataustyökaluja", + "ManageDownloadClients": "Hallitse latauspalveluita", "IndexerHDBitsSettingsCodecsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", "IconForFinalesHelpText": "Näytä kuvake sarjojen ja tuotantokausien päätösjaksoille (perustuen käytettävissä oleviin jaksotietoihin).", "IconForCutoffUnmet": "Katkaisutasokuvake", "ImportListSearchForMissingEpisodes": "Etsi puuttuvia jaksoja", "LatestSeason": "Uusin kausi", "IndexerHDBitsSettingsCodecs": "Koodekit", - "IndexerHDBitsSettingsMediums": "Mediatyypit", + "IndexerHDBitsSettingsMediums": "Muodot", "IndexerSettingsAnimeStandardFormatSearchHelpText": "Etsi animea myös vakionumeroinnilla.", "LibraryImport": "Kirjastoon tuonti", "Logout": "Kirjaudu ulos", "IndexerSettings": "Tietolähdeasetukset", "IncludeHealthWarnings": "Sisällytä kuntovaroitukset", - "ListsLoadError": "Virhe ladattaessa listoja", - "IndexerValidationUnableToConnect": "Tietolähdettä ei tavoiteta: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", + "ListsLoadError": "Listojen lataus epäonnistui", + "IndexerValidationUnableToConnect": "Tietolähteeseen ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", "MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.", "MetadataSourceSettingsSeriesSummary": "Tietoja siitä, mistä {appName} saa sarjojen ja jaksojen tiedot.", @@ -941,10 +941,10 @@ "OrganizeLoadError": "Virhe ladattaessa esikatseluita", "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 \"{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.", + "QualityDefinitionsLoadError": "Laatumääritysten lataus epäonnistui", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", + "RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon latauspalvelukohtaisiin asetuksiin.", "QualityProfile": "Laatuprofiili", "QualityLimitsSeriesRuntimeHelpText": "Rajoituksia säädetään automaattisesti sarjan esitysajan ja tiedoston sisältämän jaksomäärän perusteella.", "QualitySettingsSummary": "Laatukoot ja nimeäminen", @@ -953,13 +953,13 @@ "AutoRedownloadFailed": "Uudelleenlataus epäonnistui", "ChooseImportMode": "Valitse tuontitila", "ChooseAnotherFolder": "Valitse muu kansio", - "DownloadClientCheckNoneAvailableHealthCheckMessage": "Lataustyökaluja ei ole käytettävissä", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "Latauspalveluita ei ole käytettävissä", "Downloaded": "Ladattu", "Global": "Yleiset", "GeneralSettings": "Yleiset asetukset", "ImportListsLoadError": "Tuontilistojen lataus epäonnistui", "Importing": "Tuodaan", - "IndexerDownloadClientHealthCheckMessage": "Tietolähteet virheellisillä lataustyökaluilla: {indexerNames}.", + "IndexerDownloadClientHealthCheckMessage": "Tietolähteet virheellisillä latauspalveluilla: {indexerNames}.", "Indexer": "Tietolähde", "Location": "Sijainti", "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.", @@ -999,8 +999,8 @@ "Uptime": "Käyttöaika", "MonitorNoEpisodes": "Ei mitään", "MonitorNoEpisodesDescription": "Mitään jaksoja ei valvota.", - "RemotePathMappingWrongOSPathHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja lataustyökalun asetukset.", - "RemoveFailedDownloadsHelpText": "Poista epäonnistuneet lataukset lataustyökalun historiasta.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Etälatauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", + "RemoveFailedDownloadsHelpText": "Poista epäonnistuneet lataukset latauspalvelun historiasta.", "RemoveSelected": "Poista valitut", "RemoveSelectedBlocklistMessageText": "Haluatko varmasti poistaa valitut kohteet estolistalta?", "RemoveSelectedItemsQueueMessageText": "Haluatko varmasti poistaa jonosta {selectedCount} kohdetta?", @@ -1010,12 +1010,12 @@ "SearchForAllMissingEpisodes": "Etsi kaikkia puuttuvia jaksoja", "Seasons": "Kaudet", "SearchAll": "Etsi kaikkia", - "SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB ID-tunnisteilla (esim. \"tvdb:71663\").", - "RootFoldersLoadError": "Virhe ladattaessa juurikansioita", + "SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB-tunnisteilla (esim. \"tvdb:71663\").", + "RootFoldersLoadError": "Juurikansioiden lataus epäonnistui", "SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.", "Year": "Vuosi", "WeekColumnHeader": "Viikkosarakkeen otsikko", - "UpdateStartupNotWritableHealthCheckMessage": "Päivitystä ei voida asentaa, koska käyttäjällä \"{userName}\" ei ole kirjoitusoikeutta käynnistyskansioon \"{startupFolder}\".", + "UpdateStartupNotWritableHealthCheckMessage": "Päivitystä ei voida asentaa, koska käyttäjällä {userName} ei ole kirjoitusoikeutta käynnistyskansioon \"{startupFolder}\".", "UpgradeUntilEpisodeHelpText": "Kun tämä laatutaso on saavutettu, ei {appName} enää kaappaa jaksoja.", "No": "Ei", "CustomFilters": "Omat suodattimet", @@ -1060,9 +1060,9 @@ "ChmodFolder": "chmod-kansio", "ChangeFileDateHelpText": "Muuta tiedoston päiväys tuonnin/kirjaston uudelleentarkistuksen yhteydessä.", "ChownGroupHelpText": "Ryhmän nimi tai GID. Käytä GID:tä etätiedostojärjestelmille.", - "ClientPriority": "Lataustyökalun painotus", + "ClientPriority": "Latauspalvelun painotus", "CloneProfile": "Monista profiili", - "Connect": "Kytkökset", + "Connect": "Ilmoituspalvelut", "CopyToClipboard": "Kopioi leikepöydälle", "CustomFormat": "Mukautettu muoto", "DefaultCase": "Oletusarvoinen kirjainkoko", @@ -1097,7 +1097,7 @@ "CustomFormats": "Mukautetut muodot", "Apply": "Käytä", "AddingTag": "Tunniste lisätään", - "CountImportListsSelected": "{count} tuotilistaa on valittu", + "CountImportListsSelected": "{count} tuontilista(a) on valittu", "ImportExtraFiles": "Tuo oheistiedostot", "DeleteCustomFormat": "Poista mukautettu muoto", "Repeat": "Toista", @@ -1112,11 +1112,11 @@ "BlackholeWatchFolderHelpText": "Kansio, josta {appName}in tulee tuoda valmistuneet lataukset.", "AptUpdater": "Asenna päivitys APT-työkalun avulla", "Original": "Alkuperäiset", - "Overview": "Yleiskatsaus", + "Overview": "Tiivistelmä", "Posters": "Julisteet", "RecyclingBin": "Roskakori", "RecyclingBinCleanup": "Roskakorin tyhjennys", - "RecyclingBinCleanupHelpText": "Arvo \"0\" (nolla) poistaa automaattisen tyhjennyksen käytöstä.", + "RecyclingBinCleanupHelpText": "Poista automaattinen tyhjennys käytöstä asettamalla arvoksi 0.", "ReleaseSceneIndicatorAssumingScene": "Oletetuksena kohtausnumerointi.", "ConditionUsingRegularExpressions": "Ehto vastaa säännöllisiä lausekkeita. Huomioi, että merkeillä `\\^$.|?*+()[{`on erityismerkityksiä ja ne on erotettava `\\`-merkillä", "CreateGroup": "Luo ryhmä", @@ -1127,7 +1127,7 @@ "DeleteEpisodeFile": "Poista jakson tiedosto", "DeleteEmptyFolders": "Poista tyhjät kansiot", "DeleteImportList": "Poista tuontilista", - "DeleteNotification": "Poista ilmoitus", + "DeleteNotification": "Poista ilmoituspalvelu", "DeleteSelectedImportListsMessageText": "Haluatko varmasti poistaa valitut {count} tuontilistaa?", "DeletedReasonUpgrade": "Tiedosto poistettiin päivitetyn version tuomiseksi", "DownloadClientFreeboxNotLoggedIn": "Ei kirjautunut", @@ -1138,10 +1138,10 @@ "DeleteEpisodeFileMessage": "Haluatko varmasti poistaa kohteen \"{path}\"?", "DeleteEpisodeFromDisk": "Poista jakso levyltä", "DeleteEpisodesFiles": "Poista {episodeFileCount} jaksotiedostoa", - "DownloadPropersAndRepacksHelpTextWarning": "Käytä mukautettuja muotoja automaattisiin Proper- ja Repack-päivityksiin.", + "DownloadPropersAndRepacksHelpTextWarning": "Käytä mukautettuja muotoja automaattisiin Proper-/Repack-päivityksiin.", "EnableInteractiveSearchHelpText": "Profiilia käytetään manuaalihakuun.", "EpisodeTitleRequiredHelpText": "Viivästytä tuontia enintään kaksi vuorokautta, jos jakson nimeä käytetään nimeämiseen ja nimeä ei ole vielä julkaistu.", - "ExtraFileExtensionsHelpTextsExamples": "Esimerkiksi '\"sub, .nfo\" tai \"sub,nfo\".", + "ExtraFileExtensionsHelpTextsExamples": "Esimerkiksi \"sub, .nfo\" tai \"sub,nfo\".", "DeleteSelectedEpisodeFiles": "Poista valitut jaksotiedostot", "Grab": "Kaappaa", "ImportUsingScriptHelpText": "Kopioi tiedostot tuontia varten oman komentosarjan avulla (esim. transkoodausta varten).", @@ -1157,7 +1157,7 @@ "ReplaceWithSpaceDash": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki\"", "ReplaceWithSpaceDashSpace": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki välilyönti\"", "SearchIsNotSupportedWithThisIndexer": "Tämä tietolähde ei tue hakua.", - "DownloadClientDownloadStationProviderMessage": "{appName} ei voi yhdistää Download Stationiin, jos DSM-tilisi on määritetty käyttämään kaksivaihesita tunnistautumista.", + "DownloadClientDownloadStationProviderMessage": "{appName} ei voi muodostaa yhteyttä Download Stationiin, jos DSM-tili on määritetty käyttämään kaksivaiheista tunnistautumista.", "UnsavedChanges": "Muutoksia ei ole tallennettu", "VideoDynamicRange": "Videon dynaaminen alue", "Airs": "Esitetään", @@ -1166,10 +1166,10 @@ "Branch": "Haara", "ClickToChangeReleaseGroup": "Vaihda julkaisuryhmää painamalla tästä", "DownloadClientQbittorrentTorrentStateUnknown": "Tuntematon lataustila: {state}", - "DownloadClientQbittorrentValidationCategoryAddFailure": "Kategorian määritys epäonnistui", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} ei pyri tuomaan valmistuneita latauksia ilman kategoriamääritystä.", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Kategorian määrittäminen epäonnistui", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} ei yritä tuoda valmistuneita latauksia ilman kategoriaa.", "DownloadPropersAndRepacks": "Proper- ja repack-julkaisut", - "DownloadPropersAndRepacksHelpText": "Määrittää päivitetäänkö tiedostot automaattisesti Proper- ja Repack-julkaisuihin (kunnollinen/uudelleenpaketoitu).", + "DownloadPropersAndRepacksHelpText": "Määrittää päivitetäänkö tiedostot automaattisesti Proper-/Repack-julkaisuihin.", "SingleEpisode": "Yksittäinen jakso", "SmartReplaceHint": "\"Yhdysmerkki\" tai \"Välilyönti Yhdysmerkki\" nimen perusteella.", "FormatAgeHour": "tunti", @@ -1178,7 +1178,7 @@ "InteractiveImportNoSeason": "Jokaiselle valitulle tiedostolle on määritettävä tuotantokausi.", "InteractiveSearchSeason": "Etsi kauden kaikkia jaksoja manuaalihaulla", "Space": "Välilyönti", - "DownloadPropersAndRepacksHelpTextCustomFormat": "Käytä 'Älä suosi' -valintaa suosiaksesi mukautettujen muotojen pisteytystä Proper- ja Repack-merkintöjä enemmän.", + "DownloadPropersAndRepacksHelpTextCustomFormat": "\"Älä suosi\" käyttää Proper-/Repack-julkaisujen sijaan mukautettujen muotojen pisteytystä.", "AuthenticationRequiredPasswordHelpTextWarning": "Syötä uusi salasana", "AuthenticationMethod": "Tunnistautumistapa", "AuthenticationMethodHelpTextWarning": "Valitse sopiva tunnistautumistapa", @@ -1208,9 +1208,9 @@ "AutoRedownloadFailedHelpText": "Etsi ja pyri lataamaan eri julkaisu automaattisesti.", "Clone": "Monista", "CloneCustomFormat": "Monista mukautettu muoto", - "ConnectSettings": "Kytkösasetukset", - "ConnectionLost": "Ei yhteyttä", - "Connections": "Yhteydet", + "ConnectSettings": "Ilmoituspavelun asetukset", + "ConnectionLost": "Yhteys menetettiin", + "Connections": "Ilmoituspalvelut", "ContinuingSeriesDescription": "Lisää jaksoja/tuotantokausia on odotettavissa.", "ColonReplacementFormatHelpText": "Määritä, mitä {appName} tekee tiedostonimien kaksoispisteille.", "CountSelectedFiles": "{selectedCount} tiedostoa on valittu", @@ -1227,10 +1227,10 @@ "DotNetVersion": ".NET", "DownloadClientPneumaticSettingsStrmFolder": "Strm-kansio", "DoNotPrefer": "Älä suosi", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää etuliitteen Delugen JSON-URL-osoitteeseen (ks. {url}).", - "DownloadClientDownloadStationValidationApiVersion": "Download Stationin API-versiota ei tueta. Sen tulee olla vähintään {requiredVersion} (versioita {minVersion}–{maxVersion} tuetaan).", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää Delugen JSON-URL-osoitteeseen etuliitteen, ks. \"{url})\".", + "DownloadClientDownloadStationValidationApiVersion": "Download Stationin rajapinnan versiota ei tueta. Sen tulee olla vähintään {requiredVersion} (versioita {minVersion}–{maxVersion} tuetaan).", "DownloadClientFloodSettingsRemovalInfo": "{appName} suorittaa torrenttien automaattisen poiston sen tietolähdeastuksissa määritettyjen jakoasetusten perusteella.", - "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää etuliitteen Flood-rajapintaan (esim. {url}).", + "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää Flood-rajapintaan etuliitteen, esim. \"{url}\".", "DownloadClientRTorrentSettingsUrlPath": "URL-sijainti", "ExtraFileExtensionsHelpText": "Pilkuin eroteltu listaus tuotavista oheistiedostoista (.nfo-tiedostot tuodaan \".nfo-orig\"-nimellä).", "MultiEpisode": "Useita jaksoja", @@ -1242,7 +1242,7 @@ "StandardEpisodeFormat": "Tavallisten jaksojen kaava", "SceneNumberNotVerified": "Kohtausnumeroa ei ole vielä vahvistettu", "Scene": "Kohtaus", - "RssSyncIntervalHelpText": "Aikaväli minuutteina. Arvo \"0\" (nolla) kytkee toiminnon pois käytöstä pysäyttäen automaattisen julkaisukaappauksen täysin.", + "RssSyncIntervalHelpText": "Aikaväli minuutteina. Poista toiminto käytöstä asettamalla arvoksi 0, joka pysäyttää automaattisen julkaisukaappauksen täysin.", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Vahvista uusi salasana", "Category": "Kategoria", "ChownGroup": "chown-ryhmä", @@ -1256,19 +1256,19 @@ "CustomFormatUnknownCondition": "Tuntematon mukautetun muodon ehto \"{implementation}\".", "ColonReplacement": "Kaksoispisteen korvaus", "DeleteReleaseProfile": "Poista julkaisuprofiili", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Kategoriatuki lisättiin qBittorrent-versiossa 3.3.0. Päivitä asennuksesi tai yritä uudelleen ilman kategoriaa.", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Kategoriatuki lisättiin qBittorrentin versiossa 3.3.0. Päivitä asennuksesi tai yritä uudelleen ilman kategoriaa.", "LocalAirDate": "Paikallinen esitysaika", "IncludeCustomFormatWhenRenamingHelpText": "Mahdollista tämän muodon käyttö \"{Custom Formats}\" -nimeämiskaavan kanssa.", "BypassProxyForLocalAddresses": "Ohjaa paikalliset osoitteet välityspalvelimen ohi", "DeleteSelectedEpisodeFilesHelpText": "Haluatko varmasti poistaa valitut jaksotiedostot?", "ReplaceWithDash": "Korvaa yhdysmerkillä", - "ConnectSettingsSummary": "Ilmoitukset, yhteydet mediapalvelimiin ja soittimiin, sekä mukautetut komentosarjat.", + "ConnectSettingsSummary": "Yhteydet ilmoituspalveluihin, mediapalvelimiin ja soittimiin, sekä mukautetut komentosarjat.", "DockerUpdater": "Hanki päivitys päivittämällä Docker-säiliö", - "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määritys on suositeltavaa", + "DownloadClientQbittorrentValidationCategoryRecommended": "Kategorian määrittäminen on suositeltavaa", "NoEpisodeInformation": "Jaksotietoja ei ole saatavilla.", - "ManualGrab": "Manuaalinen kaappaus", + "ManualGrab": "Manuaalikaappaus", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Vaihtoehtoinen jaettu kansio latauksille. Käytä Download Stationin oletussijaintia jättämällä tyhjäksi.", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin \"BT/HTTP/FTP/NZB > Location\" -asetuksiin.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sinun on kirjauduttava Diskstationillesi tunnuksella {username} ja määritettävä se manuaalisesti Download Stationin asetusten kohtaan \"BT/HTTP/FTP/NZB\" > \"Location\".", "NoEpisodeOverview": "Jaksolle ei ole kuvausta.", "Table": "Taulukko", "BypassDelayIfAboveCustomFormatScoreHelpText": "Käytä ohitusta, kun julkaisun pisteytys on määritetyn mukautetun muodon vähimmäispisteytystä korkeampi.", @@ -1310,8 +1310,8 @@ "Ok": "Ok", "General": "Yleiset", "Folders": "Kansiot", - "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteissä käytettävät tietolähteet eivät ole käytettävissä hiljattaisten virheiden vuoksi", - "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Haussa käytettävät tietolähteet eivät ole käytettävissä hiljattaisten virheiden vuoksi", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat tietolähteet eivät ole hiljattaisten tietolähdevirheiden vuoksi tilapaisesti käytettävissä.", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Hakua tukevat tietolähteet eivät ole hiljattaisten tietolähdevirheiden vuoksi tilapaisesti käytettävissä.", "IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.", "IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika", "KeyboardShortcutsSaveSettings": "Tallenna asetukset", @@ -1319,23 +1319,23 @@ "ManageImportLists": "Tuontilistojen hallinta", "ManageLists": "Listojen hallunta", "MatchedToSeason": "Kohdistettu kauteen", - "Min": "Alin", + "Min": "Pienin", "MultiSeason": "Useita kausia", "NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth -todennuksen salasana.", "NotificationsNtfySettingsAccessTokenHelpText": "Valinnainen tunnistepohjainen todennus. Ensisijainen ennen käyttäjätunnusta ja salasanaa.", "NotificationsPlexSettingsAuthToken": "Todennustunniste", - "NotificationsPlexSettingsAuthenticateWithPlexTv": "Plex.tv-tunnistautuminen", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Tunnistaudu Plexillä", "Existing": "On jo olemassa", "NotificationsTelegramSettingsChatId": "Keskustelun ID", "NotificationsSlackSettingsUsernameHelpText": "Slack-julkaisulle käytettävä käyttäjätunnus", "NotificationsTelegramSettingsSendSilently": "Lähetä äänettömästi", "NotificationsTelegramSettingsTopicId": "Ketjun ID", "ProcessingFolders": "Käsittelykansiot", - "Preferred": "Tavoite", + "Preferred": "Suosittu", "SslCertPasswordHelpText": "Pfx-tiedoston salasana", "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".", "TorrentBlackholeSaveMagnetFilesReadOnly": "Vain luku", - "TestAllClients": "Lataustyökalujen testaus", + "TestAllClients": "Koesta latauspalvelut", "TorrentDelayHelpText": "Minuuttiviive, joka odotetaan ennen julkaisun Torrent-kaappausta.", "EditSelectedImportLists": "Muokkaa valittuja tuontilistoja", "EnableAutomaticAdd": "Käytä automaattilisäystä", @@ -1349,7 +1349,7 @@ "IndexerSettingsAnimeCategoriesHelpText": "Pudotusvalikko. Poista anime käytöstä jättämällä tyhjäksi.", "IndexerValidationCloudFlareCaptchaExpired": "CloudFlaren CAPTCHA-tunniste on vanhentunut. Päivitä se.", "KeyboardShortcutsConfirmModal": "Vastaa vahvistuskysymykseen hyväksyvästi", - "ManualImport": "Manuaalinen tuonti", + "ManualImport": "Manuaalituonti", "MediaManagementSettings": "Medianhallinnan asetukset", "Mechanism": "Mekanismi", "Negate": "Kiellä", @@ -1359,7 +1359,7 @@ "SelectReleaseGroup": "Aseta julkaisuryhmä", "SendAnonymousUsageData": "Lähetä nimettömiä käyttötietoja", "TorrentBlackholeSaveMagnetFiles": "Tallenna magnet-tiedostot", - "NotificationTriggersHelpText": "Valitse tämän ilmoituksen laukaisevat tapahtumat.", + "NotificationTriggersHelpText": "Valitse ilmoituksen laukaisevat tapahtumat.", "OneSeason": "1 kausi", "OpenBrowserOnStart": "Avaa selain käynnistettäessä", "Profiles": "Profiilit", @@ -1370,7 +1370,7 @@ "Security": "Suojaus", "SslCertPassword": "SSL-varmenteen salasana", "StandardEpisodeTypeDescription": "Numerointikaavalla SxxEyy julkaistut jaksot.", - "TorrentBlackholeSaveMagnetFilesExtension": "Tallennettujen magnet-tiedostojen pääte", + "TorrentBlackholeSaveMagnetFilesExtension": "Tallenna magnet-tiedostojen pääte", "NoEventsFound": "Tapahtumia ei löytynyt", "ImportListExclusions": "Tuontilistojen poikkeukset", "Logging": "Lokikirjaus", @@ -1396,7 +1396,7 @@ "EpisodeFileDeleted": "Jaksotiedosto poistettiin", "Folder": "Kansio", "Links": "Linkit", - "Max": "Korkein", + "Max": "Suurin", "MaximumLimits": "Enimmäisrajoitukset", "MinimumLimits": "Vähimmäisrajoitukset", "NoDelay": "Ei viivettä", @@ -1423,7 +1423,7 @@ "EnableRss": "Käytä RSS-syötettä", "EpisodeFileDeletedTooltip": "Jaksotiedosto poistettiin", "EpisodeHasNotAired": "Jaksoa ei ole esitetty", - "EpisodeMissingAbsoluteNumber": "Jaksolle ei ole absoluuttista jaksonumeroa", + "EpisodeMissingAbsoluteNumber": "Jaksolle ei ole absoluuttista numeroa.", "EpisodeNumbers": "Jaksojen numerointi", "Retention": "Säilytys", "ShortDateFormat": "Lyhyen päiväyksen esitys", @@ -1452,27 +1452,27 @@ "DownloadIgnoredEpisodeTooltip": "Jakson latausta ei huomioitu", "ExpandAll": "Laajenna kaikki", "Enable": "Käytä", - "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli kohtaat ongelmia näiden viestien tulkinnassa, tavoitat tukemme alla olevilla linkkeillä.", + "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli et osaa tulkita näitä viestejä, tavoitat tukemme alla olevilla linkeillä.", "MegabytesPerMinute": "Megatavua minuutissa", "MustContain": "Täytyy sisältää", "NoLinks": "Linkkejä ei ole", "Proxy": "Välityspalvelin", "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", - "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai Trakt-listoilta, ja listapoikkeusten hallinta.", + "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai palveluista, ja poikkeuslistojen hallinta.", "LongDateFormat": "Pitkän päiväyksen esitys", "UnknownEventTooltip": "Tuntematon tapahtuma", "UnknownDownloadState": "Tuntematon lataustila: {state}", "ImportScriptPath": "Tuontikomentosarjan sijainti", "NotificationsAppriseSettingsTags": "Apprisen tunnisteet", "NotificationsAppriseSettingsServerUrlHelpText": "Apprise-palvelimen URL-osoite. SIsällytä myös http(s):// ja portti (tarvittaessa).", - "DownloadClientSettingsUseSslHelpText": "Muodosta {clientName} -yhteys käyttäen salattua yhteyttä.", + "DownloadClientSettingsUseSslHelpText": "Muodosta {clientName}-yhteys käyttäen salattua yhteyttä.", "NotificationsKodiSettingsCleanLibraryHelpText": "Siivoa kirjasto päivityksen jälkeen.", - "NotificationsJoinSettingsDeviceNamesHelpText": "Pilkuin eroteltu listaus täydellisistä tai osittaisista laitenimistä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", + "NotificationsJoinSettingsDeviceNamesHelpText": "Pilkuin eroteltu listaus laitteiden täydellisistä tai osittaisista nimistä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", "FormatDateTime": "{formattedDate} {formattedTime}", "NotificationsCustomScriptSettingsArguments": "Argumentit", "NotificationsCustomScriptSettingsName": "Oma komentosarja", "NotificationsCustomScriptSettingsArgumentsHelpText": "Komentosarjalle välitettävät argumentit.", - "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Määritä kaappausilmoituksissa välitettäviä tietueet.", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Muuta kaappausilmoituksiin sisällytettäviä tietoja.", "NotificationsDiscordSettingsOnImportFields": "Tuonti-ilmoitusten tietueet", "NotificationsEmailSettingsName": "Sähköposti", "NotificationsDiscordSettingsOnManualInteractionFields": "Toimenpidetarveilmoitusten tietueet", @@ -1480,15 +1480,15 @@ "NotificationsKodiSettingsGuiNotification": "Ilmoita käyttöliittymässä", "NotificationsKodiSettingsDisplayTimeHelpText": "Määrittää ilmoituksen näyttöajan sekunteina.", "NotificationsMailgunSettingsUseEuEndpointHelpText": "Käytä MailGunin EU-päätepistettä.", - "NotificationsNtfySettingsClickUrl": "Painalluksen URL-osoite", + "NotificationsNtfySettingsClickUrl": "Painalluksen URL", "NotificationsNotifiarrSettingsApiKeyHelpText": "Käyttäjätililtäsi löytyvä rajapinnan (API) avain.", - "NotificationsNtfySettingsClickUrlHelpText": "Valinnainen URL-osoite, joka ilmoitusta painettaessa avataan.", + "NotificationsNtfySettingsClickUrlHelpText": "Valinnainen, ilmoitusta painettaessa avattava URL-osoite.", "NotificationsNtfySettingsTopics": "Topikit", "NotificationsPushcutSettingsApiKeyHelpText": "Rajapinnan (API) avaimia voidaan hallita Puscut-sovelluksen tiliosiossa.", - "NotificationsPushoverSettingsSoundHelpText": "Ilmoituksen ääni. Käytä oletusta jättämällä tyhjäksi.", + "NotificationsPushoverSettingsSoundHelpText": "Ilmoitusääni. Käytä oletusta jättämällä tyhjäksi.", "NotificationsSettingsWebhookMethodHelpText": "Lähetyksessä käytettävä HTTP-menetelmä.", "NotificationsSimplepushSettingsEvent": "Tapahtuma", - "NotificationsTelegramSettingsSendSilentlyHelpText": "Lähettää viestin äänettömästi, jolloin vastanottaja saa ilmoituksen ilman ääntä.", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Lähettää viestin äänettömästi, jolloin vastaanottajat saavat ilmoituksen ilman ääntä.", "Rejections": "Hylkäykset", "NoImportListsFound": "Tuotilistoja ei löytynyt", "OnManualInteractionRequired": "Kun tarvitaan manuaalisia toimenpiteitä", @@ -1499,11 +1499,11 @@ "BlocklistReleaseHelpText": "Estää {appName}ia lataamasta tätä julkaisua uudelleen RSS-syötteen tai automaattihaun tuloksista.", "ChangeCategory": "Vaihda kategoria", "NotificationsPushoverSettingsDevicesHelpText": "Laitenimet, joihin ilmoitukset lähetetään (lähetä kaikkiin jättämällä tyhjäksi).", - "NotificationsNtfySettingsTagsEmojisHelpText": "Valinnainen pilkuin eroteltu listaus käytettävistä tunnisteista tai emjeista.", + "NotificationsNtfySettingsTagsEmojisHelpText": "Valinnainen pilkuin eroteltu listaus käytettävistä tunnisteista tai emojeista.", "NotificationsSlackSettingsChannelHelpText": "Korvaa saapuvan webhook-viestin oletuskanavan (#other-channel).", "IgnoreDownload": "Ohita lataus", "IgnoreDownloadHint": "Estää {appName}ia käsittelemästä tätä latausta jatkossa.", - "NotificationsAppriseSettingsStatelessUrls": "Apprisen tilaton URL-osoite", + "NotificationsAppriseSettingsStatelessUrls": "Apprisen tilattomat URL:t", "NotificationsDiscordSettingsAvatarHelpText": "Muuta ilmoituksissa käytettävää käyttäjäkuvaketta.", "NotificationsDiscordSettingsAvatar": "Käyttäjäkuvake", "NotificationsEmailSettingsCcAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään kopioina.", @@ -1513,7 +1513,7 @@ "NotificationsEmbySettingsUpdateLibraryHelpText": "Määrittää päivitetäänkö palvelimen kirjasto tuonnin, uudelleennimeämisen tai poiston yhteydessä.", "NotificationsGotifySettingIncludeSeriesPoster": "Sisällytä sarjan juliste", "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Sisällytä sarjan juliste ilmoitukseen.", - "NotificationsGotifySettingsPriorityHelpText": "Ilmoituksen painotus.", + "NotificationsGotifySettingsPriorityHelpText": "Ilmoituksen ensisijaisuus.", "NotificationsJoinSettingsDeviceIds": "Laite-ID:t", "NotificationsJoinSettingsApiKeyHelpText": "Join-tilisi asetuksista löytyvä rajapinnan (API) avain (paina Join API -painiketta).", "NotificationsJoinSettingsDeviceNames": "Laitenimet", @@ -1521,7 +1521,7 @@ "NotificationsMailgunSettingsApiKeyHelpText": "MailGunissa luotu rajapinnan (API) avain.", "NotificationsMailgunSettingsUseEuEndpoint": "Käytä EU-päätepistettä", "NotificationsNtfySettingsAccessToken": "Käyttötunniste", - "NotificationsNtfySettingsServerUrl": "Palvelimen URL-osoite", + "NotificationsNtfySettingsServerUrl": "Palvelimen URL", "NotificationsNtfySettingsTagsEmojis": "Ntfy-tunnisteet ja -emojit", "NotificationsPushcutSettingsNotificationName": "Ilmoituksen nimi", "NotificationsPushoverSettingsRetryHelpText": "Hätäilmoituksen uudelleenyritysten välinen aika.", @@ -1529,32 +1529,32 @@ "NotificationsSettingsUpdateLibrary": "Päivitä kirjasto", "NotificationsSettingsUpdateMapPathsFrom": "Kohdista sijainnit lähteeseen", "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Vastaanottavan ryhmän ID tai vastaanottajan puhelinnumero.", - "NotificationsSignalValidationSslRequired": "Näyttää siltä, että SSL-yhteys vaaditaan", - "NotificationsSignalSettingsUsernameHelpText": "Käyttäjätunnus, jolla Signal-API:lle lähetettävät pyynnöt todennetaan.", - "NotificationsTelegramSettingsTopicIdHelpText": "Lähetä ilmoitus tiettyyn ketjuun määrittämällä ketjun ID. Käytä yleistä aihetta jättämällä tyhjäksi (vain superryhmille).", - "NotificationsTraktSettingsAuthenticateWithTrakt": "Trakt-tunnistautuminen.", + "NotificationsSignalValidationSslRequired": "SSL-yhteys näyttää olevan pakollinen.", + "NotificationsSignalSettingsUsernameHelpText": "Käyttäjätunnus, jolla Signalin rajapinnalle lähetettävät pyynnöt todennetaan.", + "NotificationsTelegramSettingsTopicIdHelpText": "Lähetä ilmoitus tiettyyn ketjuun määrittämällä ketjun ID-tunniste. Käytä yleistä aihetta jättämällä tyhjäksi (vain superryhmille).", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Tunnistaudu Traktilla", "NotificationsTwitterSettingsAccessToken": "Käyttötunniste", "NotificationsTwitterSettingsAccessTokenSecret": "Käyttötunniste-salaisuus", "NotificationsTwitterSettingsDirectMessage": "Suora viesti", - "NotificationsTwitterSettingsConsumerSecretHelpText": "Kuluttajan salaisuus (consumer secret) X (Twitter) -sovelluksesta.", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Kuluttajasalaisuus (\"consumer secret\") X-sovelluksesta.", "NotificationsTwitterSettingsDirectMessageHelpText": "Lähetä julkisen viestin sijaan suora viesti.", "NotificationsTwitterSettingsMention": "Maininta", "NotificationsValidationInvalidApiKeyExceptionMessage": "Rajapinnan avain ei kelpaa: {exceptionMessage}", "NotificationsValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", - "NotificationsValidationUnableToConnectToService": "Palvelua {serviceName} ei tavoiteta.", - "NotificationsValidationUnableToConnectToApi": "Palvelun {service} rajapintaa ei tavoiteta. Palvelinyhteys epäonnistui: ({responseCode}) {exceptionMessage}.", + "NotificationsValidationUnableToConnectToService": "Palveluun {serviceName} ei voitu muodostaa yhteyttä.", + "NotificationsValidationUnableToConnectToApi": "Palvelun {service} rajapintaan ei voitu muodostaa yhteyttä. Palvelinyhteys epäonnistui: ({responseCode}) {exceptionMessage}.", "ReleaseHash": "Julkaisun hajatusarvo", "False": "Epätosi", "CustomFormatsSpecificationRegularExpressionHelpText": "Mukautetun muodon säännöllisen lausekkeen kirjainkokoa ei huomioida.", "CustomFormatsSpecificationRegularExpression": "Säännöllinen lauseke", "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä saatu app_token-tietue.", - "ImportListsPlexSettingsAuthenticateWithPlex": "Plex.tv-tunnistautuminen", + "ImportListsPlexSettingsAuthenticateWithPlex": "Tunnistaudu Plexillä", "ImportListsSettingsAccessToken": "Käyttötunniste", "ManageClients": "Hallitse työkaluja", "NotificationsAppriseSettingsConfigurationKey": "Apprise-määritysavain", - "NotificationTriggers": "Laukaisimet", + "NotificationTriggers": "Ilmoituksen laukaisijat", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Pysyvää tallennustilaa käyttävän ratkaisun määritysavain. Jätä tyjäksi, jos käytetään tilatonta URL-osoitetta.", - "NotificationsAppriseSettingsServerUrl": "Apprise-palvelimen URL-osoite", + "NotificationsAppriseSettingsServerUrl": "Apprise-palvelimen URL", "NotificationsAppriseSettingsNotificationType": "Apprisen ilmoitustyyppi", "NotificationsAppriseSettingsStatelessUrlsHelpText": "Yksi tai useita pilkuin eroteltuja URL-osoitteita ilmoitusten kohdistamiseen. Jätä tyhjäksi, jos käytetään pysyvää tallennustilaa.", "NotificationsAppriseSettingsTagsHelpText": "Ilmoita vain vastaavalla tavalla merkityille kohteille.", @@ -1562,7 +1562,7 @@ "NotificationsCustomScriptSettingsProviderMessage": "Testaus suorittaa komentosarjan EventType-arvolla \"{eventTypeTest}\". Varmista, että komentosarjasi käsittelee tämän oikein.", "NotificationsDiscordSettingsWebhookUrlHelpText": "Discord-kanavan webhook-viestinnän URL-osoite.", "NotificationsDiscordSettingsAuthor": "Julkaisija", - "NotificationsDiscordSettingsOnImportFieldsHelpText": "Määritä tuonti-ilmoituksissa välitettävät tietueet.", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Muuta tuonti-ilmoituksiin sisällytettäviä tietoja.", "NotificationsEmailSettingsBccAddress": "Piilokopio-osoitteet", "NotificationsEmailSettingsCcAddress": "Kopio-osoitteet", "NotificationsEmailSettingsBccAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään piilokopioina.", @@ -1572,11 +1572,11 @@ "NotificationsEmailSettingsRecipientAddressHelpText": "Pilkuin eroteltu listaus sähköpostiosoitteista, joihin viestit lähetetään.", "NotificationsJoinSettingsNotificationPriority": "Ilmoituksen painotus", "NotificationsJoinValidationInvalidDeviceId": "Laite-ID:issä näyttäisi olevan virheitä.", - "NotificationsNtfySettingsServerUrlHelpText": "Käytä julkista palvelinta jättämällä tyhjäksi ({url}).", - "NotificationsNtfySettingsTopicsHelpText": "Pilkuin eroteltu listaus topikeista, joihin ilmoitukset lähetetään.", - "NotificationsNtfyValidationAuthorizationRequired": "Tunnistautuminen vaaditaan", + "NotificationsNtfySettingsServerUrlHelpText": "Käytä julkista palvelinta ({url}) jättämällä tyhjäksi.", + "NotificationsNtfySettingsTopicsHelpText": "Pilkuin eroteltu listaus aiheista, joille ilmoitukset lähetetään.", + "NotificationsNtfyValidationAuthorizationRequired": "Vaatii tunnistautumisen", "NotificationsPushBulletSettingSenderId": "Lähettäjän ID", - "NotificationsPushBulletSettingSenderIdHelpText": "Lähettävän laitteen ID. Käytä laitteen pushbullet.com-URL-osoitteen \"device_iden\"-arvoa (lähetä itseltäsi jättämällä tyhjäksi).", + "NotificationsPushBulletSettingSenderIdHelpText": "Lähettävän laitteen ID-tunniste. Käytä laitteen pushbullet.com-URL-osoitteen \"device_iden\"-arvoa. Lähetä itseltäsi jättämällä tyhjäksi.", "NotificationsPushBulletSettingsAccessToken": "Käyttötunniste", "NotificationsPushBulletSettingsChannelTags": "Kanavatunnisteet", "NotificationsPushcutSettingsTimeSensitive": "Kiireellinen", @@ -1590,30 +1590,30 @@ "NotificationsSimplepushSettingsEventHelpText": "Mukauta push-ilmoitusten toimintaa.", "NotificationsSignalSettingsSenderNumber": "Lähettäjän numero", "NotificationsSettingsWebhookMethod": "HTTP-menetelmä", - "NotificationsSignalSettingsPasswordHelpText": "Salasana, jolla Signal-API:lle lähetettävät pyynnöt todennetaan.", + "NotificationsSignalSettingsPasswordHelpText": "Salasana, jolla Signalin rajapinnalle lähetettävät pyynnöt todennetaan.", "NotificationsTraktSettingsExpires": "Erääntyy", "NotificationsSynologyValidationInvalidOs": "On oltava Synology", "NotificationsSynologySettingsUpdateLibraryHelpText": "Kehota paikallista localhost-synoindexiä päivittämääin kirjasto.", "NotificationsTwitterSettingsMentionHelpText": "Mainitse tämä käyttäjä lähetettävissä twiiteissä.", "OnApplicationUpdate": "Kun sovellus päivitetään", "NotificationsTwitterSettingsConsumerSecret": "Kuluttajan salaisuus", - "NotificationsTwitterSettingsConnectToTwitter": "Muodosta X (Twitter) -yhteys", + "NotificationsTwitterSettingsConnectToTwitter": "Yhdistä X:ään", "ParseModalUnableToParse": "Annetun nimikkeen jäsennys ei onnistunut. Yritä uudelleen.", "True": "Tosi", "Or": "tai", "UpdateFiltered": "Päivitä suodatetut", - "DownloadClientPriorityHelpText": "Lautaustyökalujen painotus, 1– 50 (korkein-alin). Oletusarvo on 1 ja tasaveroiset erotetaan Round-Robin-tekniikalla.", - "NotificationsEmbySettingsSendNotificationsHelpText": "Ohjeista Embyä lähettämään ilmoitukset sen määritettyihin kohteisiin. Ei toimi Jellyfinin kanssa.", - "NotificationsEmbySettingsSendNotifications": "Lähetä ilmoitukset", + "DownloadClientPriorityHelpText": "Useiden latauspalveluiden painotus, 1–50 (korkein-alin). Oletusarvo on 1 ja tasaveroiset erotetaan Round-Robin-tekniikalla.", + "NotificationsEmbySettingsSendNotificationsHelpText": "Ohjeista Embyä ilmoittamaan myös siihen kytketyille palveluille. Ei toimi Jellyfinin kanssa.", + "NotificationsEmbySettingsSendNotifications": "Lähetä ilmoituksia", "NotificationsDiscordSettingsOnGrabFields": "Kaappausilmoitusten tietueet", "NotificationsGotifySettingsAppTokenHelpText": "Gotifyn luoma sovellustunniste.", - "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Määritä toimenpidetarveilmoituksissa välitettäviä tietueet.", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Muuta manuaalitoimenpideilmoituksiin sisällytettäviä tietoja.", "NotificationsEmailSettingsRecipientAddress": "Vastaanottajien osoitteet", - "NotificationsJoinSettingsDeviceIdsHelpText": "Vanhentunut, käytä tämän sijaan laitenimiä. Pilkuin eroteltu listaus laite-ID:istä, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", + "NotificationsJoinSettingsDeviceIdsHelpText": "Vanhentunut, käytä tämän sijaan laitteiden nimiä. Pilkuin eroteltu listaus laitteiden ID-tunnisteista, joihin ilmoitukset lähetetään. Jos ei määritetty, lähetetään kaikkiin laitteisiin.", "NotificationsPushBulletSettingsDeviceIds": "Laite-ID:t", "NotificationsKodiSettingsDisplayTime": "Näytä aika", - "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", - "NotificationsSettingsUseSslHelpText": "Muodosta yhteys sovellukseen {serviceName} SSL-protokollan välityksellä.", + "NotificationsSettingsWebhookUrl": "Webhook URL", + "NotificationsSettingsUseSslHelpText": "Muodosta yhteys palveluun {serviceName} SSL-protokollan välityksellä.", "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}-sijainti, jonka perusteella sarjojen sijainteja muutetaan kun {serviceName} näkemä kirjastosijainti poikkeaa {appName}in sijainnista (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.", @@ -1624,13 +1624,13 @@ "NotificationsEmailSettingsUseEncryption": "Käytä salausta", "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimen ja näyttämään sen tiedot.", "ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.", - "NotificationsTwitterSettingsConsumerKeyHelpText": "Kuluttajan avain (consumer key) X (Twitter) -sovelluksesta.", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Kuluttaja-avain (\"consumer key\") X-sovelluksesta.", "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", - "RemoveMultipleFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", - "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista lataustyökalusta\" poistaa latauksen ja sen tiedostot.", + "RemoveMultipleFromDownloadClientHint": "Poistaa lataukset ja tiedostot latauspalvelusta.", + "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa latauksen ja sen tiedostot.", "UnableToLoadAutoTagging": "Automaattimerkinnän lataus epäonnistui", "IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein etsittäessä joiltakin tietolähteiltä RSS-syötteen tai haun välityksellä. Tämä mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, mutta ennen välitystä lataustyökalulle.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin tietolähteiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", "NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä", "NotificationsSlackSettingsIconHelpText": "Muuta Slack-julkaisuissa käytettävää kuvaketta (emoji tai URL-osoite).", "NotificationsTwitterSettingsConsumerKey": "Kuluttajan avain", @@ -1644,24 +1644,24 @@ "IgnoreDownloads": "Ohita lataukset", "IgnoreDownloadsHint": "Estää {appName}ia käsittelemästä näitä latauksia jatkossa.", "NotificationsMailgunSettingsSenderDomain": "Lähettäjän verkkotunnus", - "NotificationsSignalSettingsSenderNumberHelpText": "Signal-API:n lähettäjärekisterin puhelinnumero.", + "NotificationsSignalSettingsSenderNumberHelpText": "Signalin rajapinnan lähettäjärekisterin puhelinnumero.", "NotificationsValidationInvalidAccessToken": "Käyttötunniste on virheellinen.", "NotificationsSlackSettingsWebhookUrlHelpText": "Slack-kanavan webhook-viestinnän URL-osoite.", - "NotificationsDiscordSettingsAuthorHelpText": "Korvaa ilmoitukselle näytettävä upotettu julkaisijatieto. Jos tyhjä, käytetään instanssin nimeä.", + "NotificationsDiscordSettingsAuthorHelpText": "Korvaa ilmoituksessa näytettävä upotettu julkaisijatieto. Käytä instanssin nimeä jättämällä tyhjäksi.", "NotificationsSlackSettingsChannel": "Kanava", "NotificationsSlackSettingsIcon": "Kuvake", "NotificationsPushBulletSettingsChannelTagsHelpText": "Pilkuin eroteltu listaus kanavatunnisteista, joille ilmoitukset lähetetään.", "NotificationsPushBulletSettingsDeviceIdsHelpText": "Pilkuin eroteltu listaus laite-ID:istä, joihin ilmoitukset lähetetään (lähetä kaikille jättämällä tyhjäksi).", - "NotificationsSignalSettingsGroupIdPhoneNumber": "Ryhmän iD/puhelinnumero", + "NotificationsSignalSettingsGroupIdPhoneNumber": "Ryhmän ID/puhelinnumero", "NotificationsPushoverSettingsUserKey": "Käyttäjäavain", "NotificationsPushoverSettingsExpireHelpText": "Hätäilmoitusten uudelleenyrityksen enimmäisaika (enimmäisarvo on 86400 sekuntia).", "NotificationsSimplepushSettingsKey": "Avain", "NotificationsValidationInvalidHttpCredentials": "HTTP Auth -tunnistetiedot eivät kelpaa: {exceptionMessage}", - "NotificationsValidationUnableToConnect": "Yhteydenmuodostus ei onnistu: {exceptionMessage}", + "NotificationsValidationUnableToConnect": "Yhteyttä ei voitu muodostaa: {exceptionMessage}.", "NotificationsValidationInvalidAuthenticationToken": "Todennustunniste on virheellinen", - "RemoveFromDownloadClientHint": "Poistaa latauksen ja ladatut tiedostot lataustyökalusta.", + "RemoveFromDownloadClientHint": "Poistaa latauksen ja tiedostot latauspalvelusta.", "RemoveQueueItemRemovalMethod": "Poistotapa", - "RemoveQueueItemsRemovalMethodHelpTextWarning": "\"Poista lataustyökalusta\" poistaa lataukset ja niiden tiedostot.", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa lataukset ja niiden tiedostot.", "Umask": "Umask", "FilterGreaterThan": "on suurempi kuin", "TheTvdb": "TheTVDB", @@ -1673,24 +1673,24 @@ "FilterNotInNext": "ei seuraavina", "IndexerValidationNoResultsInConfiguredCategories": "Kysely onnistui, mutta tietolähteesi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", "TvdbId": "TheTVDB ID", - "DownloadClientSettingsInitialState": "Virheellinen tila", + "DownloadClientSettingsInitialState": "Aloitustila", "DownloadClientSettingsRecentPriority": "Uusien painotus", - "DownloadClientSettingsUrlBaseHelpText": "Lisää etuliite lataustuökalun {clientName} URL-osoitteeseen, kuten {url}.", - "DownloadClientSettingsInitialStateHelpText": "Lataustyökaluun {clientName} lisättyjen torrentien aloitustila.", + "DownloadClientSettingsUrlBaseHelpText": "Lisää lataustuökalun {clientName} URL-osoitteeseen etuliitteen, esim. \"{url}\".", + "DownloadClientSettingsInitialStateHelpText": "Latauspalveluun {clientName} lisättyjen torrentien aloitustila.", "DownloadClientSettingsPostImportCategoryHelpText": "Kategoria, jonka {appName} asettaa tuonnin jälkeen. {appName} ei poista tämän kategorian torrenteja vaikka jakaminen olisi päättynyt. Säilytä alkuperäinen kategoria jättämällä tyhjäksi.", "ImportListsImdbSettingsListId": "Listan ID", "ImportListsImdbSettingsListIdHelpText": "IMDb-listan ID (esim. \"ls12345678\").", "ImportListsSonarrSettingsRootFoldersHelpText": "Lähdeinstanssin juurikansiot, joista tuodaan.", "ImportListsTraktSettingsRatingHelpText": "Suodata sarjoja arvioden perusteella (alue 0–100).", - "ImportListsTraktSettingsWatchedListFilter": "Katselulistan suodatin", - "ImportListsTraktSettingsYearsHelpText": "Suodata sarjat vuoden tai vuosivälin perusteella", - "MetadataSettingsEpisodeImages": "Jaksojen kuvat", + "ImportListsTraktSettingsWatchedListFilter": "Katseltujen listan suodatin", + "ImportListsTraktSettingsYearsHelpText": "Suodata sarjoja vuoden tai vuosivälin perusteella.", + "MetadataSettingsEpisodeImages": "Jaksojen kuvitukset", "MetadataSettingsEpisodeMetadata": "Jaksojen metatiedot", - "MetadataSettingsSeriesImages": "Sarjojen kuvat", + "MetadataSettingsSeriesImages": "Sarjojen kuvitukset", "MetadataSettingsEpisodeMetadataImageThumbs": "Jaksojen metatietojen pienoiskuvat", - "MetadataSettingsSeasonImages": "Kausien kuvat", + "MetadataSettingsSeasonImages": "Kausien kuvitukset", "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Sisällytä thumb-kuvien tunnisteet <filename>.nfo-tiedostoihin (vaatii \"Jaksojen metatiedot\" -asetuksen).", - "MetadataXmbcSettingsSeriesMetadataHelpText": "Sisällytä sarjojen täydelliset metatiedot tvshow.nfo-tiedostoihin.", + "MetadataXmbcSettingsSeriesMetadataHelpText": "Sarjojen täydelliset metatiedot tallennetaan .nfo-tiedostoihin.", "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Sisällytä JSON-muotoiset jakso-oppaat tvshow.nfo-tiedostoihin (vaatii \"Sarjan metatiedot\" -asetuksen).", "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Sisällytä sarjojen TheTVDB-URL-osoitteet tvshow.nfo-tiedostoihin (voidaan käyttään yhdessä \"Sarjan metatiedot\" -asetuksen kanssa).", "DownloadClientValidationCategoryMissingDetail": "Syötettyä kategoriaa ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.", @@ -1711,23 +1711,23 @@ "ImdbId": "IMDb ID", "MaximumSingleEpisodeAgeHelpText": "Täysiä tuotantokausia etsittäessä hyväksytään vain kausipaketit, joiden uusin jakso on tätä asetusta vanhempi. Koskee vain vakiosarjoja. Poista käytöstä asettamalla arvoksi \"0\" (nolla).", "FailedToLoadSystemStatusFromApi": "Järjestelmän tilan lataus rajapinnasta epäonnistui", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Sinun on poistettava televisiojärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Sinun on poistettava televisiojärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", "IndexerValidationNoRssFeedQueryAvailable": "RSS-syötekyselyä ei ole käytettävissä. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sinun on poistettava päiväysjärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Sinun on poistettava elokuvien järjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä Sabnzbd:stä.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sinun on poistettava päiväysjärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Sinun on poistettava elokuvien järjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", "DownloadClientSettingsCategoryHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa.", "DownloadClientSettingsDestinationHelpText": "Määrittää manuaalisen tallennuskohteen. Käytä oletusta jättämällä tyhjäksi.", "DownloadClientSettingsOlderPriority": "Vanhojen painotus", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Yli 14 päivää sitten julkaistujen jaksojen kaappauksille käytettävä painotus.", - "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Trakt-lajityyppien slug-arvoilla (pilkuin eroteltuna). Koskee vain suosituimpia listoja.", - "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse sarjatyyppi, jonka haluat tuoda.", + "ImportListsTraktSettingsGenresHelpText": "Suodata sarjoja Traktin lajityyppien slug-arvoilla (pilkuin eroteltuna). Koskee vain suosituimpia listoja.", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Jos \"Listan tyyppi\" on \"Valvottu\", valitse tuotava sarjatyyppi.", "MappedNetworkDrivesWindowsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja UKK:sta ({url}).", "MetadataSettingsSeriesMetadata": "Sarjojen metatiedot", "MetadataSettingsSeriesMetadataUrl": "Sarjojen metatietojen URL", "MetadataSettingsSeriesMetadataEpisodeGuide": "Sarjojen metatietojen jakso-opas", "SomeResultsAreHiddenByTheAppliedFilter": "Aktiivinen suodatin piilottaa joitakin tuloksia.", - "ChangeCategoryHint": "Vaihtaa latauksen kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", - "ChangeCategoryMultipleHint": "Vaihtaa latausten kategoriaksi lataustyökalun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", + "ChangeCategoryHint": "Vaihtaa latauksen kategoriaksi latauspalvelun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", + "ChangeCategoryMultipleHint": "Vaihtaa latausten kategoriaksi latauspalvelun \"Tuonnin jälkeinen kategoria\" -asetuksen kategorian.", "DownloadClientAriaSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Aria2:n oletusta jättämällä tyhjäksi.", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrentien jonotus ei ole käytössä qBittorent-asetuksissasi. Ota se käyttöön tai valitse painotukseksi \"Viimeiseksi\".", "DownloadClientSettingsCategorySubFolderHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa. Tämä luo latauskansioon [kategoria]-alikansion.", @@ -1740,17 +1740,17 @@ "SupportedAutoTaggingProperties": "{appName} tukee automaattimerkinnän säännöissä seuraavia arvoja", "RegularExpressionsCanBeTested": "Säännöllisiä lausekkeita voidaan testata [täällä]({url}).", "RssSyncIntervalHelpTextWarning": "Tämä koskee kaikkia tietolähteitä. Noudata niiden asettamia sääntöjä.", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla. Esimerkiksi \"{url}\". Oletus on \"{defaultApiUrl}\".", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla, esim. \"{url}\". Oletus on \"{defaultApiUrl}\".", "DownloadClientFreeboxSettingsHostHelpText": "Freeboxin isäntänimi tai IP-osoite. Oletus on \"{url}\" (toimii vain samassa verkossa).", - "DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on \"{port}\".", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on {port}.", "DownloadClientNzbVortexMultipleFilesMessage": "Lataus sisältää useita tiedostoja, eikä se ole työkansiossa: {outputPath}.", - "DownloadClientFreeboxUnableToReachFreeboxApi": "Freebox-rajapintaa ei tavoiteta. Tarkista \"Rajapinnan URL-osoite\" -asetuksen perus-URL ja versio.", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGetin \"KeepHistory\"-asetus on 0 ja tämä estää {appName}ia näkemästä valmistuneita latauksia.", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Freebox-rajapintaan ei voida muodostaa yhteyttä. Tarkista \"Rajapinnan URL\" -asetuksen perus-URL ja versio.", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGetin \"KeepHistory\"-asetus on \"0\", joka estää {appName}ia näkemästä valmistuneita latauksia.", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGetin \"KeepHistory\"-asetus on liian korkea.", "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGetin \"KeepHistory\"-asetuksen tulee olla pienempi kuin 25000.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Älä järjestele elokuvia", - "DownloadClientPneumaticSettingsNzbFolderHelpText": "Tämän kansion on oltava tavoitettavissa XBMC:stä.", - "DownloadClientValidationTestTorrents": "Torrent-listausten nouto epäonnistui: {exceptionMessage}.", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Tämän kansion on oltava Kodin tavoitettavissa.", + "DownloadClientValidationTestTorrents": "Torrent-listauksen nouto epäonnistui: {exceptionMessage}.", "ExportCustomFormat": "Vie mukautettu muoto", "ImportListsValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Tila, jossa torrentit lisätään qBittorrentiin. Huomioi, että pakotetut torrentit eivät noudata nopeusrajoituksia.", @@ -1770,7 +1770,7 @@ "DownloadClientValidationVerifySsl": "Vahvista SSL-asetukset", "ImportListRootFolderMissingRootHealthCheckMessage": "Tuontilistalta tai -listoilta puuttuu juurikansio: {rootFolderInfo}.", "Release": "Julkaisu", - "OrganizeRelativePaths": "Kaikki tiedostosijainnit on suhtetuttu sijaintiin: \"{path}\".", + "OrganizeRelativePaths": "Kaikki tiedostosijainnit on suhteutettu sijaintiin: \"{path}\".", "TorrentDelayTime": "Torrent-viive: {torrentDelay}", "TorrentBlackholeTorrentFolder": "Torrent-kansio", "UsenetBlackholeNzbFolder": "NZB-kansio", @@ -1781,14 +1781,14 @@ "ListRootFolderHelpText": "Juurikansio, johon listan kohteet lisätään.", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Älä järjestele sarjoja", "EnableAutomaticAddSeriesHelpText": "Lisää tämän listan sarjat {appName}iin kun synkronointi suoritetaan manuaalisesti käyttöliittymästä tai {appName}in toimesta.", - "DownloadClientValidationTestNzbs": "NZB-listausten nouto epäonnistui: {exceptionMessage}.", - "DownloadClientValidationUnableToConnect": "Lataustyökalua {clientName} ei tavoitettu", + "DownloadClientValidationTestNzbs": "NZB-listauksen nouto epäonnistui: {exceptionMessage}.", + "DownloadClientValidationUnableToConnect": "Latauspalveluun {clientName} ei voida muodostaa yhteyttä", "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.", "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.", "AddDelayProfileError": "Virhe lisättäessä viiveporofiilia. Yritä uudelleen.", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", - "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää etuliite lataustyökalun {clientName} RPC-URL-osoitteeseen. Esimerkiksi {url}. Oletus on \"{defaultUrl}\".", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää latauspalvelun {clientName} RPC-URL-osoitteeseen etuliitteen, esim. \"{url}\". Oletus on \"{defaultUrl}\".", "DownloadClientValidationApiKeyIncorrect": "Rajapinnan avain ei kelpaa", "HiddenClickToShow": "Piilotettu, näytä painamalla tästä", "ImportListsCustomListValidationAuthenticationFailure": "Tunnistautuminen epäonnistui", @@ -1801,22 +1801,22 @@ "WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats).", "LabelIsRequired": "Nimi on pakollinen", "SetIndexerFlags": "Aseta tietolähteen liput", - "ClickToChangeIndexerFlags": "Vaihda tietolähteen lippuja painamalla tästä", + "ClickToChangeIndexerFlags": "Muuta tietolähteen lippuja painamalla tästä", "CustomFormatsSpecificationFlag": "Lippu", "SelectIndexerFlags": "Valitse tietolähteen liput", - "SetIndexerFlagsModalTitle": "{modalTitle} - Aseta tietolähteen liput", + "SetIndexerFlagsModalTitle": "{modalTitle} – Aseta tietolähteen liput", "CustomFilter": "Mukautettu suodatin", "Label": "Nimi", "ShowTagsHelpText": "Näytä tunnisteet julisteen alla.", "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Kun tämä mukautetun muodon pisteytys on saavutettu, ei {appName} enää kaappaa jaksoja.", "IndexerFlags": "Tietolähteen liput", "AutoTaggingSpecificationTag": "Tunniste", - "ImportListsSimklSettingsUserListTypeCompleted": "Katseltu", + "ImportListsSimklSettingsUserListTypeCompleted": "Katsellut", "UrlBaseHelpText": "Käänteisen välityspalvelimen tukea varten. Oletusarvo on tyhjä.", "IndexerSettingsMultiLanguageRelease": "Useat kielet", "ReleaseProfileIndexerHelpTextWarning": "Jos julkaisuprofiilille määritetään tietty tietolähde, koskee se vain kyseisen tietolähteen julkaisuja.", - "ConnectionSettingsUrlBaseHelpText": "Lisää palvelimen {connectionName} URL-osoitteeseen etuliite, kuten \"{url}\".", - "Script": "Skripti", + "ConnectionSettingsUrlBaseHelpText": "Lisää palvelimen {connectionName} URL-osoitteeseen etuliitteen, esim. \"{url}\".", + "Script": "Komentosarja", "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Vaihtoehtoinen sijainti, johon valmistuneet lataukset siirretään. Käytä Delugen oletusta jättämällä tyhjäksi.", "DownloadClientDelugeSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Delugen oletusta jättämällä tyhjäksi.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä rTorrentin oletusta jättämällä tyhjäksi.", @@ -1832,5 +1832,317 @@ "FolderNameTokens": "Kansionimimuuttujat", "Delay": "Viive", "DeleteSelectedCustomFormats": "Poista mukautetut muodot", - "DownloadClientUnavailable": "Lataustyökalu ei ole käytettävissä" + "DownloadClientUnavailable": "Latauspalvelu ei ole käytettävissä", + "NoBlocklistItems": "Estettyjä kohteita ei ole", + "NoCustomFormatsFound": "Mukautettuja muotoja ei löytynyt", + "NotificationsGotifySettingsPreferredMetadataLink": "Ensisijainen metatietolinkki", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Metatietolinkki alustoille vain yhtä linkkiä tukeville alustoille.", + "NotificationsSettingsWebhookHeaders": "Otsakkeet", + "ImportListsMyAnimeListSettingsListStatus": "Listan tila", + "ImportListStatusAllUnavailableHealthCheckMessage": "Mitkään listat eivät ole virheiden vuoksi käytettävissä", + "MetadataKometaDeprecatedSetting": "Poistunut", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.", + "OnFileImport": "Kun tiedosto tuodaan", + "OnFileUpgrade": "Kun tiedosto päivitetään", + "ReleaseProfile": "Julkaisuprofiili", + "ShowTags": "Näytä tunnisteet", + "TodayAt": "Tänään klo {time}", + "ClickToChangeReleaseType": "Vaihda julkaisun tyyppiä painamalla tästä", + "CustomFormatsSpecificationSource": "Lähde", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent ilmoittaa puuttuvista tiedostoista", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Poista SABnbzd:n \"Tarkista ennen lataamista\" -asetus käytöstä", + "Menu": "Valikko", + "DownloadClientDelugeSettingsDirectory": "Latauskansio", + "UpdateStartupTranslocationHealthCheckMessage": "Päivitystä ei voida asentaa, koska käynnistyskansio \"{startupFolder}\" sijaitsee \"App Translocation\" -kansiossa.", + "Priority": "Painotus", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} toivoo jokaisen latauksen olevan omassa kansiossaan. Jos kansioon/polkuun lisätään tähtimerkki (*) SABnzbd ei luo näitä työkansioita. Korjaa tämä SABnzb:stä.", + "DownloadClientDelugeSettingsDirectoryCompleted": "Kansio, johon valmistuneet siirretään", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "Kehitysversioita käytettäessä {appName} ei välttämättä tue SABnzbd:n uusia ominaisuuksia.", + "IndexerSettingsMultiLanguageReleaseHelpText": "Mitkä kielet tämän tietolähteen monikielisiin julkaisuihin yleensä sisältyvät?", + "DownloadClientValidationSslConnectFailureDetail": "{appName} ei voi muodostaa salattua SSL-yhteyttä latauspalveluun {clientName}. Tämä voi olla laitekohtainen ongelma. Kokeile muodostaa yhteys määrittämällä {appName} ja latauspalvelu {clientName} käyttämään suojaamatonta yhteyttä.", + "DownloadClientValidationAuthenticationFailureDetail": "Vahvista käyttäjätunnuksesi ja salasanasi. Varmista myös, ettei latauspalveluun {clientName} ole määritetty sääntöjä, jotka estävät {appName}in suorittavaa laitetta tavoittamasta sitä.", + "CustomFormatsSpecificationLanguage": "Kieli", + "Files": "Tiedostot", + "DownloadClientValidationVerifySslDetail": "Varmista SSL-asetuksesi latauspalvelusta {clientName} ja {appName}ista.", + "InstallMajorVersionUpdate": "Asenna päivitys", + "MetadataKometaDeprecated": "Kometa-tiedostoja ei enää luoda ja tuki poistuu täysin versiossa 5.", + "ReleaseGroupFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Julkaisuryhmä:30}\"), että alusta (esim. \"{Julkaisuryhmä:-30}\") tuetaan.", + "InstallMajorVersionUpdateMessage": "Tämä päivitys asentaa uuden pääversion, joka ei välttämättä ole yhteensopiva laitteistosi kanssa. Haluatko varmasti asentaa päivityksen?", + "MinimumCustomFormatScoreIncrementHelpText": "Pienin vaadittu olemassa olevien ja uusien julkaisujen välinen mukautetun muodon pisteytyksen korotus ennen kuin {appName} tulkitsee julkaisun päivitykseksi.", + "NotificationsGotifySettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.", + "NotificationsPlexSettingsServerHelpText": "Valitse tunnistautumisen jälkeen palvelin Plex.tv-tililtä.", + "EpisodeTitleFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{jakson nimi:30}\"), että alusta (esim. \"{jakson nimi:-30}\") tuetaan. Tarvittaessa jaksojen nimet lyhennetään automaattisesti järjestelmän rajoitukseen.", + "SeriesFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Sarjan nimi:30}\"), että alusta (esim. \"{Sarjan nimi:-30}\") tuetaan.", + "NotificationsGotifySettingsMetadataLinks": "Metatietolinkit", + "NotificationsTelegramSettingsMetadataLinks": "Metatietolinkit", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Ilmoituksiin voidaan tarvittaessa sisällyttää instannin nimi.", + "NotificationsTelegramSettingsIncludeInstanceName": "Sisällytä instanssin nimi otsikkoon", + "NzbgetHistoryItemMessage": "PAR-tila: {parStatus} – Purun tila: {unpackStatus} - Siirron tila: {moveStatus} – Komentosarjan tila: {scriptStatus} – Poiston tila: {deleteStatus} – Merkinnän tila: {markStatus}", + "Warning": "Varoitus", + "CleanLibraryLevel": "Kirjaston siivoustaso", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Tunnistaudu MyAnimeListilla", + "SkipFreeSpaceCheckHelpText": "Käytä, kun {appName} ei kykene tunnistamaan juurikansiosi käytettävissä olevaa vapaata tallennustilaa.", + "CustomFormatsSpecificationMaximumSize": "Enimmäiskoko", + "CustomFormatsSpecificationMinimumSizeHelpText": "Julkaisun tulee olla vähintään tämän kokoinen.", + "ImportListsAniListSettingsImportWatchingHelpText": "Lista: parhaillaan katselussa", + "EpisodeRequested": "Jaksoa pyydettiin", + "ImportListsAniListSettingsImportWatching": "Tuonnin katselu", + "AutoTaggingSpecificationMinimumYear": "Pienin vuosi", + "AutoTaggingSpecificationMaximumYear": "Suurin vuosi", + "AutoTaggingSpecificationOriginalLanguage": "Kieli", + "ImportListsCustomListSettingsName": "Mukautettu lista", + "BlocklistFilterHasNoItems": "Valitulla estolistasuodattimella ei löydy kohteita", + "Mixed": "Sekoitettu", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "\"Tarkista ennen lataamista\" vaikuttaa {appName}in kykyyn valvoa uusia latauksia. Lisäksi SABnzb suosittelee tämän sijaan \"Peru lataukset, jotka eivät voi valmistua\" -asetusta, koska se on varmatoimisempi.", + "CustomFormatsSpecificationMaximumSizeHelpText": "Julkaisun tulee olla enintään tämän kokoinen.", + "YesterdayAt": "Eilen klo {time}", + "NotificationsTelegramSettingsIncludeAppName": "Sisällytä {appName} otsikkoon", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Ilmoitukset voidaan tarvittaessa erottaa muista sovelluksista lisäämällä niiden eteen \"{appName}\".", + "Install": "Asenna", + "LastSearched": "Edellinen haku", + "UnableToImportAutomatically": "Automaattinen tuonti ei onnistu", + "ImportListsAniListSettingsImportCompleted": "Tuonti on valmis", + "IgnoredAddresses": "Ohitetut osoitteet", + "ImportCustomFormat": "Tuo mukautettu muoto", + "DayOfWeekAt": "{day} klo {time}", + "TomorrowAt": "Huomenna klo {time}", + "CustomFormatsSettingsTriggerInfo": "Mukautettua muotoa sovelletaan julkaisuun tai tiedostoon, kun ainakin yksi valituista ehtotyypeistä täsmää.", + "CustomColonReplacementFormatHint": "Kelvollinen tiedostojärjestelmän merkki, kuten kaksoispiste (kirjain).", + "CustomColonReplacementFormatHelpText": "Kaksoispisteiden korvaukseen käytettävät merkit.", + "CustomColonReplacement": "Mukautettu kaksoispisteiden korvaus", + "LogSizeLimit": "Lokin kokorajoitus", + "LogSizeLimitHelpText": "Lokitiedoston enimmäiskoko ennen pakkausta. Oletusarvo on 1 Mt.", + "CountVotes": "{votes} ääntä", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Media: esitys ei ole vielä alkanut", + "CutoffNotMet": "Katkaisutasoa ei ole saavutettu", + "DatabaseMigration": "Tietokannan siirto", + "Fallback": "Varmistus", + "ImportListsAniListSettingsImportReleasingHelpText": "Media: parhaillaan esityksessä olevat uudet jaksot", + "ImportListsPlexSettingsWatchlistRSSName": "Plexin katselulistan RSS-syöte", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Haluamasi listan tyyppi, jolta tuodaan. Aseta kaikille listoille arvoksi \"All\".", + "UpdateUiNotWritableHealthCheckMessage": "Päivityksen asennus ei onnistu, koska käyttäjällä {userName} ei ole kirjoitusoikeutta käyttöliittymäkansioon \"{uiFolder}\".", + "Unlimited": "Rajoittamaton", + "FailedToFetchSettings": "Asetusten nouto epäonnistui", + "ImportListSettings": "Tuontilistojen yleisasetukset", + "CustomFormatsSpecificationExceptLanguage": "Paitsi kieli", + "CustomFormatsSpecificationExceptLanguageHelpText": "Täsmää, jos havaitaan mikä tahansa muu kuin valittu kieli.", + "DownloadClientRTorrentProviderMessage": "rTorrent ei pysäytä torrenteja niiden saavuttaessa jakomääritykset. {appName} suorittaa tietolähdeasetusten jakomäärityksiin perustuvan torrentien automaattisen poiston vain \"Poista valmistuneet\" -asetuksen ollessa käytössä. Tuonnin jälkeen se asettaa näkymän {importedView} rTorrent-näkymäksi, jota voidaan käyttää rTorrentin komentosarjojen kanssa.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Tämä lisää torrentit ja magnet-linkit rTorentiin pysäytetyssä tilassa. Tämä saattaa rikkoa margnet-tiedostot.", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Polku XMLRPC-päätteeseen, ks. \"{url}\". Käytettäessä ruTorrentia tämä on yleensä RPC2 tai [ruTorrentin sijainti]{url2}.", + "EditSelectedCustomFormats": "Muokkaa valittuja mukautettuja muotoja", + "From": "lähteestä", + "Group": "Ryhmä", + "HasUnmonitoredSeason": "Sisältää valvomattomia kausia", + "Here": "tässä", + "ImportList": "Tuontilista", + "ImportListStatusUnavailableHealthCheckMessage": "Listat eivät ole virheiden vuoksi käytettävissä: {importListNames}", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: katseltu", + "ImportListsAniListSettingsImportDropped": "Tuonti keskeytettiin", + "ImportListsAniListSettingsImportFinished": "Tuonti on valmis", + "ImportListsAniListSettingsImportFinishedHelpText": "media: kaikki jaksot on esitetty", + "ImportListsAniListSettingsImportHiatus": "Tuontitauko", + "ImportListsAniListSettingsImportPaused": "Tuonti on pysäytetty", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: aiotaan katsella", + "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: parhaillaan uudelleenkatselussa", + "ImportListsCustomListSettingsUrl": "Listan URL", + "ImportListsCustomListValidationConnectionError": "Kyseiseen URL-osoitteeseen ei voitu tehdä pyyntöä. Tilakoodi: {exceptionStatusCode}.", + "ImportListsPlexSettingsWatchlistName": "Plexin katselulista", + "ImportListsSettingsAuthUser": "Tunnista käyttäjä", + "ImportListsSettingsExpires": "Erääntyy", + "ImportListsSettingsRefreshToken": "Päivitä tunniste", + "Filters": "Suodattimet", + "ImportListsAniListSettingsImportHiatusHelpText": "media: sarja on tauolla", + "IncludeCustomFormatWhenRenaming": "Sisällytä mukautetut muodot uudelleennimettäessä", + "PreferTorrent": "Suosi torrentia", + "PreferUsenet": "Suosi Usenetiä", + "SmartReplace": "Älykäs korvaus", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Kaikki listat vaativat osittaisten noutojen vuoksi manuaalisia toimenpiteitä.", + "ImportListsAniListSettingsImportDroppedHelpText": "Lista: keskeytetty", + "ImportListsCustomListSettingsUrlHelpText": "Sarjalistan URL-osoite.", + "ImportListsSimklSettingsListType": "Listan tyyppi", + "ImportListsSimklSettingsName": "Simkl-käyttäjän katselulista", + "AutoTaggingSpecificationGenre": "Lajityypit", + "AutoTaggingSpecificationQualityProfile": "Laatuprofiili", + "AutoTaggingSpecificationRootFolder": "Juurikansio", + "AutoTaggingSpecificationSeriesType": "Sarjan tyyppi", + "AutoTaggingSpecificationStatus": "Tila", + "CustomFormatsSpecificationMinimumSize": "Vähimmäiskoko", + "ImportListsAniListSettingsAuthenticateWithAniList": "Tunnistaudu AniListilla", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: pidossa", + "ImportListsAniListSettingsImportPlanning": "Tuonnin suunnittelu", + "ImportListsAniListSettingsUsernameHelpText": "Tuotavan listan käyttäjätunnus", + "ImportListsSettingsRssUrl": "RSS-syötteen URL", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Tunnistaudu Simkl:llä", + "ImportListsTraktSettingsAdditionalParameters": "Muut parametrit", + "DownloadClientValidationSslConnectFailure": "Salattua SSL-yhteyttä ei voida muodostaa", + "HttpHttps": "HTTP(S)", + "ManageFormats": "Hallitse muotoja", + "MinimumCustomFormatScoreIncrement": "Pienin mahdollinen mukautetun muodon pisteytyksen korotus", + "FavoriteFolderAdd": "Lisää suosikkikansio", + "FavoriteFolderRemove": "Poista suosikkikansio", + "FavoriteFolders": "Suosikkikansiot", + "ImportListsAniListSettingsImportNotYetReleased": "Tuontia ei ole vielä julkaistu", + "ImportListsAniListSettingsImportReleasing": "Tuonnin julkaisu", + "ImportListsAniListSettingsImportRepeating": "Tuonnin toistuvuus", + "CountCustomFormatsSelected": "{count} mukautettu(a) muoto(a) on valittu", + "CustomFormatsSpecificationReleaseGroup": "Julkaisuryhmä", + "CustomFormatsSpecificationResolution": "Resoluutio", + "InstallMajorVersionUpdateMessageLink": "Saat lisätietoja osoitteesta [{domain}]({url}).", + "ManageCustomFormats": "Hallitse mukautettuja muotoja", + "NextAiringDate": "Seuraava esitys: {date}", + "NoLimitForAnyRuntime": "Ei toistoajan rajoitusta", + "NoMatchFound": "Hakua ei löytynyt!", + "NoMinimumForAnyRuntime": "Ei toistoajan vähimmäiskestoa", + "OnEpisodeFileDelete": "Kun jaksotiedosto poistetaan", + "Restore": "Palauta", + "Logs": "Lokitiedot", + "Pending": "Odottaa", + "QualitiesHelpText": "Listalla ylempänä olevia laatuja painotetaan enemmän vaikkei niitä ole valittu. Samoissa ryhmissä olevat laadut ovat tasaveroisia. Valitse vain halutut laadut.", + "RecentFolders": "Viimeisimmät kansiot", + "ReleaseType": "Julkaisun tyyppi", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "Tämän tietolähteen kanssa ei voida käyttää RSS-syötettä.", + "ScriptPath": "Komentosarjan sijainti", + "SslCertPath": "SSL-varmenteen sijainti", + "StartImport": "Aloita tuonti", + "Umask777Description": "{octal} – Kaikilla kirjoitus", + "Ungroup": "Pura ryhmä", + "UpdatePath": "Muuta sijaintia", + "UpdateSeriesPath": "Muuta sarjan sijaintia", + "UpgradeUntilCustomFormatScore": "Päivitä kunnes mukautetun muodon pisteytys on", + "Usenet": "Usenet", + "WaitingToProcess": "Odottaa käsittelyä", + "WithFiles": "Tiedostoineen", + "Qualities": "Laadut", + "Total": "Kaikkiaan", + "ImportListsTraktSettingsPopularName": "Traktin suosituimmat listat", + "MatchedToSeries": "Kohdistettu sarjaan", + "ListSyncTag": "Listan synkronointitunniste", + "LocalStorageIsNotSupported": "Paikallista tallennustilaa ei tueta tai se ei ole käytössä. Lisäosa tai yksityinen selaustila on saattanut poistaa sen käytöstä.", + "MarkAsFailedConfirmation": "Haluatko varmasti merkitä nimikkeen \"{sourceTitle}\" epäonnistuneeksi?", + "MyComputer": "Oma tietokone", + "Real": "Todellinen", + "Proper": "Kunnollinen", + "Umask750Description": "{octal} – Omistajalla kirjoitus, ryhmällä luku", + "ReleaseRejected": "Julkaisu hylättiin", + "ReleaseSceneIndicatorSourceMessage": "{message} julkaisua on numeroitu epäselvästi, eikä jaksoa voida tunnistaa luotettavasti.", + "RetentionHelpText": "Vain Usenet: määritä rajoittamaton säilytys asettamalla arvoksi 0.", + "SupportedIndexers": "{appName} tukee kaikkien Newznab-yhteensopivien tietolähteiden ohella myös monia muita alla listattuja tietolähteitä.", + "TagIsNotUsedAndCanBeDeleted": "Tunniste ei ole käytössä ja voidaan poistaa.", + "Umask775Description": "{octal} – Omistajalla ja ryhmällä kirjoitus, muilla luku", + "SeasonsMonitoredPartial": "Osittainen", + "WaitingToImport": "Odottaa tuontia", + "SupportedCustomConditions": "{appName} tukee alla oleviin julkaisujen ominaisuuksiin perustuvia mukautettuja ehtoja.", + "TorrentsDisabled": "Torrentit on poistettu käytöstä", + "WhyCantIFindMyShow": "Miksen löydä sarjaani?", + "ImportListsTraktSettingsUserListUsernameHelpText": "Listan tuontiin käytettävä käyttäjänimi. Käytä tunnistautunutta käyttäjää jättämällä tyhjäksi.", + "OnImportComplete": "Kun tuonti valmistuu", + "RatingVotes": "Arvioäänet", + "SeasonsMonitoredNone": "Ei mitään", + "SeasonsMonitoredAll": "Kaikki", + "Result": "Tulos", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Tunnistaudu Traktilla", + "ShowBanners": "Näytä bannerit", + "ImportListsTraktSettingsUserListTypeWatched": "Käyttäjän katseltujen lista", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Odotetut sarjat", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Viikkokohtaisesti katselluimmat sarjat", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Kuukausikohtaisesti katselluimmat sarjat", + "Premiere": "Ensiesitys", + "MetadataPlexSettingsEpisodeMappings": "Jaksojen kohdistukset", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Sisällytä .plexmatch-tiedostoon kaikkien jaksotiedostojen kohdistukset.", + "SeasonsMonitoredStatus": "Kausia valvotaan", + "NoLeaveIt": "Ei, anna olla", + "UpgradeUntil": "Päivitä kunnes", + "UsenetDisabled": "Usenet on poistettu käytöstä", + "MatchedToEpisodes": "Kohdistettu jaksoihin", + "Reorder": "Järjestä uudelleen", + "Score": "Pisteytys", + "StartProcessing": "Aloita käsittely", + "Umask755Description": "{octal} – Omistajalla kirjoitus, muilla luku", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Aiotaan katsella", + "ImportListsTraktSettingsGenres": "Lajityypit", + "ImportListsTraktSettingsListNameHelpText": "Tuotavan listan nimi. Listan on oltava julkinen tai sinulla on oltava sen käyttöoikeus.", + "ImportListsTraktSettingsPopularListTypePopularShows": "Suositut sarjat", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Kaikkien aikojen sarjasuositukset", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Kuukausikohtaiset sarjasuositukset", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Vuosikohtaiset sarjasuositukset", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Vuosikohtaisesti katselluimmat sarjat", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Kaikkien aikojen katselluimmat sarjat", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Nousevat sarjat", + "ImportListsTraktSettingsUserListName": "Trakt-käyttäjän listat", + "ImportListsTraktSettingsUsernameHelpText": "Tuotavan listan käyttäjätunnus", + "ImportListsTraktSettingsWatchedListTypeAll": "Kaikki", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100 % katseltu", + "Local": "Paikalliset", + "Paused": "Keskeytetty", + "SelectReleaseType": "Valitse julkaisun tyyppi", + "UiSettings": "Käyttöliittymän asetukset", + "Unavailable": "Ei käytettävissä", + "ImportListsSimklSettingsListTypeHelpText": "Tuotavan listan tyyppi.", + "ImportListsSimklSettingsShowType": "Sarjan tyyppi", + "ImportListsSimklSettingsShowTypeHelpText": "Tuotavan sarjan tyyppi.", + "ImportListsSimklSettingsUserListTypeDropped": "Unohdetut", + "ImportListsSimklSettingsUserListTypeHold": "Odottavat", + "ImportListsSimklSettingsUserListTypeWatching": "Katselussa", + "ImportListsTraktSettingsLimit": "Rajoitus", + "ImportListsTraktSettingsListName": "Listan nimi", + "ImportListsTraktSettingsListType": "Listan tyyppi", + "ImportListsTraktSettingsListTypeHelpText": "Tuotavan listan tyyppi.", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Jos \"Listan tyyppi\" on \"Katseltu\", valitse listan järjestyspeuste.", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Muut Trakt-rajapinnan parametrit.", + "ImportListsTraktSettingsLimitHelpText": "Rajoita noudettavien sarjojen määrää.", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Viikkokohtaiset sarjasuositukset", + "ImportListsTraktSettingsUserListTypeCollection": "Käyttäjän kokoelmat", + "ImportListsTraktSettingsUserListTypeWatch": "Käyttäjän katselulista", + "ImportListsTraktSettingsWatchedListSorting": "Katselulistan järjestys", + "ImportListsTraktSettingsYears": "Vuodet", + "ImportListsTraktSettingsWatchedListTypeInProgress": "Katselussa", + "OnEpisodeFileDeleteForUpgrade": "Kun jaksotiedosto poistetaan päivitystä varten", + "TagCannotBeDeletedWhileInUse": "Tunnistetta ei voida poistaa kun se on käytössä.", + "Umask770Description": "{octal} – Omistajalla ja ryhmällä kirjoitus", + "SslCertPathHelpText": "Pfx-tiedoston sijainti", + "Torrents": "Torrentit", + "Wiki": "Wiki", + "Level": "Taso", + "LiberaWebchat": "Libera Webchat", + "ListSyncLevelHelpText": "Kirjaston sarjoja käsitellään valinnan perusteella, jos ne poistuvat tai niitä ei löydy tuontilistoilta.", + "ListSyncTagHelpText": "Tämä tunniste lisätään, kun sarja poistuu listoilta tai sitä ei enää ole listoilla.", + "LogOnly": "Vain loki", + "MarkAsFailed": "Merkitse epäonnistuneeksi", + "ImportListsSonarrSettingsFullUrlHelpText": "Tuonnin lähteenä olevan {appName}-instanssin täydellinen URL-osoite portteineen.", + "ImportListsValidationTestFailed": "Testi keskeytettiin virheen vuoksi: {exceptionMessage}", + "IndexerSettingsRssUrlHelpText": "Syötä tietolähteen {indexer} kanssa toimivan RSS-syötteen URL-osoite.", + "ImportListsSonarrValidationInvalidUrl": "{appName} URL on virheellinen. Puuttuuko URL-perusta?", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ota valmistuneiden latausten käsittely käyttöön, jos mahdollista (ei tue useiden tietokoneiden ympäristöä).", + "InvalidFormat": "Virheellinen muoto", + "IndexerValidationUnableToConnectTimeout": "Tietolähteeseen ei voitu muodostaa yhteyttä, mahdollisesti aikakatkaisun vuoksi. yritä uudelleen tai tarkista verkkoasetukset. {exceptionMessage}.", + "IndexerSettingsCookieHelpText": "Jos sivusto vaatii RSS-syötteen käyttöön kirjautumisevästeen, on se noudettava selaimen avulla.", + "IndexerValidationUnableToConnectServerUnavailable": "Tietolähteeseen ei voitu muodostaa yhteyttä, koska sen palvelin ei ole tavoitettavissa. Yritä myöhemmin uudelleen. {exceptionMessage}.", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackettin \"all\"-päätettä ei tueta. Lisää tietolähteet yksitellen.", + "IndexerValidationUnableToConnectInvalidCredentials": "Tietolähteeseen ei voitu muodostaa yhteyttä virheellisten käyttäjätietojen vuoksi. {exceptionMessage}.", + "IndexerSettingsRssUrl": "RSS-syötteen URL", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Synkronoi sarjojen valvonta", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Synkronoi kausien valvontatila {appName}-instanssista. Tämän ollessa käytössä omaa valvontatilaa ei huomioida.", + "ImportListsValidationUnableToConnectException": "Tuontilistaan ei voitu muodostaa yhteyttä: {exceptionMessage}. Saat lisätietoja virheen lähellä olevista lokimerkinnöistä.", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "IPTorrentsin luoma täysi RSS-syöte, joka käyttää vain valitsemiasi kategorioita (HD, SD, x264, yms...).", + "IndexerJackettAllHealthCheckMessage": "Jackettin ei-tuettua \"all\"-päätettä käyttävät tietolähteet: {indexerNames}.", + "IndexerSettingsAllowZeroSizeHelpText": "Sallii syötteet, jotka eivät ilmoita julkaisujen kokoa. Huomioi, ettei kokoon liittyviä tarkistuksia tällöin suoriteta.", + "IndexerValidationSearchParametersNotSupported": "Tietolähde ei tue vaadittuja hakuparametreja.", + "ImportListsSonarrSettingsFullUrl": "Täysi URL", + "ImportListsSonarrSettingsTagsHelpText": "Lähdeinstanssin tunnisteet, joilla tuodaan.", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Ota valmistuneiden latausten käsittely käyttöön, jos mahdollista.", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Lähdeinstanssin laatuprofiilit, joilla tuodaan.", + "MountSeriesHealthCheckMessage": "Sarjan sijainnin sisältävä media on kytketty vain luku -tilassa: ", + "IndexerSettingsFailDownloads": "Hylättävät lataukset", + "IndexerSettingsFailDownloadsHelpText": "Valmistuneita latauksia käsitellessään {appName} tulkitsee valitut tiedostotyypit epäonnistuneiksi latauksiksi.", + "IndexerSettingsMinimumSeeders": "Jakajien vähimmäismäärä", + "IndexerSettingsMinimumSeedersHelpText": "Kaappaukseen vaadittava jakajien vähimmäismäärä.", + "IndexerValidationCloudFlareCaptchaRequired": "Sivusto on suojattu CloudFlare CAPTCHA:lla ja se vaatii kelvollisen CAPTCHA-tunnisteen.", + "IndexerValidationFeedNotSupported": "Tietolähteen syötettä ei tueta: {exceptionMessage}", + "IndexerValidationJackettAllNotSupported": "Jackettin \"all\"-päätettä ei tueta. Lisää tietolähteet yksitellen.", + "KeepAndUnmonitorSeries": "Säilytä sarja ja lopeta sen valvonta", + "KeepAndTagSeries": "Säilytä sarja ja merkitse se tunnisteella", + "IndexerValidationRequestLimitReached": "Pyyntörajoitus on saavutettu: {exceptionMessage}", + "IndexerValidationTestAbortedDueToError": "Testi keskeytettiin virheen vuoksi: {exceptionMessage}", + "IndexerValidationUnableToConnectResolutionFailure": "Tietolähteeseen ei voitu muodostaa yhteyttä. Tarkista yhteytesi tietolähteen palvelimeen ja DNS:ään. {exceptionMessage}." } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 5a673192b..d81623e1b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -2061,7 +2061,7 @@ "Umask770Description": "{octal} - Владелец и группа - запись", "UiSettings": "Настройки пользовательского интерфейса", "UiLanguage": "Язык пользовательского интерфейса", - "Ui": "Пользовательский интерфейс", + "Ui": "Интерфейс", "ShowSeriesTitleHelpText": "Показать название сериала под постером", "ShowSeasonCount": "Показать количество сезонов", "TorrentBlackholeSaveMagnetFilesExtension": "Сохранить магнет-файлы с расширением", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 19b097da0..87cb3a6a0 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -7,7 +7,7 @@ "AddConditionImplementation": "Koşul Ekle - {implementationName}", "EditConnectionImplementation": "Bildirimi Düzenle - {implementationName}", "AddConnectionImplementation": "Bağlantı Ekle - {implementationName}", - "AddIndexerImplementation": "Yeni Dizinleyici Ekle - {implementationName}", + "AddIndexerImplementation": "Yeni İndeksleyici Ekle - {implementationName}", "EditIndexerImplementation": "Koşul Ekle - {implementationName}", "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", "AddedToDownloadQueue": "İndirme kuyruğuna eklendi", @@ -31,7 +31,7 @@ "AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle", "AddImportListExclusionError": "Yeni bir liste dışlaması eklenemiyor, lütfen tekrar deneyin.", "AddImportListImplementation": "İçe Aktarım Listesi Ekle -{implementationName}", - "AddIndexer": "Dizinleyici Ekle", + "AddIndexer": "İndeksleyici Ekle", "AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın", "AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", "AddReleaseProfile": "Yayın Profili Ekle", @@ -40,7 +40,7 @@ "AddSeriesWithTitle": "{title} Ekleyin", "Agenda": "Ajanda", "Airs": "Yayınlar", - "AddIndexerError": "Yeni dizinleyici eklenemiyor, lütfen tekrar deneyin.", + "AddIndexerError": "Yeni indeksleyici eklenemiyor, lütfen tekrar deneyin.", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Karşılanmamış bölümleri aramaya başlayın", "AddQualityProfileError": "Yeni kalite profili eklenemiyor, lütfen tekrar deneyin.", "AddRemotePathMappingError": "Yeni bir uzak yol eşlemesi eklenemiyor, lütfen tekrar deneyin.", @@ -68,7 +68,7 @@ "AddRootFolderError": "Kök klasör eklenemiyor", "CountImportListsSelected": "{count} içe aktarma listesi seçildi", "CustomFormatsSpecificationFlag": "Bayrak", - "ClickToChangeIndexerFlags": "Dizinleyici bayraklarını değiştirmek için tıklayın", + "ClickToChangeIndexerFlags": "İndeksleyici bayraklarını değiştirmek için tıklayın", "ClickToChangeReleaseGroup": "Yayım grubunu değiştirmek için tıklayın", "AppUpdated": "{appName} Güncellendi", "ApplicationURL": "Uygulama URL'si", @@ -127,7 +127,7 @@ "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} dizinleyici seçildi", + "CountIndexersSelected": "{count} indeksleyici seçildi", "CustomFormatsSpecificationRegularExpressionHelpText": "Özel Format RegEx Büyük/Küçük Harfe Duyarsızdır", "AutoRedownloadFailed": "Başarısız İndirmeleri Yenile", "AutoRedownloadFailedFromInteractiveSearch": "Etkileşimli Arama'dan Başarısız İndirmeleri Yenile", @@ -155,7 +155,7 @@ "DelayMinutes": "{delay} Dakika", "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", "DeleteReleaseProfile": "Yayımlama Profilini Sil", - "DeleteSelectedIndexers": "Dizinleyicileri Sil", + "DeleteSelectedIndexers": "İndeksleyicileri Sil", "Directory": "Dizin", "Donate": "Bağış yap", "DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil", @@ -176,11 +176,11 @@ "DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?", "DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", "DeleteQualityProfileMessageText": "'{name}' kalite profilini silmek istediğinizden emin misiniz?", - "DeleteSelectedIndexersMessageText": "Seçilen {count} dizinleyiciyi silmek istediğinizden emin misiniz?", + "DeleteSelectedIndexersMessageText": "Seçilen {count} indeksleyiciyi silmek istediğinizden emin misiniz?", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName}, etiketi {clientName} uygulamasına ekleyemedi.", "DownloadClientDownloadStationProviderMessage": "DSM hesabınızda 2 Faktörlü Kimlik Doğrulama etkinleştirilmişse {appName}, Download Station'a bağlanamaz", "DownloadClientDownloadStationValidationApiVersion": "Download Station API sürümü desteklenmiyor; en az {requiredVersion} olmalıdır. {minVersion}'dan {maxVersion}'a kadar destekler", - "DownloadClientDownloadStationValidationNoDefaultDestination": "Varsayılan hedef yok", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Varsayılan hedef bulunamadı", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Medyanın özelliklerini etiket olarak ekler. İpuçları örnektir.", "DownloadClientFloodSettingsPostImportTagsHelpText": "İndirmelere içe aktarıldıktan sonra etiket ekler.", "DownloadClientFloodSettingsUrlBaseHelpText": "Flood API'sine {url} gibi bir önek ekler", @@ -195,13 +195,13 @@ "DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu", "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", "DeleteImportList": "İçe Aktarma Listesini Sil", - "IndexerPriorityHelpText": "Dizinleyici Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", + "IndexerPriorityHelpText": "İndeksleyici Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm indeksleyicileri kullanmaya devam edecek", "DisabledForLocalAddresses": "Yerel Adreslerde Devre Dışı Bırak", "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation'da '{sharedFolder}' adında bir Paylaşımlı Klasör yok, bunu doğru belirttiğinizden emin misiniz?", - "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizinleyiciler'deki geçerli başlangıç ölçütlerine göre torrentlerin otomatik olarak kaldırılmasını sağlar", + "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> İndeksleyiciler'deki geçerli başlangıç ölçütlerine göre torrentlerin otomatik olarak kaldırılmasını sağlar", "Database": "Veri tabanı", "DelayProfileProtocol": "Protokol: {preferredProtocol}", "DownloadClientDownloadStationValidationFolderMissingDetail": "'{downloadDir}' klasörü mevcut değil, '{sharedFolder}' Paylaşımlı Klasöründe manuel olarak oluşturulması gerekiyor.", @@ -321,7 +321,7 @@ "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "{downloadClientName} indirme istemcisi, tamamlanan indirmeleri kaldıracak şekilde ayarlandı. Bu, indirilenlerin {appName} içe aktarılmadan önce istemcinizden kaldırılmasına neden olabilir.", "DownloadClientQbittorrentTorrentStatePathError": "İçe Aktarılamıyor. Yol, istemci tabanlı indirme dizini ile eşleşiyor, bu torrent için 'Üst düzey klasörü tut' seçeneği devre dışı bırakılmış olabilir veya 'Torrent İçerik Düzeni' 'Orijinal' veya 'Alt Klasör Oluştur' olarak ayarlanmamış olabilir mi?", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent, Torrentleri Paylaşım Oranı Sınırına ulaştıklarında kaldıracak şekilde yapılandırılmıştır", - "DownloadClientRTorrentProviderMessage": "rTorrent, başlangıç kriterlerini karşılayan torrentleri duraklatmaz. {appName}, torrentlerin otomatik olarak kaldırılmasını Ayarlar->Dizinleyiciler'deki geçerli tohum kriterlerine göre yalnızca Tamamlandı Kaldırma etkinleştirildiğinde gerçekleştirecektir. İçe aktardıktan sonra, davranışı özelleştirmek için rTorrent komut dosyalarında kullanılabilen {importedView}'ı bir rTorrent görünümü olarak ayarlayacaktır.", + "DownloadClientRTorrentProviderMessage": "rTorrent, başlangıç kriterlerini karşılayan torrentleri duraklatmaz. {appName}, torrentlerin otomatik olarak kaldırılmasını Ayarlar->İndeksleyiciler'deki geçerli tohum kriterlerine göre yalnızca Tamamlandı Kaldırma etkinleştirildiğinde gerçekleştirecektir. İçe aktardıktan sonra, davranışı özelleştirmek için rTorrent komut dosyalarında kullanılabilen {importedView}'ı bir rTorrent görünümü olarak ayarlayacaktır.", "DownloadClientValidationAuthenticationFailureDetail": "Kullanıcı adınızı ve şifrenizi kontrol edin. Ayrıca, {appName} çalıştıran ana bilgisayarın, {clientName} yapılandırmasındaki WhiteList sınırlamaları nedeniyle {clientName} erişiminin engellenip engellenmediğini de doğrulayın.", "DownloadStationStatusExtracting": "Çıkarılıyor: %{progress}", "DownloadClientNzbVortexMultipleFilesMessage": "İndirme birden fazla dosya içeriyor ve bir iş klasöründe değil: {outputPath}", @@ -392,7 +392,7 @@ "MoveAutomatically": "Otomatik Olarak Taşı", "MustContainHelpText": "Yayın, bu terimlerden en az birini içermelidir (büyük / küçük harfe duyarsız)", "NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor", - "EditSelectedIndexers": "Seçili Dizinleyicileri Düzenle", + "EditSelectedIndexers": "Seçili İndeksleyicileri Düzenle", "EnableProfileHelpText": "Yayımlama profilini etkinleştirmek için işaretleyin", "EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli periyotlarda yayın değişikliği aradığında kullanacak", "FormatTimeSpanDays": "{days}g {time}", @@ -407,7 +407,7 @@ "FormatRuntimeHours": "{hours}s", "LanguagesLoadError": "Diller yüklenemiyor", "ListWillRefreshEveryInterval": "Liste yenileme periyodu {refreshInterval}dır", - "ManageIndexers": "Dizinleyicileri Yönet", + "ManageIndexers": "İndeksleyicileri Yönet", "ManualGrab": "Manuel Alımlarda", "DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri", "DownloadClients": "İndirme İstemcileri", @@ -445,7 +445,7 @@ "ManageLists": "Listeleri Yönet", "MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.", "Never": "Asla", - "NoIndexersFound": "Dizinleyici bulunamadı", + "NoIndexersFound": "İndeksleyici bulunamadı", "NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.", "NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası", "NotificationsAppriseSettingsUsernameHelpText": "HTTP Temel Kimlik Doğrulama Kullanıcı Adı", @@ -486,14 +486,14 @@ "Test": "Test Et", "HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [log kayıtlarınızı]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", "IndexerSettingsRejectBlocklistedTorrentHashes": "Alırken Engellenen Torrent Karmalarını Reddet", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent alındıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı indeksleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent alındıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.", "NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı", "NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü", "NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si", "NotificationsJoinSettingsApiKeyHelpText": "Katıl hesap ayarlarınızdaki API Anahtarı (API'ye Katıl düğmesine tıklayın).", "ImportScriptPathHelpText": "İçe aktarma için kullanılacak komut dosyasının yolu", "Label": "Etiket", - "NoDelay": "Gecikme yok", + "NoDelay": "Gecikmesiz", "NoImportListsFound": "İçe aktarma listesi bulunamadı", "FormatAgeMinute": "dakika", "FormatAgeMinutes": "dakika", @@ -504,7 +504,7 @@ "NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?", "NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın", "InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor", - "IndexerDownloadClientHelpText": "Bu dizinleyiciden almak için hangi indirme istemcisinin kullanılacağını belirtin", + "IndexerDownloadClientHelpText": "Bu indeksleyiciden almak için hangi indirme istemcisinin kullanılacağını belirtin", "DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil", "LastDuration": "Yürütme Süresi", "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)", @@ -526,7 +526,7 @@ "BlocklistRelease": "Kara Liste Sürümü", "CustomFormats": "Özel Formatlar", "DeleteDownloadClientMessageText": "'{name}' indirme istemcisini silmek istediğinizden emin misiniz?", - "DeleteIndexerMessageText": "'{name}' dizinleyicisini silmek istediğinizden emin misiniz?", + "DeleteIndexerMessageText": "'{name}' indeksleyicisini silmek istediğinizden emin misiniz?", "OneMinute": "1 dakika", "TaskUserAgentTooltip": "API'yi çağıran uygulama tarafından sağlanan Kullanıcı Aracısı", "SkipRedownload": "Yeniden İndirmeyi Atla", @@ -555,9 +555,9 @@ "RemoveFailedDownloads": "Başarısız İndirmeleri Kaldır", "Scheduled": "Planlı", "Underscore": "Vurgula", - "SetIndexerFlags": "Dizinleyici Bayraklarını Ayarla", + "SetIndexerFlags": "İndeksleyici Bayraklarını Ayarla", "SetReleaseGroup": "Yayımlama Grubunu Ayarla", - "SetIndexerFlagsModalTitle": "{modalTitle} - Dizinleyici Bayraklarını Ayarla", + "SetIndexerFlagsModalTitle": "{modalTitle} - İndeksleyici Bayraklarını Ayarla", "SslCertPassword": "SSL Sertifika Parolası", "Rating": "Puan", "GrabRelease": "Yayın Alma", @@ -704,7 +704,7 @@ "PreviouslyInstalled": "Daha Önce Kurulmuş", "QualityCutoffNotMet": "Kalite sınırı karşılanmadı", "QueueIsEmpty": "Kuyruk boş", - "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizinleyicinin ayarlanması, bu profilin yalnızca söz konusu dizinleyicinin yayınlarına uygulanmasına neden olur.", + "ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir indeksleyicinin ayarlanması, bu profilin yalnızca söz konusu indeksleyicinin yayınlarına uygulanmasına neden olur.", "ResetDefinitionTitlesHelpText": "Değerlerin yanı sıra tanım başlıklarını da sıfırlayın", "SecretToken": "Gizlilik Token'ı", "SetReleaseGroupModalTitle": "{modalTitle} - Yayımlama Grubunu Ayarla", @@ -720,7 +720,7 @@ "Uptime": "Çalışma süresi", "RemotePath": "Uzak Yol", "File": "Dosya", - "ReleaseProfileIndexerHelpText": "Profilin hangi dizinleyiciye uygulanacağını belirtin", + "ReleaseProfileIndexerHelpText": "Profilin hangi indeksleyiciye uygulanacağını belirtin", "TablePageSize": "Sayfa Boyutu", "NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil", "NotificationsTwitterSettingsAccessTokenSecret": "Erişim Token Gizliliği", @@ -736,7 +736,7 @@ "RemoveQueueItemRemovalMethod": "Kaldırma Yöntemi", "RemoveQueueItemRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirme işlemini ve dosyaları indirme istemcisinden kaldıracaktır.", "RemoveSelectedItems": "Seçili öğeleri kaldır", - "SelectIndexerFlags": "Dizinleyici Bayraklarını Seçin", + "SelectIndexerFlags": "İndeksleyici Bayraklarını Seçin", "Started": "Başlatıldı", "Size": "Boyut", "SupportedCustomConditions": "{appName}, aşağıdaki yayın özelliklerine göre özel koşulları destekler.", @@ -784,22 +784,22 @@ "NotificationsSignalSettingsPasswordHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan şifre", "NotificationsSimplepushSettingsEventHelpText": "Anlık bildirimlerin davranışını özelleştirme", "NotificationsSlackSettingsUsernameHelpText": "Slack'e gönderilecek kullanıcı adı", - "QueueFilterHasNoItems": "Seçilen kuyruk filtresinde hiç öğe yok", + "QueueFilterHasNoItems": "Seçilen kuyruk filtresinde hiç öğe bulunamadı", "ReleaseGroups": "Yayımlama Grupları", "IncludeCustomFormatWhenRenamingHelpText": "Özel formatları yeniden adlandırma formatına dahil et", "Logging": "Loglama", "MinutesSixty": "60 Dakika: {sixty}", "SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin", "Repack": "Yeniden paketle", - "IndexerFlags": "Dizinleyici Bayrakları", - "Indexer": "Dizinleyici", - "Indexers": "Dizinleyiciler", - "IndexerPriority": "Dizinleyici Önceliği", - "IndexerOptionsLoadError": "Dizinleyici seçenekleri yüklenemiyor", - "IndexerSettings": "Dizinleyici Ayarları", + "IndexerFlags": "İndeksleyici Bayrakları", + "Indexer": "İndeksleyici", + "Indexers": "İndeksleyiciler", + "IndexerPriority": "İndeksleyici Önceliği", + "IndexerOptionsLoadError": "İndeksleyici seçenekleri yüklenemiyor", + "IndexerSettings": "İndeksleyici Ayarları", "MustContain": "İçermeli", "MustNotContain": "İçermemeli", - "RssSyncIntervalHelpTextWarning": "Bu, tüm dizinleyiciler için geçerli olacaktır, lütfen onlar tarafından belirlenen kurallara uyun", + "RssSyncIntervalHelpTextWarning": "Bu, tüm indeksleyiciler için geçerli olacaktır, lütfen onlar tarafından belirlenen kurallara uyun", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor", "ImportListExclusions": "İçe Aktarma Listesinden Hariç Bırakılan(lar)", "UiLanguage": "Arayüz Dili", @@ -820,7 +820,7 @@ "Monitored": "Takip Ediliyor", "MonitoredOnly": "Sadece Takip Edilen", "External": "Harici", - "MassSearchCancelWarning": "Bu işlem, {appName} yeniden başlatılmadan veya tüm dizin oluşturucularınız devre dışı bırakılmadan başlatılır ise iptal edilemez.", + "MassSearchCancelWarning": "Bu, {appName} uygulamasını yeniden başlatmadan veya tüm İndeksleyiciler devre dışı bırakılmadan başlatılır ise iptal edilemez.", "IncludeUnmonitored": "Takip Edilmeyenleri Dahil Et", "MonitorSelected": "Seçilenleri Bırak", "MonitoredStatus": "Takip Edilen/Durum", @@ -968,7 +968,7 @@ "DeleteEmptySeriesFoldersHelpText": "Disk taraması sırasında ve bölüm dosyaları silindiğinde boş dizi ve sezon klasörlerini silin", "DeleteEpisodesFiles": "{episodeFileCount} Bölüm Dosyasını Sil", "DeleteImportListExclusion": "İçe Aktarma Listesi Hariç Tutmasını Sil", - "DeleteIndexer": "Dizinleyiciyi Sil", + "DeleteIndexer": "İndeksleyiciyi Sil", "Docker": "Docker", "DockerUpdater": "Güncellemeyi almak için docker konteynerini güncelleyin", "DeleteSelectedSeries": "Seçili Serileri Sil", @@ -1084,7 +1084,7 @@ "EditDelayProfile": "Gecikme Profilini Düzenle", "EventType": "Etkinlik tipi", "ImportedTo": "İçeri Aktarıldı", - "IndexerDownloadClientHealthCheckMessage": "Geçersiz indirme istemcilerine sahip dizinleyiciler: {indexerNames}.", + "IndexerDownloadClientHealthCheckMessage": "Geçersiz indirme istemcilerine sahip indeksleyiciler: {indexerNames}.", "Component": "Bileşen", "Connection": "Bağlantılar", "Reorder": "Yeniden sırala", @@ -1296,7 +1296,7 @@ "DeleteSelectedEpisodeFilesHelpText": "Seçili bölüm dosyalarını silmek istediğinizden emin misiniz?", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "{downloadClientName} ile iletişim kurulamıyor. {errorMessage}", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Son 14 gün içinde yayınlanan bölümleri almaya öncelik verin", - "EnableInteractiveSearchHelpTextWarning": "Bu dizinleyici ile arama desteklenmiyor", + "EnableInteractiveSearchHelpTextWarning": "Bu indeksleyici ile arama desteklenmiyor", "EpisodeAirDate": "Bölüm Yayın Tarihi", "EnableColorImpairedModeHelpText": "Renk engelli kullanıcıların renkleri daha iyi ayırt edebilmelerini sağlamak için değiştirilmiş stil", "EnableRss": "RSS'yi etkinleştir", @@ -1318,8 +1318,8 @@ "ImportList": "Listeler", "ImportListExclusionsLoadError": "Hariç Tutulanlar Listesi yüklenemiyor", "ImportLists": "Listeler", - "IndexersLoadError": "Dizinleyiciler yüklenemiyor", - "IndexersSettingsSummary": "Dizinleyiciler ve yayımlama kısıtlamaları", + "IndexersLoadError": "İndeksleyiciler yüklenemiyor", + "IndexersSettingsSummary": "İndeksleyiciler ve indeksleyici seçenekleri", "InteractiveImport": "Etkileşimli İçe Aktarma", "InteractiveImportNoLanguage": "Seçilen her dosya için dil seçilmelidir", "InteractiveSearch": "Etkileşimli Arama", @@ -1360,12 +1360,12 @@ "NoLeaveIt": "Hayır, Bırak", "NoLimitForAnyRuntime": "Herhangi bir çalışma zamanı için sınır yok", "NoLinks": "Bağlantı Yok", - "NoLogFiles": "Log kayıt dosyası henüz yok", + "NoLogFiles": "Log kayıt dosyası henüz oluşturulmadı", "NoMatchFound": "Eşleşme bulunamadı!", "NoMinimumForAnyRuntime": "Herhangi bir çalışma süresi için minimum değer yok", "NoResultsFound": "Sonuç bulunamadı", "NoTagsHaveBeenAddedYet": "Henüz etiket eklenmedi", - "NoUpdatesAreAvailable": "Güncelleme yok", + "NoUpdatesAreAvailable": "Güncelleme bulunamadı", "None": "Yok", "NotificationsGotifySettingsPreferredMetadataLink": "Tercih Edilen Meta Veri Bağlantısı", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Yalnızca tek bir bağlantıyı destekleyen istemciler için meta veri bağlantısı", @@ -1495,7 +1495,7 @@ "Tasks": "Görevler", "TestAll": "Tümünü Test Et", "TestAllClients": "Tüm İstemcileri Test Et", - "TestAllIndexers": "Dizinleyicileri Test Et", + "TestAllIndexers": "İndeksleyicileri Test Et", "TestAllLists": "Tüm Listeleri Test Et", "Time": "Zaman", "TimeFormat": "Zaman formatı", @@ -1658,9 +1658,9 @@ "ImportListsSimklSettingsUserListTypeHold": "Tut", "ImportListsSimklSettingsUserListTypeWatching": "İzlenen", "ImportListsSonarrSettingsFullUrl": "Tam URL", - "IndexerJackettAllHealthCheckMessage": "Desteklenmeyen Jackett 'tümü' uç noktasını kullanan dizinleyiciler: {indexerNames}", - "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle tüm dizinleyiciler kullanılamıyor", - "IndexerSearchNoInteractiveHealthCheckMessage": "Etkileşimli Arama etkinleştirildiğinde hiçbir dizinleyici kullanılamaz, {appName} herhangi bir etkileşimli arama sonucu sağlamayacaktır", + "IndexerJackettAllHealthCheckMessage": "Desteklenmeyen Jackett 'tümü' uç noktasını kullanan indeksleyiciler: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle tüm indeksleyiciler kullanılamıyor", + "IndexerSearchNoInteractiveHealthCheckMessage": "Etkileşimli Arama etkinleştirildiğinde hiçbir indeksleyici kullanılamaz, {appName} herhangi bir etkileşimli arama sonucu sağlamayacaktır", "IndexerSettingsAllowZeroSizeHelpText": "Bunu etkinleştirmek, sürüm boyutunu belirtmeyen beslemeleri kullanmanıza olanak tanır; ancak dikkatli olun, boyutla ilgili kontroller gerçekleştirilmeyecektir.", "IndexerSettingsAnimeCategoriesHelpText": "Açılır listeyi boş bırakın, animeyi devre dışı bırakın", "IndexerSettingsAnimeStandardFormatSearch": "Anime Standart Format Arama", @@ -1679,7 +1679,7 @@ "IRCLinkText": "#sonarr Daima Özgür", "EpisodeTitleRequiredHelpText": "Bölüm başlığı adlandırma biçimindeyse ve bölüm başlığı TBA ise 48 saate kadar içe aktarmayı önleyin", "FullSeason": "Tam Sezon", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle kullanılamayan dizinleyiciler: {indexerNames}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle kullanılamayan indeksleyiciler: {indexerNames}", "IndexerSettingsAdditionalParametersNyaa": "Ek Parametreler", "IndexerSettingsApiUrl": "API URL'si", "IndexerSettingsCookieHelpText": "Sitenizin rss'e erişmek için bir giriş çerezine ihtiyacı varsa, bunu bir tarayıcı aracılığıyla almanız gerekecektir.", @@ -1764,8 +1764,8 @@ "ImportListsTraktSettingsPopularListTypeTopYearShows": "Yıla Göre En Çok İzlenen Diziler", "ImportListsValidationUnableToConnectException": "İçe aktarma listesine bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin.", "ImportMechanismHandlingDisabledHealthCheckMessage": "Tamamlanmış İndirme İşlemini Etkinleştir", - "IndexerRssNoIndexersAvailableHealthCheckMessage": "Son zamanlardaki dizinleyici hataları nedeniyle tüm rss uyumlu dizinleyiciler geçici olarak kullanılamıyor", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS senkronizasyonu etkinleştirildiğinde dizinleyiciler kullanılamaz, {appName} yeni sürümleri otomatik olarak almayacaktır", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Son zamanlardaki indeksleyici hataları nedeniyle tüm rss uyumlu indeksleyiciler geçici olarak kullanılamıyor", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS senkronizasyonu etkinleştirildiğinde tüm indeksleyiciler kullanılamaz, {appName} yeni sürümleri otomatik olarak almayacaktır", "IndexerSettingsCategoriesHelpText": "Açılır listeyi boş bırakın, standart/günlük gösterileri devre dışı bırakın", "EpisodeTitleFootNote": "İsteğe bağlı olarak kesmeyi üç nokta (`...`) dahil olarak maksimum bayt boyutuna göre kontrol edin. Sondan (örn. `{Episode Title:30}`) veya başlangıçtan (örn. `{Episode Title:-30}`) kesme her ikisi de desteklenmektedir. Bölüm başlıkları, gerekirse dosya sistemi sınırlamalarına göre otomatik olarak kesilecektir.", "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} bu sürümün hangi dizi ve bölüm için olduğunu belirleyemedi. {appName} bu sürümü otomatik olarak içe aktaramayabilir. '{title}' öğesini almak ister misiniz?", @@ -1773,8 +1773,8 @@ "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Mümkünse Tamamlanmış İndirme İşlemini Etkinleştirin (Çoklu Bilgisayar desteklenmiyor)", "IndexerIPTorrentsSettingsFeedUrlHelpText": "IPTorrents tarafından yalnızca seçtiğiniz kategorileri (HD, SD, x264, vb.) kullanarak oluşturulan tam RSS besleme URL'si", "IndexerSettingsAdditionalNewznabParametersHelpText": "Lütfen kategoriyi değiştirmeniz durumunda yabancı dil sürümlerini önlemek için alt gruplar hakkında zorunlu/kısıtlı kurallar eklemeniz gerekeceğini unutmayın.", - "IndexerValidationNoResultsInConfiguredCategories": "Sorgu başarılı, ancak dizinleyicinizden yapılandırılan kategorilerde hiçbir sonuç döndürülmedi. Bu, dizinleyici veya dizinleyici kategori ayarlarınızdaki bir sorun olabilir.", - "IndexerValidationQuerySeasonEpisodesNotSupported": "Dizinleyici geçerli sorguyu desteklemiyor. Kategorilerin ve/veya sezon/bölüm aramasının desteklenip desteklenmediğini kontrol edin. Daha fazla ayrıntı için günlüğü kontrol edin.", + "IndexerValidationNoResultsInConfiguredCategories": "Sorgu başarılı, ancak indeksleyicinizden yapılandırılan kategorilerde hiçbir sonuç döndürülmedi. Bu, indeksleyici veya indeksleyici kategori ayarlarınızdan kaynaklı bir sorun olabilir.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "İndeksleyici geçerli sorguyu desteklemiyor. Kategorilerin ve/veya sezon/bölüm aramasının desteklenip desteklenmediğini kontrol edin. Daha fazla ayrıntı için günlüğü kontrol edin.", "MarkAsFailedConfirmation": "'{sourceTitle}' öğesini başarısız olarak işaretlemek istediğinizden emin misiniz?", "ErrorLoadingContent": "Bu içerik yüklenirken bir hata oluştu", "FilterContains": "içerir", @@ -1784,39 +1784,39 @@ "ImportListsTraktSettingsLimitHelpText": "Alınacak dizi sayısını sınırlayın", "ImportListsTraktSettingsUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı", "ImportListsTraktSettingsWatchedListSortingHelpText": "Liste Türü İzlenen ise, listeyi sıralamak için sırayı seçin", - "IndexerSearchNoAutomaticHealthCheckMessage": "Otomatik Arama etkinleştirildiğinde hiçbir dizinleyici kullanılamaz, {appName} herhangi bir otomatik arama sonucu sağlamayacaktır", + "IndexerSearchNoAutomaticHealthCheckMessage": "Otomatik Arama etkinleştirildiğinde hiçbir indeksleyici kullanılamaz, {appName} herhangi bir otomatik arama sonucu sağlamayacaktır", "IndexerSettingsApiUrlHelpText": "Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. API anahtarınız ana sunucuya gönderilecektir.", "IndexerSettingsSeasonPackSeedTimeHelpText": "Bir sezon paketi torrentinin durdurulmadan önce başlatılması gereken süre, boş bırakıldığında indirme istemcisinin varsayılanı kullanılır", "IndexerValidationCloudFlareCaptchaRequired": "Site CloudFlare CAPTCHA tarafından korunmaktadır. Geçerli CAPTCHA belirteci gereklidir.", - "IndexerValidationUnableToConnectServerUnavailable": "Dizinleyiciye bağlanılamıyor, dizinleyicinin sunucusu kullanılamıyor. Daha sonra tekrar deneyin. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "İndeksleyiciye bağlanılamıyor, indeksleyicinin sunucusu kullanılamıyor. Daha sonra tekrar deneyin. {exceptionMessage}.", "ImportListsTraktSettingsUserListUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı (Yetkili Kullanıcı için boş bırakın)", "ImportListsTraktSettingsYearsHelpText": "Diziyi yıla veya yıl aralığına göre filtreleyin", "IndexerHDBitsSettingsMediums": "Ortamlar", - "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Son zamanlardaki dizinleyici hataları nedeniyle tüm arama yeteneğine sahip dizinleyiciler geçici olarak kullanılamıyor", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Son zamanlardaki indeksleyici hataları nedeniyle tüm arama yeteneğine sahip indeksleyiciler geçici olarak kullanılamıyor", "IndexerSettingsSeasonPackSeedTime": "Sezon Paketi Seed Süresi", - "IndexerValidationJackettAllNotSupportedHelpText": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen dizinleyicileri tek tek ekleyin", - "IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, dizinleyici veya dizinleyici kategori ayarlarınızdaki bir sorun olabilir.", - "IndexerValidationUnableToConnectResolutionFailure": "Dizinleyiciye bağlanılamıyor bağlantı hatası. Dizinleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen indeksleyicileri tek tek ekleyin", + "IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, indeksleyici veya indeksleyici kategori ayarlarınızdan kaynaklı bir sorun olabilir.", + "IndexerValidationUnableToConnectResolutionFailure": "İndeksleyiciye bağlanılamıyor bağlantı hatası. İndeksleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.", "IndexerSettingsFailDownloads": "Başarısız İndirmeler", "IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName} bu seçili dosya türlerini başarısız indirmeler olarak değerlendirecektir.", "IndexerSettingsMinimumSeeders": "Minimum Seeder", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "{indexer} uyumlu bir RSS beslemesine URL girin", "IndexerSettingsWebsiteUrl": "Web site URL'si", - "IndexerStatusAllUnavailableHealthCheckMessage": "Tüm dizinleyiciler hatalar nedeniyle kullanılamıyor", - "IndexerStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle kullanılamayan dizinleyiciler: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tüm indeksleyiciler hatalar nedeniyle kullanılamıyor", + "IndexerStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle kullanılamayan indeksleyiciler: {indexerNames}", "IndexerTagSeriesHelpText": "Bu indeksleyiciyi yalnızca en az bir eşleşen etiketi olan seriler için kullanın. Tüm serilerle kullanmak için boş bırakın.", "IndexerValidationCloudFlareCaptchaExpired": "CloudFlare CAPTCHA token'ınızın süresi doldu, lütfen yenileyin.", - "IndexerValidationFeedNotSupported": "Dizinleyici beslemesi desteklenmiyor: {exceptionMessage}", + "IndexerValidationFeedNotSupported": "indeksleyici beslemesi desteklenmiyor: {exceptionMessage}", "IndexerValidationInvalidApiKey": "Geçersiz API Anahtarı", - "IndexerValidationJackettAllNotSupported": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen dizinleyicileri tek tek ekleyin", + "IndexerValidationJackettAllNotSupported": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen indeksleyicileri tek tek ekleyin", "IndexerValidationRequestLimitReached": "Talep sınırına ulaşıldı: {exceptionMessage}", - "IndexerValidationSearchParametersNotSupported": "Dizinleyici gerekli arama parametrelerini desteklemiyor", + "IndexerValidationSearchParametersNotSupported": "İndeksleyici gerekli arama parametrelerini desteklemiyor", "IndexerValidationTestAbortedDueToError": "Test bir hata nedeniyle iptal edildi: {exceptionMessage}", - "IndexerValidationUnableToConnect": "Dizinleyiciye bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin", - "IndexerValidationUnableToConnectHttpError": "Dizinleyiciye bağlanılamıyor, lütfen DNS ayarlarınızı kontrol edin ve IPv6'nın çalıştığından veya devre dışı olduğundan emin olun. {exceptionMessage}.", - "IndexerValidationUnableToConnectInvalidCredentials": "Dizinleyiciye bağlanılamıyor, geçersiz kimlik bilgileri. {exceptionMessage}.", - "IndexerValidationUnableToConnectTimeout": "Dizinleyiciye bağlanılamıyor, muhtemelen zaman aşımı nedeniyle. Tekrar deneyin veya ağ ayarlarınızı kontrol edin. {exceptionMessage}.", + "IndexerValidationUnableToConnect": "İndeksleyiciye bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin", + "IndexerValidationUnableToConnectHttpError": "İndeksleyiciye bağlanılamıyor, lütfen DNS ayarlarınızı kontrol edin ve IPv6'nın çalıştığından veya devre dışı olduğundan emin olun. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "İndeksleyiciye bağlanılamıyor, geçersiz kimlik bilgileri. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "İndeksleyiciye bağlanılamıyor, muhtemelen zaman aşımı nedeniyle. Tekrar deneyin veya ağ ayarlarınızı kontrol edin. {exceptionMessage}.", "InteractiveImportNoEpisode": "Her seçili dosya için bir veya daha fazla bölüm seçilmelidir", "InteractiveImportNoSeason": "Her seçilen dosya için sezon seçilmelidir", "InteractiveImportNoSeries": "Her seçilen dosya için dizi seçilmelidir", @@ -1968,7 +1968,7 @@ "ReleaseSceneIndicatorUnknownMessage": "Bu bölüm için numaralandırma değişiklik göstermektedir ve sürüm bilinen hiçbir eşleştirmeyle uyuşmamaktadır.", "ReleaseSceneIndicatorUnknownSeries": "Bilinmeyen bölüm veya dizi.", "ReleaseType": "Sürüm Türü", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker kullanıyorsunuz; indirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak bu dizin konteynerin içinde görünmüyor. Uzak yol eşlemelerinizi ve konteyner hacmi ayarlarınızı inceleyin.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker kullanıyorsunuz; indirme istemcisi {downloadClientName} indirmeleri {path} dizinine yerleştiriyor ancak bu dizin konteynerin içinde görünmüyor. Uzak yol eşlemelerinizi ve konteyner bağlama ayarlarınızı inceleyin.", "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} indirilen bölümü {path} görebiliyor ancak erişemiyor. Muhtemelen izin hatası.", "RemotePathMappingFileRemovedHealthCheckMessage": "{path} dosyası işleme sırasında kaldırıldı.", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "{downloadClientName} indirme istemcisi {path} dizinindeki dosyaları raporladı ancak {appName} bu dizini göremiyor. Klasörün izinlerini ayarlamanız gerekebilir.", From 8cd5cd603ade5fffbcef323f9c8963ad918e0ed8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 26 Dec 2024 12:32:57 -0800 Subject: [PATCH 724/762] Fixed: Improve synchronization logic for import list items Closes #7511 --- .../ImportListItemServiceFixture.cs | 250 ++++++++++++++++-- .../ImportListItems/ImportListItemService.cs | 70 ++++- 2 files changed, 294 insertions(+), 26 deletions(-) diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs index 661e2b357..ff6ac9afa 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs @@ -12,46 +12,256 @@ namespace NzbDrone.Core.Test.ImportListTests { public class ImportListItemServiceFixture : CoreTest<ImportListItemService> { - [SetUp] - public void SetUp() + private void GivenExisting(List<ImportListItemInfo> existing) { - var existing = Builder<ImportListItemInfo>.CreateListOfSize(3) - .TheFirst(1) - .With(s => s.TvdbId = 6) - .With(s => s.ImdbId = "6") - .TheNext(1) - .With(s => s.TvdbId = 7) - .With(s => s.ImdbId = "7") - .TheNext(1) - .With(s => s.TvdbId = 8) - .With(s => s.ImdbId = "8") - .Build().ToList(); Mocker.GetMock<IImportListItemInfoRepository>() .Setup(v => v.GetAllForLists(It.IsAny<List<int>>())) .Returns(existing); } [Test] - public void should_insert_new_update_existing_and_delete_missing() + public void should_insert_new_update_existing_and_delete_missing_based_on_tvdb_id() { - var newItems = Builder<ImportListItemInfo>.CreateListOfSize(3) + var existing = Builder<ImportListItemInfo>.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) .TheFirst(1) - .With(s => s.TvdbId = 5) - .TheNext(1) .With(s => s.TvdbId = 6) .TheNext(1) .With(s => s.TvdbId = 7) .Build().ToList(); + var newItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 5) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 6) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List<ImportListItemInfo> { newItem, updatedItem }; + var numDeleted = Subject.SyncSeriesForList(newItems, 1); numDeleted.Should().Be(1); + Mocker.GetMock<IImportListItemInfoRepository>() - .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 5)), Times.Once()); + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == newItem.TvdbId)), Times.Once()); + Mocker.GetMock<IImportListItemInfoRepository>() - .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 2 && s[0].TvdbId == 6 && s[1].TvdbId == 7)), Times.Once()); + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == updatedItem.TvdbId)), Times.Once()); + Mocker.GetMock<IImportListItemInfoRepository>() - .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 8)), Times.Once()); + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId != newItem.TvdbId && s[0].TvdbId != updatedItem.TvdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_imdb_id() + { + var existing = Builder<ImportListItemInfo>.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.ImdbId = "6") + .TheNext(1) + .With(s => s.ImdbId = "7") + .Build().ToList(); + + var newItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = "5") + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List<ImportListItemInfo> { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId == newItem.ImdbId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId == updatedItem.ImdbId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId != newItem.ImdbId && s[0].ImdbId != updatedItem.ImdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_tmdb_id() + { + var existing = Builder<ImportListItemInfo>.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.TmdbId = 6) + .TheNext(1) + .With(s => s.TmdbId = 7) + .Build().ToList(); + + var newItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 5) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List<ImportListItemInfo> { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId == newItem.TmdbId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId == updatedItem.TmdbId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId != newItem.TmdbId && s[0].TmdbId != updatedItem.TmdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_mal_id() + { + var existing = Builder<ImportListItemInfo>.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.MalId = 6) + .TheNext(1) + .With(s => s.MalId = 7) + .Build().ToList(); + + var newItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 5) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 6) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List<ImportListItemInfo> { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId == newItem.MalId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId == updatedItem.MalId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId != newItem.MalId && s[0].MalId != updatedItem.MalId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_anilist_id() + { + var existing = Builder<ImportListItemInfo>.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.AniListId = 6) + .TheNext(1) + .With(s => s.AniListId = 7) + .Build().ToList(); + + var newItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 5) + .Build(); + + var updatedItem = Builder<ImportListItemInfo>.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 6) + .Build(); + + GivenExisting(existing); + var newItems = new List<ImportListItemInfo> { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId == newItem.AniListId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId == updatedItem.AniListId)), Times.Once()); + + Mocker.GetMock<IImportListItemInfoRepository>() + .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId != newItem.AniListId && s[0].AniListId != updatedItem.AniListId)), Times.Once()); } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs index 852a30ee5..793d6c548 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider.Events; @@ -30,14 +31,38 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { var existingListSeries = GetAllForLists(new List<int> { listId }); - listSeries.ForEach(l => l.Id = existingListSeries.FirstOrDefault(e => e.TvdbId == l.TvdbId)?.Id ?? 0); + var toAdd = new List<ImportListItemInfo>(); + var toUpdate = new List<ImportListItemInfo>(); - _importListSeriesRepository.InsertMany(listSeries.Where(l => l.Id == 0).ToList()); - _importListSeriesRepository.UpdateMany(listSeries.Where(l => l.Id > 0).ToList()); - var toDelete = existingListSeries.Where(l => !listSeries.Any(x => x.TvdbId == l.TvdbId)).ToList(); - _importListSeriesRepository.DeleteMany(toDelete); + listSeries.ForEach(item => + { + var existingItem = FindItem(existingListSeries, item); - return toDelete.Count; + if (existingItem == null) + { + toAdd.Add(item); + return; + } + + // Remove so we'll only be left with items to remove at the end + existingListSeries.Remove(existingItem); + toUpdate.Add(existingItem); + + existingItem.Title = item.Title; + existingItem.Year = item.Year; + existingItem.TvdbId = item.TvdbId; + existingItem.ImdbId = item.ImdbId; + existingItem.TmdbId = item.TmdbId; + existingItem.MalId = item.MalId; + existingItem.AniListId = item.AniListId; + existingItem.ReleaseDate = item.ReleaseDate; + }); + + _importListSeriesRepository.InsertMany(toAdd); + _importListSeriesRepository.UpdateMany(toUpdate); + _importListSeriesRepository.DeleteMany(existingListSeries); + + return existingListSeries.Count; } public List<ImportListItemInfo> GetAllForLists(List<int> listIds) @@ -55,5 +80,38 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { return _importListSeriesRepository.Exists(tvdbId, imdbId); } + + private ImportListItemInfo FindItem(List<ImportListItemInfo> existingItems, ImportListItemInfo item) + { + return existingItems.FirstOrDefault(e => + { + if (e.TvdbId > 0 && item.TvdbId > 0 && e.TvdbId == item.TvdbId) + { + return true; + } + + if (e.ImdbId.IsNotNullOrWhiteSpace() && item.ImdbId.IsNotNullOrWhiteSpace() && e.ImdbId == item.ImdbId) + { + return true; + } + + if (e.TmdbId > 0 && item.TmdbId > 0 && e.TmdbId == item.TmdbId) + { + return true; + } + + if (e.MalId > 0 && item.MalId > 0 && e.MalId == item.MalId) + { + return true; + } + + if (e.AniListId > 0 && item.AniListId > 0 && e.AniListId == item.AniListId) + { + return true; + } + + return false; + }); + } } } From f05e552e8e6dc02cd26444073ab9a678dcb36492 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 28 Dec 2024 15:32:04 +0200 Subject: [PATCH 725/762] Suggest adding IP to RPC whitelist for on failed Transmission auth --- .../Clients/Transmission/TransmissionProxy.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index d20076153..2b827394d 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -7,6 +7,7 @@ using System.Net; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -261,7 +262,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) { - var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); + var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}"; var sessionId = _authSessionIdCache.Find(authKey); @@ -273,24 +274,26 @@ namespace NzbDrone.Core.Download.Clients.Transmission authLoginRequest.SuppressHttpError = true; var response = _httpClient.Execute(authLoginRequest); - if (response.StatusCode == HttpStatusCode.MovedPermanently) - { - var url = response.Headers.GetSingleValue("Location"); - throw new DownloadClientException("Remote site redirected to " + url); - } - else if (response.StatusCode == HttpStatusCode.Conflict) + switch (response.StatusCode) { - sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); + case HttpStatusCode.MovedPermanently: + var url = response.Headers.GetSingleValue("Location"); - if (sessionId == null) - { - throw new DownloadClientException("Remote host did not return a Session Id."); - } - } - else - { - throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); + throw new DownloadClientException("Remote site redirected to " + url); + case HttpStatusCode.Forbidden: + throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist."); + case HttpStatusCode.Conflict: + sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); + + if (sessionId == null) + { + throw new DownloadClientException("Remote host did not return a Session Id."); + } + + break; + default: + throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); } _logger.Debug("Transmission authentication succeeded."); From 8aad79fd3e14eb885724a5e5790803c289be2f25 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 30 Dec 2024 04:09:48 +0200 Subject: [PATCH 726/762] Check if backup folder is writable on backup --- src/NzbDrone.Common/ArchiveService.cs | 19 ++++++++++--------- src/NzbDrone.Core/Backup/BackupService.cs | 11 +++++++++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Common/ArchiveService.cs b/src/NzbDrone.Common/ArchiveService.cs index 1f06719c5..662538ad4 100644 --- a/src/NzbDrone.Common/ArchiveService.cs +++ b/src/NzbDrone.Common/ArchiveService.cs @@ -42,17 +42,18 @@ namespace NzbDrone.Common public void CreateZip(string path, IEnumerable<string> files) { - using (var zipFile = ZipFile.Create(path)) + _logger.Debug("Creating archive {0}", path); + + using var zipFile = ZipFile.Create(path); + + zipFile.BeginUpdate(); + + foreach (var file in files) { - zipFile.BeginUpdate(); - - foreach (var file in files) - { - zipFile.Add(file, Path.GetFileName(file)); - } - - zipFile.CommitUpdate(); + zipFile.Add(file, Path.GetFileName(file)); } + + zipFile.CommitUpdate(); } private void ExtractZip(string compressedFile, string destination) diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index e45370214..d398ef1d3 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup { _logger.ProgressInfo("Starting Backup"); + var backupFolder = GetBackupFolder(backupType); + _diskProvider.EnsureFolder(_backupTempFolder); - _diskProvider.EnsureFolder(GetBackupFolder(backupType)); + _diskProvider.EnsureFolder(backupFolder); + + if (!_diskProvider.FolderWritable(backupFolder)) + { + throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable"); + } var dateNow = DateTime.Now; var backupFilename = $"sonarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; - var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); + var backupPath = Path.Combine(backupFolder, backupFilename); Cleanup(); From ac7c05c0502e7c01f603b3a4c21b0ecb74934177 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 4 Jan 2025 16:13:35 +0000 Subject: [PATCH 727/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Matti Meikäläinen <diefor-93@hotmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 16 ++++++++-------- src/NzbDrone.Core/Localization/Core/fr.json | 12 ++++++------ src/NzbDrone.Core/Localization/Core/tr.json | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index f7fbc2413..feb8e6370 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -43,7 +43,7 @@ "TimeFormat": "Kellonajan esitys", "UiLanguage": "Käyttöliittymän kieli", "UiLanguageHelpText": "{appName}in käyttöliittymän kieli.", - "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.", + "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.", @@ -136,7 +136,7 @@ "AuthForm": "Lomake (kirjautumissivu)", "Backup": "Varmuuskopiointi", "AutomaticSearch": "Automaattihaku", - "BackupRetentionHelpText": "Säilytysjaksoa vanhemmat varmuuskopiot siivotaan automaattisesti.", + "BackupRetentionHelpText": "Säilytysaikaa vanhemmat varmuuskopiot siivotaan automaattisesti.", "BackupsLoadError": "Varmuuskopioinnin lataus epäonnistui", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Mukautetun muodon vähimmäispisteytys, jolla ensisijaisen protokollan viiveen ohitus sallitaan.", "BypassDelayIfHighestQuality": "Ohita, jos korkein laatu", @@ -708,7 +708,7 @@ "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", - "SelectSeries": "Valitse sarja", + "SelectSeries": "Valitse sarjoja", "SelectSeasonModalTitle": "{modalTitle} – Valitse kausi", "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", "TimeLeft": "Aikaa jäljellä", @@ -1032,7 +1032,7 @@ "MonitorNewItems": "Valvo uusia kohteita", "MonitorSpecialEpisodesDescription": "Valvo kaikkia erikoisjaksoja muuttamatta muiden jaksojen tilaa.", "MonitorNoNewSeasons": "Ei uusia kausia", - "OpenSeries": "Kun sarja avataan", + "OpenSeries": "Avaa sarja", "NoChange": "Ei muutosta", "Today": "Tänään", "Titles": "Nimikkeet", @@ -1101,7 +1101,7 @@ "ImportExtraFiles": "Tuo oheistiedostot", "DeleteCustomFormat": "Poista mukautettu muoto", "Repeat": "Toista", - "InteractiveImport": "Manuaalituonti", + "InteractiveImport": "Manuaalinen tuonti", "NotificationsKodiSettingAlwaysUpdate": "Päivitä aina", "NotificationsKodiSettingAlwaysUpdateHelpText": "Määrittää päivitetäänkö kirjasto myös videotoiston aikana.", "Connection": "Yhteys", @@ -1205,7 +1205,7 @@ "Authentication": "Tunnistautuminen", "AutomaticAdd": "Automaattinen lisäys", "AutoAdd": "Automaattilisäys", - "AutoRedownloadFailedHelpText": "Etsi ja pyri lataamaan eri julkaisu automaattisesti.", + "AutoRedownloadFailedHelpText": "Etsi ja pyri lataamaan korvaava julkaisu automaattisesti.", "Clone": "Monista", "CloneCustomFormat": "Monista mukautettu muoto", "ConnectSettings": "Ilmoituspavelun asetukset", @@ -1238,7 +1238,7 @@ "MultiEpisodeInvalidFormat": "Useita jaksoja: virheellinen kaava", "AutoRedownloadFailedFromInteractiveSearch": "Uudelleenlataus manuaalihaun tuloksista epäonnistui", "Blocklist": "Estolista", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi ja pyri lataamaan eri julkaisu automaattisesti vaikka epäonnistunut julkaisu oli kaapattu manuaalihaun tuloksista.", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Etsi ja lataa uusi vastaava julkaisu, kun epäonnistunut lataus on valittu manuaalihaun tuloksista.", "StandardEpisodeFormat": "Tavallisten jaksojen kaava", "SceneNumberNotVerified": "Kohtausnumeroa ei ole vielä vahvistettu", "Scene": "Kohtaus", @@ -1349,7 +1349,7 @@ "IndexerSettingsAnimeCategoriesHelpText": "Pudotusvalikko. Poista anime käytöstä jättämällä tyhjäksi.", "IndexerValidationCloudFlareCaptchaExpired": "CloudFlaren CAPTCHA-tunniste on vanhentunut. Päivitä se.", "KeyboardShortcutsConfirmModal": "Vastaa vahvistuskysymykseen hyväksyvästi", - "ManualImport": "Manuaalituonti", + "ManualImport": "Manuaalinen tuonti", "MediaManagementSettings": "Medianhallinnan asetukset", "Mechanism": "Mekanismi", "Negate": "Kiellä", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index f8768f3da..1c1272c24 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -294,7 +294,7 @@ "Restart": "Redémarrer", "RestartNow": "Redémarrer maintenant", "SaveSettings": "Enregistrer les paramètres", - "ShowMonitoredHelpText": "Afficher l'état de surveillance sous le poster", + "ShowMonitoredHelpText": "Affiche l'état de surveillance sous le poster", "SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre", "Sunday": "Dimanche", "TorrentDelay": "Retard du torrent", @@ -401,7 +401,7 @@ "SearchForMissing": "Rechercher les manquants", "SearchAll": "Tout rechercher", "Series": "Série", - "ShowSearchHelpText": "Afficher le bouton de recherche au survol", + "ShowSearchHelpText": "Affiche le bouton de recherche au survol", "SmartReplace": "Remplacement intelligent", "SmartReplaceHint": "Dash ou Space Dash selon le nom", "Space": "Espace", @@ -435,7 +435,7 @@ "EditDownloadClientImplementation": "Modifier le client de téléchargement - {implementationName}", "External": "Externe", "Monday": "Lundi", - "ShowQualityProfileHelpText": "Afficher le profil de qualité sous l'affiche", + "ShowQualityProfileHelpText": "Affiche le profil de qualité sous l'affiche", "IncludeCustomFormatWhenRenamingHelpText": "Inclure dans le format de renommage {Formats personnalisés}", "SelectDropdown": "Sélectionner...", "InteractiveImportNoFilesFound": "Aucun fichier vidéo n'a été trouvé dans le dossier sélectionné", @@ -728,7 +728,7 @@ "ShowEpisodeInformation": "Afficher les informations sur l'épisode", "ShowEpisodeInformationHelpText": "Afficher le titre et le numéro de l'épisode", "ShowEpisodes": "Afficher les épisodes", - "ShowMonitored": "Afficher le chemin", + "ShowMonitored": "Afficher l'état de surveillance", "ShowNetwork": "Afficher le réseau", "ShowPath": "Afficher le chemin", "ShowPreviousAiring": "Afficher la diffusion précédente", @@ -737,7 +737,7 @@ "ShowSearch": "Afficher la recherche", "ShowSeasonCount": "Afficher le nombre de saisons", "ShowSizeOnDisk": "Afficher la taille sur le disque", - "ShowTitle": "Montrer le titre", + "ShowTitle": "Afficher le titre", "ShowSeriesTitleHelpText": "Afficher le titre de la série sous l'affiche", "ShowUnknownSeriesItems": "Afficher les éléments de série inconnus", "ShowUnknownSeriesItemsHelpText": "Afficher les éléments sans série dans la file d'attente. Cela peut inclure des séries, des films ou tout autre élément supprimé dans la catégorie de {appName}", @@ -2075,7 +2075,7 @@ "DayOfWeekAt": "{day} à {time}", "TomorrowAt": "Demain à {time}", "TodayAt": "Aujourd'hui à {time}", - "ShowTagsHelpText": "Afficher les labels sous l'affiche", + "ShowTagsHelpText": "Affiche les labels sous l'affiche", "ShowTags": "Afficher les labels", "CountVotes": "{votes} votes", "NoBlocklistItems": "Aucun élément de la liste de blocage", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 87cb3a6a0..20eff4938 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -1008,7 +1008,7 @@ "Debug": "Hata ayıklama", "DailyEpisodeTypeFormat": "Tarih ({format})", "DeleteSeriesFolders": "Dizi Klasörlerini Sil", - "Discord": "Uyuşmazlık", + "Discord": "Discord", "DeleteSeriesFoldersHelpText": "Dizi klasörlerini ve tüm içeriklerini silin", "QualitySettings": "Kalite Ayarları", "ReplaceWithSpaceDashSpace": "Space Dash Space ile değiştirin", From 1969e0107f01843c9c3cddd98d85f366f27f403a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 4 Jan 2025 16:59:38 -0800 Subject: [PATCH 728/762] Bump version to 4.0.12 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1083b1a98..f635c61b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.11 + VERSION: 4.0.12 jobs: backend: From 4dcc015fb19ceb57d2e8f4985c5137e765829d1c Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 5 Jan 2025 02:56:49 +0100 Subject: [PATCH 729/762] Fixed: qBittorrent Ratio Limit Check Closes #7527 --- .../QBittorrentTests/QBittorrentFixture.cs | 48 +++++++++++++++++++ .../Clients/QBittorrent/QBittorrent.cs | 4 +- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 4b126835b..593962ead 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -712,6 +712,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeTrue(); } + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state) + { + GivenGlobalSeedLimits(1.0f); + GivenCompletedTorrent(state, ratio: 1.1006066990976857f); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state) + { + GivenGlobalSeedLimits(1.0f); + GivenCompletedTorrent(state, ratio: 0.9999f); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + [TestCase("pausedUP")] [TestCase("stoppedUP")] public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state) @@ -724,6 +748,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeTrue(); } + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state) + { + GivenGlobalSeedLimits(2.0f); + GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [TestCase("pausedUP")] + [TestCase("stoppedUP")] + public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state) + { + GivenGlobalSeedLimits(2.0f); + GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + [TestCase("pausedUP")] [TestCase("stoppedUP")] public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 8bec2d2fd..3df40a123 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -630,14 +630,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { if (torrent.RatioLimit >= 0) { - if (torrent.Ratio >= torrent.RatioLimit) + if (torrent.RatioLimit - torrent.Ratio <= 0.001f) { return true; } } else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) { - if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio) + if (config.MaxRatio - torrent.Ratio <= 0.001f) { return true; } From 035c474f10c257331a5f47e863d24af82537e335 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 5 Jan 2025 02:58:24 +0100 Subject: [PATCH 730/762] Fixed: Listening on all IPv4 Addresses Closes #7526 --- src/NzbDrone.Core/Validation/IpValidation.cs | 6 ------ src/Sonarr.Api.V3/Config/HostConfigController.cs | 1 - 2 files changed, 7 deletions(-) diff --git a/src/NzbDrone.Core/Validation/IpValidation.cs b/src/NzbDrone.Core/Validation/IpValidation.cs index eb5863caa..f4afa1f66 100644 --- a/src/NzbDrone.Core/Validation/IpValidation.cs +++ b/src/NzbDrone.Core/Validation/IpValidation.cs @@ -1,5 +1,4 @@ using FluentValidation; -using FluentValidation.Validators; using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Validation @@ -10,10 +9,5 @@ namespace NzbDrone.Core.Validation { return ruleBuilder.Must(x => x.IsValidIpAddress()).WithMessage("Must contain wildcard (*) or a valid IP Address"); } - - public static IRuleBuilderOptions<T, string> NotListenAllIp4Address<T>(this IRuleBuilder<T, string> ruleBuilder) - { - return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!0\.0\.0\.0)")).WithMessage("Use * instead of 0.0.0.0"); - } } } diff --git a/src/Sonarr.Api.V3/Config/HostConfigController.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs index a108e70c3..a565f1653 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigController.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -33,7 +33,6 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.BindAddress) .ValidIpAddress() - .NotListenAllIp4Address() .When(c => c.BindAddress != "*" && c.BindAddress != "localhost"); SharedValidator.RuleFor(c => c.Port).ValidPort(); From f843107c252a80050e2b9024ccae83279f77fb15 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 10 Jan 2025 21:33:21 +0000 Subject: [PATCH 731/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: CaveMan474 <Caveman_TheLastOne@proton.me> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Lars <lars.erik.heloe@gmail.com> Co-authored-by: Mickaël O <mickael.ouillon@ac-bordeaux.fr> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: RabSsS01 <royermatthieu78@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: mryx007 <mryx@mail.de> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 6 +-- src/NzbDrone.Core/Localization/Core/fi.json | 6 +-- src/NzbDrone.Core/Localization/Core/fr.json | 30 +++++++++++++- .../Localization/Core/nb_NO.json | 39 ++++++++++++++++++- src/NzbDrone.Core/Localization/Core/ro.json | 3 +- src/NzbDrone.Core/Localization/Core/tr.json | 12 +++--- 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 53fb847b7..e44038b60 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -162,7 +162,7 @@ "SelectEpisodesModalTitle": "{modalTitle} – Episode(n) auswählen", "SeasonPassTruncated": "Es werden nur die letzten 25 Staffeln gezeigt. Gehen Sie zu den Details, um alle Staffeln zu sehen", "RestartReloadNote": "Hinweis: {appName} startet während des Wiederherstellungsvorgangs automatisch neu und lädt die Benutzeroberfläche neu.", - "AutoRedownloadFailedHelpText": "Suchen Sie automatisch nach einer anderen Version und versuchen Sie, sie herunterzuladen", + "AutoRedownloadFailedHelpText": "Automatisch nach einem anderen Release suchen und versuchen es herunterzuladen", "AirDate": "Ausstrahlungsdatum", "AgeWhenGrabbed": "Alter (bei Erfassung)", "ApplyTagsHelpTextHowToApplySeries": "So wenden Sie Tags auf die ausgewählte Serie an", @@ -229,7 +229,7 @@ "UiLanguageHelpText": "Sprache, die {appName} für die Benutzeroberfläche verwendet", "UsenetBlackhole": "Usenet Blackhole", "YesCancel": "Ja Abbrechen", - "AutoRedownloadFailedFromInteractiveSearchHelpText": "Suchen Sie automatisch nach einer anderen Version und versuchen Sie, sie herunterzuladen, wenn eine fehlerhafte Version aus der interaktiven Suche ausgewählt wurde", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Falls ein Release aus der interaktiven Suche fehlschlägt, automatisch nach einer alternativen Version suchen und versuchen, sie herunterzuladen", "DownloadClientQbittorrentSettingsSequentialOrder": "Fortlaufende Reihenfolge", "AutoTagging": "Automatisches Tagging", "AddRootFolder": "Stammverzeichnis hinzufügen", @@ -486,7 +486,7 @@ "StartImport": "Import starten", "StartProcessing": "Verarbeitung starten", "Tasks": "Aufgaben", - "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", + "ThemeHelpText": "UI-Design ändern: Der 'Auto'-Modus übernimmt das Betriebssystem-Theme, um automatisch zwischen Light- und Dark-Modus zu wechseln. Inspiriert von Theme.Park", "Theme": "Design", "TestAllLists": "Prüfe alle Listen", "Titles": "Titel", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index feb8e6370..309e74fad 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -252,7 +252,7 @@ "SceneInformation": "Kohtaustiedot", "SelectFolderModalTitle": "{modalTitle} – Valitse kansio", "SelectQuality": "Valitse laatu", - "SeriesFolderFormatHelpText": "Käytetään kun lisätään uusi sarja tai siirretään sarjoja sarjaeditorin avulla.", + "SeriesFolderFormatHelpText": "Käytetään kun lisätään uusia sarjoja tai siirretään vanhoja kun niiden tietoja muokataan.", "SeriesIndexFooterMissingMonitored": "Jaksoja puuttuu (sarjaa valvotaan)", "ShowPreviousAiring": "Näytä edellinen esitys", "ShowBannersHelpText": "Korvaa nimet bannerikuvilla.", @@ -684,7 +684,7 @@ "QuickSearch": "Pikahaku", "QualityProfilesLoadError": "Laatuprofiilien lataus epäonnistui", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} jaksotiedostoa", - "SeriesEditor": "Sarjaeditori", + "SeriesEditor": "Sarjojen muokkaus", "SeriesIndexFooterMissingUnmonitored": "Jaksoja puuttuu (sarjaa ei valvota)", "SeriesIsUnmonitored": "Sarjaa ei valvota", "Sunday": "Sunnuntai", @@ -708,7 +708,7 @@ "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", - "SelectSeries": "Valitse sarjoja", + "SelectSeries": "Sarjojen monivalinta", "SelectSeasonModalTitle": "{modalTitle} – Valitse kausi", "ShowUnknownSeriesItems": "Näytä tuntemattomien sarjojen kohteet", "TimeLeft": "Aikaa jäljellä", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 1c1272c24..c6a5fd2a1 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1047,7 +1047,7 @@ "IncludeCustomFormatWhenRenaming": "Inclure un format personnalisé lors du changement de nom", "InteractiveImportNoSeries": "Les séries doivent être choisies pour chaque fichier sélectionné", "Level": "Niveau", - "LibraryImport": "Importer biblio.", + "LibraryImport": "Importer bibliothèque", "ListExclusionsLoadError": "Impossible de charger les exclusions de liste", "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", @@ -2104,5 +2104,31 @@ "LogSizeLimit": "Limite de taille du journal", "DeleteSelectedImportListExclusionsMessageText": "Êtes-vous sûr de vouloir supprimer les exclusions de la liste d'importation sélectionnée ?", "CustomFormatsSpecificationExceptLanguage": "Excepté Langue", - "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponf si l'autre langue que celle sélectionné est présente" + "CustomFormatsSpecificationExceptLanguageHelpText": "Correspond si l'autre langue que celle sélectionné est présente", + "IndexerSettingsFailDownloadsHelpText": "Lors du traitement des téléchargements terminés, {appName} traitera ces types de fichiers sélectionnés comme des téléchargements ayant échoué.", + "IndexerSettingsFailDownloads": "Échec des téléchargements", + "Completed": "Complété", + "CutoffNotMet": "Seuil non atteint", + "DownloadClientUnavailable": "Client de téléchargement indisponible", + "CountCustomFormatsSelected": "{count} format(s) personnalisé(s) sélectionné(s)", + "Delay": "Retard", + "NotificationsGotifySettingsMetadataLinks": "Liens de métadonnées", + "NotificationsGotifySettingsMetadataLinksHelpText": "Ajouter un lien vers les métadonnées de la série lors de l'envoi de notifications", + "MetadataKometaDeprecated": "Les fichiers Kometa ne seront plus créés, le support sera complètement supprimé dans la v5", + "Premiere": "Première", + "RecentFolders": "Dossiers récents", + "UpdatePath": "Chemin de mise à jour", + "UpdateSeriesPath": "Mettre à jour le chemin de la série", + "NoCustomFormatsFound": "Aucun format personnalisé trouvé", + "EditSelectedCustomFormats": "Modifier les formats personnalisés sélectionnés", + "SkipFreeSpaceCheckHelpText": "À utiliser lorsque {appName} ne parvient pas à détecter l'espace libre de votre dossier racine", + "MetadataPlexSettingsEpisodeMappings": "Mappages des épisodes", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Inclure les mappages d'épisodes pour tous les fichiers dans le fichier .plexmatch", + "FailedToFetchSettings": "Impossible de récupérer les paramètres", + "DeleteSelectedCustomFormats": "Supprimer les format(s) personnalisé(s)", + "DeleteSelectedCustomFormatsMessageText": "Êtes-vous sûr de vouloir supprimer {count} format(s) personnalisé(s) sélectionné(s) ?", + "LastSearched": "Dernière recherche", + "FolderNameTokens": "Jetons de nom de dossier", + "ManageCustomFormats": "Gérer les formats personnalisés", + "Menu": "Menu" } diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index e6bd6ad21..0b2cbcdd8 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -18,5 +18,42 @@ "Actions": "Handlinger", "AddCustomFilter": "Legg til eget filter", "AddConnection": "Legg til tilkobling", - "AddDelayProfile": "Legg til forsinkelsesprofil" + "AddDelayProfile": "Legg til forsinkelsesprofil", + "AddRootFolderError": "Kunne ikke legge til rotmappe", + "AddDelayProfileError": "Kunne ikke legge til ny forsinkelsesprofil, vennligst prøv igjen.", + "AddIndexerError": "Kunne ikke legge til ny indekser, vennligst prøv igjen.", + "AddList": "Legg til liste", + "AddListError": "Kunne ikke legge til ny liste, vennligst prøv igjen.", + "AddRootFolder": "Legg til Rotmappe", + "AddConnectionImplementation": "Legg til tilkobling - {implementationName}", + "AddDownloadClientImplementation": "Ny Nedlastingsklient - {implementationName}", + "AddImportListImplementation": "Legg til importliste - {implementationName}", + "AddIndexerImplementation": "Legg til indekser - {implementationName}", + "AddToDownloadQueue": "Legg til nedlastningskø", + "AddedToDownloadQueue": "Lagt til nedlastningskø", + "AfterManualRefresh": "Etter manuell oppdatering", + "AddCustomFormat": "Nytt Egendefinert format", + "AddCustomFormatError": "Kunne ikke legge til nytt egendefinert format, vennligst prøv på nytt.", + "AddDownloadClient": "Ny Nedlastingsklient", + "AddDownloadClientError": "Kunne ikke legge til ny Nedlastingsklient, vennligst prøv igjen.", + "AddImportList": "Ny Importliste", + "AddIndexer": "Legg til indekser", + "AddNewRestriction": "Legg til ny begrensning", + "AddNotificationError": "Kunne ikke legge til ny varsling, vennligst prøv igjen.", + "AddQualityProfile": "Legg til kvalitetsprofil", + "AddQualityProfileError": "Kunne ikke legge til ny kvalitetsprofil, vennligst prøv igjen.", + "AddReleaseProfile": "Legg til utgivelsesprofil", + "AddNew": "Legg til ny", + "Age": "Alder", + "Agenda": "Agenda", + "AddNewSeries": "Legg til ny serie", + "AddNewSeriesError": "Kunne ikke laste søkeresultat, vennligst prøv igjen.", + "AddNewSeriesHelpText": "Det er enkelt å legge til en ny serie. Bare begynn å taste navnet på serien du vil legge til.", + "AddNewSeriesRootFolderHelpText": "Undermappa \"{folder}\" vil bli automatisk laget", + "AddNewSeriesSearchForMissingEpisodes": "Søk etter manglende episoder", + "AddRemotePathMapping": "Legg til ekstern stimapping", + "AddRemotePathMappingError": "Kunne ikke legge til ny ekstern stimapping, vennligst prøv igjen.", + "AddSeriesWithTitle": "Legg til {title}", + "Added": "Lagt til", + "AddedDate": "Lagt til: {date}" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index ca88a4036..79171d7d9 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -211,5 +211,6 @@ "DownloadClientUnavailable": "Client de descărcare indisponibil", "Clone": "Clonează", "DownloadClientSettingsOlderPriority": "Prioritate mai vechi", - "DownloadClientSettingsRecentPriority": "Prioritate recente" + "DownloadClientSettingsRecentPriority": "Prioritate recente", + "Absolute": "Absolut" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 20eff4938..e7edf7d80 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -615,7 +615,7 @@ "DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?", "Or": "veya", "OverrideGrabModalTitle": "Geçersiz Kıl ve Al - {title}", - "PreferProtocol": "{preferredProtocol}'u tercih edin", + "PreferProtocol": "{preferredProtocol} Tercih Edin", "PreferredProtocol": "Tercih Edilen Protokol", "PublishedDate": "Yayınlanma Tarihi", "RemoveQueueItem": "Kaldır - {sourceTitle}", @@ -835,7 +835,7 @@ "UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz", "Wanted": "Arananlar", "Cutoff": "Kesinti", - "Required": "Gerekli", + "Required": "Zorunlu", "AirsTbaOn": "Daha sonra duyurulacak {networkLabel}'de", "AllFiles": "Tüm dosyalar", "AllSeriesAreHiddenByTheAppliedFilter": "Tüm sonuçlar uygulanan filtre tarafından gizlendi", @@ -864,7 +864,7 @@ "TomorrowAt": "Yarın {time}'da", "NoBlocklistItems": "Engellenenler listesi öğesi yok", "YesterdayAt": "Dün saat {time}'da", - "CustomFormatsSpecificationExceptLanguage": "Dil Dışında", + "CustomFormatsSpecificationExceptLanguage": "Dil Hariç", "CustomFormatsSpecificationExceptLanguageHelpText": "Seçilen dil dışında herhangi bir dil mevcutsa eşleşir", "LastSearched": "Son Aranan", "Enable": "Etkinleştir", @@ -1011,7 +1011,7 @@ "Discord": "Discord", "DeleteSeriesFoldersHelpText": "Dizi klasörlerini ve tüm içeriklerini silin", "QualitySettings": "Kalite Ayarları", - "ReplaceWithSpaceDashSpace": "Space Dash Space ile değiştirin", + "ReplaceWithSpaceDashSpace": "Boşluk, Tire ve Boşluk ile Değiştir", "Continuing": "Devam Ediyor", "CleanLibraryLevel": "Kütüphane Seviyesini Temizle", "ClickToChangeSeason": "Sezonu değiştirmek için tıklayın", @@ -1435,8 +1435,8 @@ "RenameFiles": "Yeniden Adlandır", "Renamed": "Yeniden adlandırıldı", "Replace": "Değiştir", - "ReplaceWithDash": "Dash ile değiştir", - "ReplaceWithSpaceDash": "Space Dash ile değiştirin", + "ReplaceWithDash": "Tire ile değiştir", + "ReplaceWithSpaceDash": "Tire ve Boşluk ile Değiştir", "RequiredHelpText": "Özel formatın uygulanabilmesi için bu {implementationName} koşulunun eşleşmesi gerekir. Aksi takdirde tek bir {implementationName} eşleşmesi yeterlidir.", "RestartRequiredHelpTextWarning": "Etkili olması için yeniden başlatma gerektirir", "RetentionHelpText": "Yalnızca Usenet: Sınırsız saklamaya ayarlamak için sıfıra ayarlayın", From c589c4f85e49d9d30c48e778e9ea08e692544730 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 4 Jan 2025 19:10:06 -0800 Subject: [PATCH 732/762] Fixed: Tooltips for detailed error messages --- .../src/Components/Form/FormInputGroup.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 4ed86e8e6..98c6e586a 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -258,14 +258,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) { {helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null} {errors.map((error, index) => { - return 'message' in error ? ( - <FormInputHelpText - key={index} - text={error.message} - isError={true} - isCheckInput={checkInput} - /> - ) : ( + return 'errorMessage' in error ? ( <FormInputHelpText key={index} text={error.errorMessage} @@ -274,23 +267,30 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) { isError={true} isCheckInput={checkInput} /> + ) : ( + <FormInputHelpText + key={index} + text={error.message} + isError={true} + isCheckInput={checkInput} + /> ); })} {warnings.map((warning, index) => { - return 'message' in warning ? ( + return 'errorMessage' in warning ? ( <FormInputHelpText key={index} - text={warning.message} + text={warning.errorMessage} + link={warning.infoLink} + tooltip={warning.detailedDescription} isWarning={true} isCheckInput={checkInput} /> ) : ( <FormInputHelpText key={index} - text={warning.errorMessage} - link={warning.infoLink} - tooltip={warning.detailedDescription} + text={warning.message} isWarning={true} isCheckInput={checkInput} /> From 3c8268c428688cc703af76b648c9b3385858274f Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 11 Jan 2025 02:05:23 +0100 Subject: [PATCH 733/762] Additional logging for custom format score --- .../DecisionEngine/DownloadDecisionMaker.cs | 2 ++ .../CustomFormatAllowedByProfileSpecification.cs | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 57ef05786..aac270bcf 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -117,6 +117,8 @@ namespace NzbDrone.Core.DecisionEngine remoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(remoteEpisode, remoteEpisode.Release.Size); remoteEpisode.CustomFormatScore = remoteEpisode?.Series?.QualityProfile?.Value.CalculateCustomFormatScore(remoteEpisode.CustomFormats) ?? 0; + _logger.Trace("Custom Format Score of '{0}' [{1}] calculated for '{2}'", remoteEpisode.CustomFormatScore, remoteEpisode.CustomFormats?.ConcatToString(), report.Title); + remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any(); decision = GetDecisionForReport(remoteEpisode, searchCriteria); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs index 425c183d1..9a5023977 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs @@ -1,3 +1,4 @@ +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -6,6 +7,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class CustomFormatAllowedbyProfileSpecification : IDownloadDecisionEngineSpecification { + private readonly Logger _logger; + + public CustomFormatAllowedbyProfileSpecification(Logger logger) + { + _logger = logger; + } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; @@ -19,6 +27,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return DownloadSpecDecision.Reject(DownloadRejectionReason.CustomFormatMinimumScore, "Custom Formats {0} have score {1} below Series profile minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore); } + _logger.Trace("Custom Format Score of {0} [{1}] above Series profile minumum {2}", score, subject.CustomFormats.ConcatToString(), minScore); + return DownloadSpecDecision.Accept(); } } From 1fea0b3d1085aae8380dc84bae35b66b97236803 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:37:06 +0200 Subject: [PATCH 734/762] Remote image links for Discord's manual interaction needed --- src/NzbDrone.Core/Notifications/Discord/Discord.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 11ae1127f..540b8d420 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -554,7 +554,7 @@ namespace NzbDrone.Core.Notifications.Discord { embed.Thumbnail = new DiscordImage { - Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url + Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl }; } @@ -562,7 +562,7 @@ namespace NzbDrone.Core.Notifications.Discord { embed.Image = new DiscordImage { - Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url + Url = series?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.RemoteUrl }; } From 1609f0c9647b89bf55b8c043eeffc8a61653a1e5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 11 Jan 2025 02:05:46 +0100 Subject: [PATCH 735/762] New: Show release source in history grab popup --- .../History/Details/HistoryDetails.tsx | 34 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 3 ++ 2 files changed, 37 insertions(+) diff --git a/frontend/src/Activity/History/Details/HistoryDetails.tsx b/frontend/src/Activity/History/Details/HistoryDetails.tsx index b5116b3d9..ae2ec4a66 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.tsx +++ b/frontend/src/Activity/History/Details/HistoryDetails.tsx @@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) { indexer, releaseGroup, seriesMatchType, + releaseSource, customFormatScore, nzbInfoUrl, downloadClient, @@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) { const downloadClientNameInfo = downloadClientName ?? downloadClient; + let releaseSourceMessage = ''; + + switch (releaseSource) { + case 'Unknown': + releaseSourceMessage = translate('Unknown'); + break; + case 'Rss': + releaseSourceMessage = translate('RSS'); + break; + case 'Search': + releaseSourceMessage = translate('Search'); + break; + case 'UserInvokedSearch': + releaseSourceMessage = translate('UserInvokedSearch'); + break; + case 'InteractiveSearch': + releaseSourceMessage = translate('InteractiveSearch'); + break; + case 'ReleasePush': + releaseSourceMessage = translate('ReleasePush'); + break; + default: + releaseSourceMessage = ''; + } + return ( <DescriptionList> <DescriptionListItem @@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) { /> ) : null} + {releaseSource ? ( + <DescriptionListItem + descriptionClassName={styles.description} + title={translate('ReleaseSource')} + data={releaseSourceMessage} + /> + ) : null} + {nzbInfoUrl ? ( <span> <DescriptionListItemTitle> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a01ae4846..8bf5ba4e5 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1667,12 +1667,14 @@ "ReleaseProfiles": "Release Profiles", "ReleaseProfilesLoadError": "Unable to load Release Profiles", "ReleaseRejected": "Release Rejected", + "ReleasePush": "Release Push", "ReleaseSceneIndicatorAssumingScene": "Assuming Scene numbering.", "ReleaseSceneIndicatorAssumingTvdb": "Assuming TVDB numbering.", "ReleaseSceneIndicatorMappedNotRequested": "Mapped episode wasn't requested in this search.", "ReleaseSceneIndicatorSourceMessage": "{message} releases exist with ambiguous numbering, unable to reliably identify episode.", "ReleaseSceneIndicatorUnknownMessage": "Numbering varies for this episode and release does not match any known mappings.", "ReleaseSceneIndicatorUnknownSeries": "Unknown episode or series.", + "ReleaseSource": "Release Source", "ReleaseTitle": "Release Title", "ReleaseType": "Release Type", "Reload": "Reload", @@ -2118,6 +2120,7 @@ "UsenetDelayTime": "Usenet Delay: {usenetDelay}", "UsenetDisabled": "Usenet Disabled", "Username": "Username", + "UserInvokedSearch": "User Invoked Search", "UtcAirDate": "UTC Air Date", "Version": "Version", "VersionNumber": "Version {version}", From fa0f77659cbd3e9efdae55bbedb30fd8288622a6 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 11 Jan 2025 02:06:05 +0100 Subject: [PATCH 736/762] Additional logging for delay profile decisions Closes #7558 --- .../Specifications/RssSync/DelaySpecification.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 1a9339bb3..cb7cd6cc0 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -41,10 +41,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (delay == 0) { - _logger.Debug("QualityProfile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol); + _logger.Debug("Delay Profile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol); return DownloadSpecDecision.Accept(); } + _logger.Debug("Delay Profile requires a waiting period of {0} minutes for {1}", delay, subject.Release.DownloadProtocol); + var qualityComparer = new QualityModelComparer(qualityProfile); if (isPreferredProtocol) @@ -95,6 +97,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (oldest != null && oldest.Release.AgeMinutes > delay) { + _logger.Debug("Oldest pending release {0} has been delayed for {1}, longer than the set delay of {2}. Release will be accepted", oldest.Release.Title, oldest.Release.AgeMinutes, delay); return DownloadSpecDecision.Accept(); } From ec73a1339632c1c41137ccf2c5bea0a4a7d8de58 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 11 Jan 2025 02:06:19 +0100 Subject: [PATCH 737/762] Update translation widget --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5636cb739..d43366f93 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr -[![Translated](https://translate.servarr.com/widgets/servarr/-/sonarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/) +[![Translated](https://translate.servarr.com/widget/servarr/sonarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/) [![Backers on Open Collective](https://opencollective.com/Sonarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Sonarr/sponsors/badge.svg)](#sponsors) [![Mega Sponsors on Open Collective](https://opencollective.com/Sonarr/megasponsors/badge.svg)](#mega-sponsors) From 7d77500667b70a44d85984b585f58392efd0490d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 8 Jan 2025 18:54:25 -0800 Subject: [PATCH 738/762] Fixed: Series being unmonitored when still in Import List Closes #7555 --- .../ImportListItemServiceFixture.cs | 32 ++--- .../ImportListSyncServiceFixture.cs | 118 ++++++++++++++++-- .../Migration/217_add_mal_and_anilist_ids.cs | 15 +++ .../ImportListItemRepository.cs | 22 +--- .../ImportListItems/ImportListItemService.cs | 34 +++-- .../ImportLists/ImportListSyncService.cs | 10 +- .../SkyHook/Resource/ShowResource.cs | 3 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 2 + src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 2 + src/NzbDrone.Core/Tv/Series.cs | 4 + 10 files changed, 174 insertions(+), 68 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs index ff6ac9afa..c0fcb9dcc 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Test.ImportListTests { private void GivenExisting(List<ImportListItemInfo> existing) { - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Setup(v => v.GetAllForLists(It.IsAny<List<int>>())) .Returns(existing); } @@ -58,13 +58,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == newItem.TvdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == updatedItem.TvdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId != newItem.TvdbId && s[0].TvdbId != updatedItem.TvdbId)), Times.Once()); } @@ -107,13 +107,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId == newItem.ImdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId == updatedItem.ImdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].ImdbId != newItem.ImdbId && s[0].ImdbId != updatedItem.ImdbId)), Times.Once()); } @@ -156,13 +156,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId == newItem.TmdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId == updatedItem.TmdbId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TmdbId != newItem.TmdbId && s[0].TmdbId != updatedItem.TmdbId)), Times.Once()); } @@ -205,13 +205,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId == newItem.MalId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId == updatedItem.MalId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].MalId != newItem.MalId && s[0].MalId != updatedItem.MalId)), Times.Once()); } @@ -254,13 +254,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId == newItem.AniListId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId == updatedItem.AniListId)), Times.Once()); - Mocker.GetMock<IImportListItemInfoRepository>() + Mocker.GetMock<IImportListItemRepository>() .Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].AniListId != newItem.AniListId && s[0].AniListId != updatedItem.AniListId)), Times.Once()); } } diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 26e8cab7e..d0491fb74 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -42,24 +42,45 @@ namespace NzbDrone.Core.Test.ImportListTests .TheFirst(1) .With(s => s.TvdbId = 6) .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalIds = new HashSet<int> { 6 }) + .With(s => s.AniListIds = new HashSet<int> { 6 }) + .With(s => s.Monitored = true) .TheNext(1) .With(s => s.TvdbId = 7) .With(s => s.ImdbId = "7") + .With(s => s.TmdbId = 7) + .With(s => s.MalIds = new HashSet<int> { 7 }) + .With(s => s.AniListIds = new HashSet<int> { 7 }) + .With(s => s.Monitored = true) .TheNext(1) .With(s => s.TvdbId = 8) .With(s => s.ImdbId = "8") + .With(s => s.TmdbId = 8) + .With(s => s.MalIds = new HashSet<int> { 8 }) + .With(s => s.AniListIds = new HashSet<int> { 8 }) + .With(s => s.Monitored = true) .Build().ToList(); _list2Series = Builder<ImportListItemInfo>.CreateListOfSize(3) .TheFirst(1) .With(s => s.TvdbId = 6) .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 6) + .With(s => s.AniListId = 6) .TheNext(1) .With(s => s.TvdbId = 7) .With(s => s.ImdbId = "7") + .With(s => s.TmdbId = 7) + .With(s => s.MalId = 7) + .With(s => s.AniListId = 7) .TheNext(1) .With(s => s.TvdbId = 8) .With(s => s.ImdbId = "8") + .With(s => s.TmdbId = 8) + .With(s => s.MalId = 8) + .With(s => s.AniListId = 8) .Build().ToList(); _importListFetch = new ImportListFetchResult(_list1Series, false); @@ -110,6 +131,10 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock<IImportListExclusionService>() .Setup(v => v.All()) .Returns(new List<ImportListExclusion>()); + + Mocker.GetMock<IImportListItemService>() + .Setup(s => s.All()) + .Returns(new List<ImportListItemInfo>()); } private void WithTvdbId() @@ -153,6 +178,19 @@ namespace NzbDrone.Core.Test.ImportListTests }); } + private List<ImportListItemInfo> WithImportListItems(int count) + { + var importListItems = Builder<ImportListItemInfo>.CreateListOfSize(count) + .Build() + .ToList(); + + Mocker.GetMock<IImportListItemService>() + .Setup(s => s.All()) + .Returns(importListItems); + + return importListItems; + } + private void WithMonitorType(MonitorTypes monitor) { _importLists.ForEach(li => (li.Definition as ImportListDefinition).ShouldMonitor = monitor); @@ -285,16 +323,18 @@ namespace NzbDrone.Core.Test.ImportListTests { WithList(1, true); WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); _importListFetch.Series.ForEach(m => m.ImportListId = 1); - Mocker.GetMock<IImportListItemService>() - .Setup(v => v.Exists(6, It.IsAny<string>())) - .Returns(true); + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].TvdbId = _existingSeries[i].TvdbId; + } Subject.Execute(_commandAll); Mocker.GetMock<ISeriesService>() - .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); } [Test] @@ -302,18 +342,75 @@ namespace NzbDrone.Core.Test.ImportListTests { WithList(1, true); WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); _importListFetch.Series.ForEach(m => m.ImportListId = 1); - var x = _importLists; - - Mocker.GetMock<IImportListItemService>() - .Setup(v => v.Exists(It.IsAny<int>(), "6")) - .Returns(true); + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].ImdbId = _existingSeries[i].ImdbId; + } Subject.Execute(_commandAll); Mocker.GetMock<ISeriesService>() - .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_tmdb_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].TmdbId = _existingSeries[i].TmdbId; + } + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_malid_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].MalId = _existingSeries[i].MalIds.First(); + } + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_anilistid_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].AniListId = _existingSeries[i].AniListIds.First(); + } + + Subject.Execute(_commandAll); + + Mocker.GetMock<ISeriesService>() + .Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); } [Test] @@ -345,6 +442,7 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock<ISeriesService>() .Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), It.IsAny<bool>()), Times.Never()); + Mocker.GetMock<ISeriesService>() .Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never()); } diff --git a/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs b/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs new file mode 100644 index 000000000..55a2c3b46 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(217)] + public class add_mal_and_anilist_ids : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series").AddColumn("MalIds").AsString().WithDefaultValue("[]"); + Alter.Table("Series").AddColumn("AniListIds").AsString().WithDefaultValue("[]"); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs index abe546026..91a1c3a56 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs @@ -1,18 +1,16 @@ using System.Collections.Generic; -using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.ImportLists.ImportListItems { - public interface IImportListItemInfoRepository : IBasicRepository<ImportListItemInfo> + public interface IImportListItemRepository : IBasicRepository<ImportListItemInfo> { List<ImportListItemInfo> GetAllForLists(List<int> listIds); - bool Exists(int tvdbId, string imdbId); } - public class ImportListItemRepository : BasicRepository<ImportListItemInfo>, IImportListItemInfoRepository + public class ImportListItemRepository : BasicRepository<ImportListItemInfo>, IImportListItemRepository { public ImportListItemRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) @@ -23,21 +21,5 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { return Query(x => listIds.Contains(x.ImportListId)); } - - public bool Exists(int tvdbId, string imdbId) - { - List<ImportListItemInfo> items; - - if (string.IsNullOrWhiteSpace(imdbId)) - { - items = Query(x => x.TvdbId == tvdbId); - } - else - { - items = Query(x => x.TvdbId == tvdbId || x.ImdbId == imdbId); - } - - return items.Any(); - } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs index 793d6c548..f7faf607b 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; @@ -10,21 +9,18 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { public interface IImportListItemService { + List<ImportListItemInfo> All(); List<ImportListItemInfo> GetAllForLists(List<int> listIds); int SyncSeriesForList(List<ImportListItemInfo> listSeries, int listId); - bool Exists(int tvdbId, string imdbId); } public class ImportListItemService : IImportListItemService, IHandleAsync<ProviderDeletedEvent<IImportList>> { - private readonly IImportListItemInfoRepository _importListSeriesRepository; - private readonly Logger _logger; + private readonly IImportListItemRepository _importListItemRepository; - public ImportListItemService(IImportListItemInfoRepository importListSeriesRepository, - Logger logger) + public ImportListItemService(IImportListItemRepository importListItemRepository) { - _importListSeriesRepository = importListSeriesRepository; - _logger = logger; + _importListItemRepository = importListItemRepository; } public int SyncSeriesForList(List<ImportListItemInfo> listSeries, int listId) @@ -58,27 +54,27 @@ namespace NzbDrone.Core.ImportLists.ImportListItems existingItem.ReleaseDate = item.ReleaseDate; }); - _importListSeriesRepository.InsertMany(toAdd); - _importListSeriesRepository.UpdateMany(toUpdate); - _importListSeriesRepository.DeleteMany(existingListSeries); + _importListItemRepository.InsertMany(toAdd); + _importListItemRepository.UpdateMany(toUpdate); + _importListItemRepository.DeleteMany(existingListSeries); return existingListSeries.Count; } + public List<ImportListItemInfo> All() + { + return _importListItemRepository.All().ToList(); + } + public List<ImportListItemInfo> GetAllForLists(List<int> listIds) { - return _importListSeriesRepository.GetAllForLists(listIds).ToList(); + return _importListItemRepository.GetAllForLists(listIds).ToList(); } public void HandleAsync(ProviderDeletedEvent<IImportList> message) { - var seriesOnList = _importListSeriesRepository.GetAllForLists(new List<int> { message.ProviderId }); - _importListSeriesRepository.DeleteMany(seriesOnList); - } - - public bool Exists(int tvdbId, string imdbId) - { - return _importListSeriesRepository.Exists(tvdbId, imdbId); + var seriesOnList = _importListItemRepository.GetAllForLists(new List<int> { message.ProviderId }); + _importListItemRepository.DeleteMany(seriesOnList); } private ImportListItemInfo FindItem(List<ImportListItemInfo> existingItems, ImportListItemInfo item) diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 291ecba27..489b4a7bc 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -299,12 +299,18 @@ namespace NzbDrone.Core.ImportLists var seriesToUpdate = new List<Series>(); var seriesInLibrary = _seriesService.GetAllSeries(); + var allListItems = _importListItemService.All(); foreach (var series in seriesInLibrary) { - var seriesExists = _importListItemService.Exists(series.TvdbId, series.ImdbId); + var seriesExists = allListItems.Where(l => + l.TvdbId == series.TvdbId || + l.ImdbId == series.ImdbId || + l.TmdbId == series.TmdbId || + series.MalIds.Contains(l.MalId) || + series.AniListIds.Contains(l.AniListId)).ToList(); - if (!seriesExists) + if (!seriesExists.Any()) { switch (_configService.ListSyncLevel) { diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs index 5cc7fce36..b7a9d7a42 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -24,7 +24,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public int? TvRageId { get; set; } public int? TvMazeId { get; set; } public int? TmdbId { get; set; } - + public HashSet<int> MalIds { get; set; } + public HashSet<int> AniListIds { get; set; } public string Status { get; set; } public int? Runtime { get; set; } public TimeOfDayResource TimeOfDay { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index f600057a7..c97c524dc 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -194,6 +194,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } series.ImdbId = show.ImdbId; + series.MalIds = show.MalIds; + series.AniListIds = show.AniListIds; series.Title = show.Title; series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title); series.SortTitle = SeriesTitleNormalizer.Normalize(show.Title, show.TvdbId); diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index b9dd08482..b5af90527 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -94,6 +94,8 @@ namespace NzbDrone.Core.Tv series.TvMazeId = seriesInfo.TvMazeId; series.TmdbId = seriesInfo.TmdbId; series.ImdbId = seriesInfo.ImdbId; + series.MalIds = seriesInfo.MalIds; + series.AniListIds = seriesInfo.AniListIds; series.AirTime = seriesInfo.AirTime; series.Overview = seriesInfo.Overview; series.OriginalLanguage = seriesInfo.OriginalLanguage; diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 9819773a0..2464742b7 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -17,6 +17,8 @@ namespace NzbDrone.Core.Tv Seasons = new List<Season>(); Tags = new HashSet<int>(); OriginalLanguage = Language.English; + MalIds = new HashSet<int>(); + AniListIds = new HashSet<int>(); } public int TvdbId { get; set; } @@ -24,6 +26,8 @@ namespace NzbDrone.Core.Tv public int TvMazeId { get; set; } public string ImdbId { get; set; } public int TmdbId { get; set; } + public HashSet<int> MalIds { get; set; } + public HashSet<int> AniListIds { get; set; } public string Title { get; set; } public string CleanTitle { get; set; } public string SortTitle { get; set; } From acebe87dbabe0def975bd43b9e4a40287f50fb33 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 8 Jan 2025 19:34:46 -0800 Subject: [PATCH 739/762] New: Parse releases with year and season number in brackets Closes #7559 --- src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs | 3 ++- .../ParserTests/SingleEpisodeParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index ac2ad5ce6..f7838bebf 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -35,7 +35,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)] [TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)] [TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)] - [TestCase("[Zoombie] Zom 100: Bucket List of the Dead S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Zom 100: Bucket List of the Dead", 1)] + [TestCase("[Zoombie] Series 100: Bucket List S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Series 100: Bucket List", 1)] + [TestCase("Seriesless (2016/S01/WEB-DL/1080p/AC3 5.1/DUAL/SUB)", "Seriesless (2016)", 1)] public void should_parse_full_season_release(string postTitle, string title, int season) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 42d4782d8..14d0afef1 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -175,6 +175,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series - Temporada 1 - [HDTV 1080p][Cap.101](wolfmax4k.com)", "Series", 1, 1)] [TestCase("Series [HDTV 1080p][Cap.101](wolfmax4k.com)", "Series", 1, 1)] [TestCase("Series [HDTV 1080p][Cap. 101](wolfmax4k.com).mkv", "Series", 1, 1)] + [TestCase("Amazing Title (2024/S01E07/DSNP/WEB-DL/1080p/ESP/EAC3 5.1/ING/EAC3 5.1 Atmos/SUBS) SPWEB", "Amazing Title (2024)", 1, 7)] + [TestCase("Mini Title (Miniserie) (2024/S01E07/DSNP/WEB-DL/1080p/ESP/EAC3 5.1/ING/EAC3 5.1 Atmos/SUBS) SPWEB", "Mini Title (2024)", 1, 7)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 2b603052e..8cf934131 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -51,6 +51,9 @@ namespace NzbDrone.Core.Parser // Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Spanish releases with information in brackets + new RegexReplace(@"^(?<title>.+?(?=[ ._-]\()).+?\((?<year>\d{4})\/(?<info>S[^\/]+)", "${title} (${year}) - ${info} ", RegexOptions.Compiled), }; private static readonly Regex[] ReportTitleRegex = new[] From 8f5d628c552309cfa205c67374428cde079ef006 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 17 Jan 2025 20:33:23 +0000 Subject: [PATCH 740/762] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Florian Savouré <florian.savoure@gmail.com> Co-authored-by: Georgi Panov <darkfella91@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: keysuck <joshkkim@gmail.com> Co-authored-by: warkurre86 <tom.novo.86@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/bg/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ko/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/bg.json | 91 ++++++- src/NzbDrone.Core/Localization/Core/cs.json | 200 +++++++++++++++- src/NzbDrone.Core/Localization/Core/de.json | 13 +- src/NzbDrone.Core/Localization/Core/fi.json | 71 +++--- src/NzbDrone.Core/Localization/Core/ko.json | 204 +++++++++++++++- .../Localization/Core/pt_BR.json | 223 +++++++++--------- src/NzbDrone.Core/Localization/Core/tr.json | 4 +- 7 files changed, 648 insertions(+), 158 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index 0967ef424..278700c60 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -1 +1,90 @@ -{} +{ + "AddedDate": "Добавен: {date}", + "AddANewPath": "Добави нов път", + "AddCustomFilter": "Добави персонализиран филтър", + "AddDownloadClientImplementation": "Добави клиент за изтегляне - {implementationName}", + "AddExclusion": "Добави изключение", + "AddImportList": "Добави списък за импортиране", + "AddIndexer": "Добави индексатор", + "AirsTomorrowOn": "Утре от {time} по {networkLabel}", + "AddedToDownloadQueue": "Добавен към опашката за изтегляне", + "AfterManualRefresh": "След ръчно опресняване", + "AirsDateAtTimeOn": "{date} в {time} по {networkLabel}", + "AirsTbaOn": "TBA по {networkLabel}", + "AirsTimeOn": "{time} по {networkLabel}", + "AllFiles": "Всички файлове", + "AlternateTitles": "Алтернативни заглавия", + "Any": "Всякакви", + "AddConditionImplementation": "Добави условие - {implementationName}", + "AddConnectionImplementation": "Добави връзка - {implementationName}", + "AddListExclusion": "Добави изключение от списъка", + "AddImportListExclusion": "Добави изключение от списъка за импортиране", + "AddImportListExclusionError": "Не може да се добави ново изключение от списъка за импортиране, моля, опитайте отново.", + "AddListExclusionSeriesHelpText": "Предотврати добавянето на сериили в {appName} посредством списъци", + "AnimeEpisodeTypeFormat": "Абсолютен номер на епизода ({format})", + "AddRootFolderError": "Не може да се добави основна папка, моля, опитайте отново", + "AnimeEpisodeTypeDescription": "Епизоди, издадени с абсолютен номер на епизод", + "AnalyseVideoFilesHelpText": "Извлича видео информация, като резолюция, продължителност и информация за кодека от файловете. Това изисква {appName} да прочете части от файла, което може да предизвика висока дискова или мрежова активност по време на сканирането.", + "AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките към сървърите на {appName}. Това включва информация за вашия браузър, кои страници на уеб интерфейса на {appName} използвате, докладваните грешки, както и версията на операционната система и изпълнителната среда. Ще използваме тази информация, за да приоритизираме нови функции и поправки на бъгове.", + "Anime": "Аниме", + "AddIndexerError": "Не може да се добави нов индексатор, моля, опитайте отново.", + "AddIndexerImplementation": "Добави индексатор - {implementationName}", + "AddDelayProfileError": "Не може да се добави нов профил за забавяне, моля, опитайте отново.", + "AddNotificationError": "Не може да се добави ново известие, моля, опитайте отново.", + "AddImportListImplementation": "Добави списък за импортиране - {implementationName}", + "AddList": "Добави списък", + "AddNewSeriesSearchForMissingEpisodes": "Започни търсене на липсващи епизоди", + "AddRemotePathMapping": "Добави мапиране към отдалечен път", + "AddRemotePathMappingError": "Не може да се добави ново мапиране към отдалечен път, моля, опитайте отново.", + "AddToDownloadQueue": "Добави към опашката за изтегляне", + "AlreadyInYourLibrary": "Вече е във вашата библиотека", + "AnEpisodeIsDownloading": "Изтегля се епизод", + "AnimeEpisodeFormat": "Аниме Епизод Формат", + "ApiKey": "API ключ", + "Added": "Добавен", + "ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте ключа си за API, за да бъде с дължина най-малко {length} знака. Може да направите това чрез настройките или конфигурационния файл", + "AddConditionError": "Не може да се добави новo условие, моля, опитайте отново.", + "AddAutoTagError": "Не може да се добави нов автоматичен таг, моля, опитайте отново.", + "AddConnection": "Добави връзка", + "AddCustomFormat": "Добави персонализиран формат", + "AddCustomFormatError": "Не може да се добави нов персонализиран формат, моля, опитайте отново.", + "AddDelayProfile": "Добави профил за забавяне", + "AddDownloadClient": "Добави клиент за изтегляне", + "AddDownloadClientError": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.", + "AddListExclusionError": "Не може да се добави ново изключение от списъка, моля, опитайте отново.", + "AddNewRestriction": "Добави новo ограничение", + "AddListError": "Не може да се добави нов списък, моля, опитайте отново.", + "AddQualityProfile": "Добави профил за качество", + "AddQualityProfileError": "Не може да се добави нов профил за качество, моля, опитайте отново.", + "AddReleaseProfile": "Добави профил за издания", + "Always": "Винаги", + "AnalyseVideoFiles": "Анализирай видео файлове", + "Analytics": "Анализ", + "AgeWhenGrabbed": "Възраст (при грабване)", + "AddAutoTag": "Добави автоматичен етикет", + "AddCondition": "Добави условие", + "AirDate": "Ефирна дата", + "AllTitles": "Всички заглавия", + "AddRootFolder": "Добави основна папка", + "Add": "Добави", + "AddingTag": "Добавяне на етикет", + "Age": "Възраст", + "All": "Всички", + "Activity": "Активност", + "AddNew": "Добави нов", + "Actions": "Действия", + "About": "Относно", + "Agenda": "Агенда", + "AddNewSeries": "Добави нов сериал", + "AddNewSeriesError": "Неуспешно зареждане на резултатите от търсенето, моля, опитайте отново.", + "AddNewSeriesHelpText": "Добавянето на нови сериали е лесно, започнете, като напишете името на сериала, който искате да добавите.", + "AddNewSeriesRootFolderHelpText": "'{folder}' подпапка ще бъде създадена автоматично", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Започни търсене на епизоди, които не са достигнали максималното качество за надграждане", + "AddSeriesWithTitle": "Добави {title}", + "Absolute": "Абсолютен", + "AllSeriesAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", + "AllSeriesInRootFolderHaveBeenImported": "Всички сериали в {path} са импортирани", + "AbsoluteEpisodeNumber": "Абсолютен епизоден номер", + "AllResultsAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", + "AppDataDirectory": "Директория на AppData" +} diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index ee77064e8..66f53aeac 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -37,7 +37,7 @@ "Connect": "Připojit", "ConnectSettingsSummary": "Oznámení, připojení k mediálním serverům/přehrávačům a vlastní skripty", "Connections": "Připojení", - "AbsoluteEpisodeNumber": "Úplné číslo dílu", + "AbsoluteEpisodeNumber": "Celkový počet epizod", "AddAutoTagError": "Nepodařilo se přidat novou automatickou značku, zkuste to prosím znovu.", "AddConditionError": "Nepodařilo se přidat novou podmínku, prosím, zkuste to znovu.", "AddConnection": "Přidat spojení", @@ -76,14 +76,14 @@ "CollectionsLoadError": "Nelze načíst sbírky", "CompletedDownloadHandling": "Zpracování stahování bylo dokončeno", "Condition": "Stav", - "UpdateMechanismHelpText": "Použijte vestavěný nástroj {appName}u pro aktualizaci nebo skript", + "UpdateMechanismHelpText": "Použij vestavěný nástroj {appName}u pro aktualizaci nebo skript", "AddCondition": "Přidat podmínku", "AutoTagging": "Automatické označování", "AddAutoTag": "Přidat automatickou značku", "AutoTaggingRequiredHelpText": "Tato podmínka {implementationName} musí odpovídat, aby se pravidlo automatického označování použilo. V opačném případě postačí jediná shoda s {implementationName}.", "AirDate": "Datum vysílání", "AllTitles": "Všechny názvy", - "AbsoluteEpisodeNumbers": "Úplné číslo dílu(ů)", + "AbsoluteEpisodeNumbers": "Celkový počet epizod", "AddRootFolder": "Přidat kořenový adresář", "Backups": "Zálohy", "Clear": "Vymazat", @@ -127,7 +127,7 @@ "Actions": "Akce", "AptUpdater": "K instalaci aktualizace používat apt", "BackupNow": "Zálohovat nyní", - "AppDataDirectory": "Adresář AppData", + "AppDataDirectory": "AppData Adresář", "ApplyTagsHelpTextHowToApplySeries": "Jak použít značky na vybrané seriály", "BackupFolderHelpText": "Relativní cesty budou v adresáři AppData {appName}u", "BlocklistReleases": "Blocklist pro vydání", @@ -138,7 +138,7 @@ "AddNewSeriesError": "Výsledky vyhledávání se nepodařilo načíst, zkuste to prosím znovu.", "AddNewSeriesHelpText": "Přidání nového seriálu je snadné, stačí začít psát název seriálu, který chcete přidat.", "AddNewSeriesRootFolderHelpText": "'{folder}' posdložka bude vytvořena automaticky", - "AddNewSeriesSearchForCutoffUnmetEpisodes": "Zahájit hledání vynechaných epizod", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Zahájit hledání Neodpovídajících vynechaných epizod", "AddNewSeriesSearchForMissingEpisodes": "Zahájit hledání chybějících epizod", "AddReleaseProfile": "Přidat profil vydání", "AddRemotePathMapping": "Přidat mapování vzdálených cest", @@ -271,7 +271,7 @@ "DailyEpisodeTypeFormat": "Datum ({format})", "Default": "Výchozí", "IndexerDownloadClientHelpText": "Zvolte, který klient pro stahování bude použit pro zachytávání z toho indexeru", - "DeletedReasonManual": "Soubor byl smazán pomocí UI", + "DeletedReasonManual": "Soubor byl odstraněn pomocí {appName}, a to buď ručně, nebo jiným nástrojem prostřednictvím rozhraní API", "DeletedReasonUpgrade": "Soubor byl odstraněn pro import lepší verze", "EditConditionImplementation": "Upravit sbírku - {implementationName}", "ClearBlocklist": "Vyčistit blocklist", @@ -321,7 +321,7 @@ "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}.", "ConnectionSettingsUrlBaseHelpText": "Přidá předponu do {connectionName} url, jako např. {url}", "CustomFormatsSpecificationRegularExpressionHelpText": "Vlastní formát RegEx nerozlišuje velká a malá písmena", - "CustomFormatsSpecificationFlag": "Vlajka", + "CustomFormatsSpecificationFlag": "Značka", "BlackholeFolderHelpText": "Složka, do které {appName} uloží soubor {extension}", "BlackholeWatchFolder": "Složka sledování", "Category": "Kategorie", @@ -331,5 +331,189 @@ "BlocklistMultipleOnlyHint": "Blokovat a nehledat náhradu", "CustomFormatsSettingsTriggerInfo": "Vlastní formát se použije na vydání nebo soubor, pokud odpovídá alespoň jednomu z různých typů zvolených podmínek.", "ChangeCategory": "Změnit kategorii", - "CustomFilter": "Vlastní filtr" + "CustomFilter": "Vlastní filtr", + "ClickToChangeIndexerFlags": "Kliknutím změníte značky indexeru", + "QualityProfile": "Profil Kvality", + "ContinuingSeriesDescription": "Očekává se více dílů/další sezóna", + "DeleteReleaseProfile": "Smazat profil vydání", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Přidá prefix do url adresy json deluge, viz {url}", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Nejprve první a poslední", + "DownloadClientRTorrentSettingsAddStopped": "Přidat zastavené", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Cesta ke koncovému bodu XMLRPC, viz {url}. Při použití ruTorrentu je to obvykle RPC2 nebo [cesta k ruTorrentu]{url2}.", + "AddListExclusion": "Přidej Seznam Výjimek", + "DeleteSelectedSeries": "Smazat vybrané seriály", + "AllSeriesAreHiddenByTheAppliedFilter": "Všechny výsledky jsou skryté použitým filtrem", + "DeleteSeriesFoldersHelpText": "Smazat složky seriálu a všechno v nich", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Tato volba vyžaduje NzbGet verze alespoň 16.0", + "Priority": "Přednost", + "ProxyBadRequestHealthCheckMessage": "Nepodařilo se otestovat proxy. Kód Stavu: {statusCode}", + "DownloadClientFreeboxSettingsPortHelpText": "Port použitý pro přístup k rozhraní Freeboxu, výchozí hodnota je ‚{port}‘", + "CountSeriesSelected": "{count} vybrané seriály", + "CleanLibraryLevel": "Vyčistit Úroveň Knihovny", + "DeleteSeriesFolders": "Smazat složky seriálu", + "DoNotPrefer": "Neupřednostňovat", + "Destination": "Cesta", + "AddListExclusionSeriesHelpText": "„Zamezit přidávání Seriálu do {appName} prostřednictvím seznamů“", + "CountCustomFormatsSelected": "{count} vybraný vlastní formát(y)", + "DeleteQualityProfile": "Smazat profil kvality", + "DeleteSpecification": "Smaž specifikace", + "MappedNetworkDrivesWindowsService": "Mapované síťové jednotky nejsou k dispozici, když běží jako služba Windows. Další informace najdete v [FAQ]({url}).", + "DeletedSeriesDescription": "Seriál byl smazán z TheTVDB", + "RecycleBinUnableToWriteHealthCheckMessage": "Nelze zapisovat do nakonfigurované složky koše: {path}. Ujistěte se, že tato cesta existuje a že do ní může zapisovat uživatel se spuštěnou {appName}", + "DeleteSelectedImportListExclusionsMessageText": "Opravdu smazat vybraný importovaný seznam vyjímek?", + "DoNotUpgradeAutomatically": "Neupgradovat automaticky", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Počáteční stav torrentů přidaných do qBittorrentu. Pamatujte, že vynucené torrenty nedodržují omezení týkající se seedů", + "DeleteSelectedCustomFormats": "Smazat vlastní formát(y)", + "ClickToChangeReleaseType": "Kliknutím změníte typ verze", + "CollapseAll": "Sbal Všechno", + "CutoffUnmetNoItems": "Žádné neodpovídající nesplněné položky", + "CutoffUnmetLoadError": "Chybné načítání nesplněných položek", + "AddDelayProfileError": "Nelze přidat nový profil zpoždění, zkuste to prosím znovu.", + "AddedDate": "Přidáno: {date}", + "AlternateTitles": "Střídej tituly", + "BlocklistAndSearchHint": "Začne hledat náhradu po blokaci", + "BlocklistAndSearchMultipleHint": "Začne vyhledávat náhrady po blokaci", + "BlocklistFilterHasNoItems": "Vybraný filtr blokování neobsahuje žádné položky", + "BlocklistOnly": "Pouze seznam blokování", + "BlocklistOnlyHint": "Přidat do seznamu blokování bez náhrady", + "DeleteRemotePathMapping": "Smazat externí cestu k souboru", + "AutoTaggingSpecificationTag": "Značka", + "ChangeCategoryHint": "Změní stahování do kategorie „Post-Import“ z aplikace Download Client", + "ChangeCategoryMultipleHint": "Změní stahování do kategorie „Post-Import“ z aplikace Download Client", + "DeleteSelected": "Smazat vybrané", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} nemohl(a) přidat etiketu k {clientName}.", + "DayOfWeekAt": "{day} v {time}", + "CountVotes": "{votes} hlasy", + "CustomColonReplacementFormatHint": "Platný znak souborového systému, například dvojtečka (písmeno)", + "ProxyFailedToTestHealthCheckMessage": "Nepodařilo se otestovat proxy: {url}", + "Completed": "Hotovo", + "DockerUpdater": "Aby jsi získal aktualizaci proveď update docker kontejneru", + "CustomFormatsSpecificationExceptLanguage": "Vyjma jazyka", + "CustomFormatsSpecificationExceptLanguageHelpText": "Odpovídá, pokud je přítomen jiný jazyk než vybraný", + "CustomFormatsSpecificationMaximumSize": "Maximální velikost", + "CustomFormatsSpecificationReleaseGroup": "Vydávající Skupina", + "CutoffNotMet": "Mezní hodnota není splněna", + "DeleteEpisodeFile": "Smazat Soubor Epizody", + "DeleteEpisodeFileMessage": "Opravdu chceš smazat '{path}'?", + "DeleteEpisodeFromDisk": "Smazat Epizodu z disku", + "DeleteIndexer": "Smazat Indexer", + "DeleteRemotePathMappingMessageText": "Opravdu chceš smazat tohle externí cestu k souboru?", + "Directory": "Adresář", + "DoNotBlocklist": "Nepřidávat do Seznamu blokování", + "DoneEditingGroups": "Úpravy skupin dokončeny", + "DeleteSeriesModalHeader": "Smazat - {title}", + "DeleteSelectedCustomFormatsMessageText": "Opravdu odstranit {count} vybraný vlastní formát(y)?", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "Nedostupný klient pro stahování", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nelze komunikovat s {downloadClientName}. {errorMessage}", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikace získaný při vytváření přístupu k Freebox API (tj. ‚app_token‘)", + "DownloadClientFreeboxSettingsAppToken": "Token aplikace", + "Enable": "Povolit", + "Episode": "Epizoda", + "DestinationPath": "Cesta Destinace", + "EnableAutomaticSearchHelpText": "Použije se při automatickém vyhledávání prostřednictvím uživatelského rozhraní nebo pomocí {appName}", + "CustomColonReplacement": "Vlastní Náhrada znaku dvojtečky", + "CustomColonReplacementFormatHelpText": "Znaky které nahradí dvojtečky", + "DeleteEpisodesFiles": "Smazat{episodeFileCount} Soubory Epizody", + "DeleteEpisodesFilesHelpText": "Smazat soubory epizody a složku seriálu", + "DeleteImportListExclusionMessageText": "Opravdu chceš smazat tento import Seznamu Vyjímek?", + "DeleteSelectedEpisodeFiles": "Smazat soubory vybrané epizody", + "DeleteSelectedEpisodeFilesHelpText": "Opravdu smazat soubory vybrané epizody?", + "DeleteSeriesFolderConfirmation": "Složka seriálu `{path}` a veškerý její obsah bude smazán.", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} celkově souborů epizody {size}", + "Details": "Detaily", + "DetailedProgressBar": "Podrobný ukazatel průběhu", + "DeleteSeriesFolderHelpText": "Smazat složku seriálu a její obsah", + "DoNotBlocklistHint": "Odstraň bez přidání do seznamu blokování", + "Donate": "Daruj", + "DownloadClientAriaSettingsDirectoryHelpText": "Volitelné umístění pro stahování, pokud chcete použít výchozí umístění Aria2, ponechte prázdné", + "DownloadClientDelugeValidationLabelPluginFailure": "Konfigurace etikety selhala", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Přidá vlastnosti médií jako značky. Nápovědy jsou příklady.", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Definuj základní adresu URL rozhraní Freebox API s verzí rozhraní API, např. ‚{url}‘, výchozí hodnota je ‚{defaultApiUrl}‘", + "DownloadClientFreeboxSettingsAppId": "ID aplikace", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID aplikace zadané při vytváření přístupu k Freebox API (tj. ‚app_id‘)", + "DownloadClientQbittorrentSettingsContentLayout": "Rozvržení obsahu", + "DownloadClientQbittorrentSettingsSequentialOrder": "Postupné pořadí", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Povolení přidá torrenty a magnety do rTorrentu v zastaveném stavu. To může způsobit poškození souborů magnet.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Volitelné umístění pro stahování, ponechte prázdné pro použití výchozího umístění rTorrentu", + "Donations": "Dary", + "Connection": "Spojení", + "DeleteSeriesFolder": "Smazat složku seriálu", + "DeleteNotification": "Smazat Oznámení", + "DownloadClientFloodSettingsAdditionalTags": "Další Značky", + "DownloadClientFloodSettingsUrlBaseHelpText": "Přidá prefix do Flood API, viz {url}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Volitelná sdílená složka, do které se mají stahované soubory ukládat, pokud chcete použít výchozí umístění Download Station, ponechte prázdné", + "DownloadClientFreeboxSettingsHostHelpText": "Název hostitele nebo IP adresa hostitele Freeboxu, výchozí hodnota je ‚{url}‘ (funguje pouze ve stejné síti)", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Stahovat nejprve první a poslední kusy (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Stahovat v postupném pořadí (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Používat zabezpečené připojení. Viz Možnosti -> WebUI -> Webové uživatelské rozhraní -> ‚Použít HTTPS místo HTTP‘ v qBittorrentu.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Volitelné umístění pro stahování, ponechte prázdné pro použití výchozího umístění Transmission", + "DownloadClients": "Klienti pro stahování", + "HealthMessagesInfoBox": "Další informace o příčině těchto zpráv o kontrole zdraví najdete kliknutím na odkaz wiki (ikona knihy) na konci řádku nebo kontrolou [logů]({link}). Pokud máte potíže s interpretací těchto zpráv, můžete se obrátit na naši podporu, a to na níže uvedených odkazech.", + "GrabRelease": "Získat vydání", + "DownloadClientRTorrentSettingsUrlPath": "Cesta URL", + "Indexer": "Indexer", + "CustomFormatsSpecificationMaximumSizeHelpText": "Vydání musí odpovídat nebo být menší než tato velikost", + "CustomFormatsSpecificationMinimumSize": "Minimální velikost", + "Deleted": "Smazáno", + "DeletedReasonEpisodeMissingFromDisk": "{appName} nenalezen soubor na disku, Došlo k odvázání souboru na epizodu v databázi", + "DeleteSeriesFolderCountConfirmation": "Opravdu smazat {count} vybraný seriál?", + "DetailedProgressBarHelpText": "Zobrazit text na Ukazateli průběhu", + "Disabled": "Zakázáno", + "DownloadClientFloodSettingsTagsHelpText": "Počáteční značky stahování. Aby bylo stahování rozpoznáno, musí mít všechny počáteční značky. Tím se zabrání konfliktům s nesouvisejícími stahováními.", + "Filters": "Filtry", + "Implementation": "Implementace", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Soubory .strm v této složce budou importovány pomocí drone", + "History": "Historie", + "Discord": "Discord", + "DotNetVersion": ".NET", + "Download": "Stáhnout", + "DownloadClient": "Download klient", + "DownloadClientSettingsInitialStateHelpText": "Počáteční stav pro torrenty přidané do {clientName}", + "DownloadClientSettingsInitialState": "Počáteční stav", + "DownloadClientSettingsDestinationHelpText": "Ručně určuje cíl stahování, pro použití výchozího nastavení nechte prázdné", + "DownloadClientSettingsUseSslHelpText": "Při připojení k {clientName} použít zabezpečené připojení", + "EditConnectionImplementation": "Upravit připojení - {implementationName}", + "EnableInteractiveSearchHelpText": "Použije se při interaktivním vyhledávání", + "Ended": "Ukončeno", + "External": "Externí", + "General": "Obecné", + "AutoTaggingSpecificationGenre": "Žánr(y)", + "AutoTaggingSpecificationMaximumYear": "Maximální Rok", + "AutoTaggingSpecificationMinimumYear": "Minimální Rok", + "AutoTaggingSpecificationOriginalLanguage": "Jazyk", + "AutoTaggingSpecificationQualityProfile": "Profil Kvality", + "AutoTaggingSpecificationRootFolder": "Kořenová Složka", + "AutoTaggingSpecificationSeriesType": "Typ seriálu", + "AutoTaggingSpecificationStatus": "Status", + "CustomFormatsSpecificationLanguage": "Jazyk", + "CustomFormatsSpecificationRegularExpression": "Běžný výraz", + "CustomFormatsSpecificationMinimumSizeHelpText": "Vydání musí být větší než tato velikost", + "CustomFormatsSpecificationResolution": "Rozlišení", + "CustomFormatsSpecificationSource": "Zdroj", + "DeleteImportListExclusion": "Smazat import Seznamu Vyjímek", + "DiskSpace": "Místo na disku", + "DeleteReleaseProfileMessageText": "Opravdu smazat profil vydání '{name}'?", + "Docker": "Docker", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Zda použít rozvržení obsahu nakonfigurované v qBittorrentu, původní rozvržení z torrentu nebo vždy vytvořit podsložku (qBittorrent 4.3.2+)", + "DownloadClientSettingsAddPaused": "Přidat pozastavené", + "DownloadClientSettingsUrlBaseHelpText": "Přidá prefix k {clientName}, například {url}", + "ExistingTag": "Stávající značka", + "ProxyResolveIpHealthCheckMessage": "Nepodařilo se vyřešit adresu IP konfigurovaného hostitele proxy {proxyHostName}", + "DownloadClientDelugeSettingsDirectory": "Adresář stahování", + "DownloadClientPneumaticSettingsNzbFolder": "Složka Nzb", + "FailedToFetchSettings": "Nepodařilo se načíst nastavení", + "DownloadClientSettings": "Nastavení klienta pro stahování", + "Grabbed": "Získáno", + "DatabaseMigration": "Migrace databáze", + "Delay": "Zpoždění", + "DeleteEmptyFolders": "Vymazat prázdné složky", + "DeleteEmptySeriesFoldersHelpText": "Smazat Složky prázdného Seriálu a Sezóny Během skenování a pokud jsou soubory epizody vymazány", + "DeleteTag": "Smazat štítek", + "DeleteTagMessageText": "Opravdu chceš smazat štítek \"{label}\"?", + "DestinationRelativePath": "Relativní cesta Destinace", + "DeleteSpecificationHelpText": "Opravdu smazat specifikaci '{name}'?", + "DownloadClientStatusAllClientHealthCheckMessage": "Všichni klienti pro stahování jsou nedostupní z důvodu selhání", + "DownloadClientStatusSingleClientHealthCheckMessage": "Klienti pro stahování jsou nedostupní z důvodu selhání: {downloadClientNames}", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Přidá předponu k url {clientName} rpc, např. {url}, výchozí hodnota je ‚{defaultUrl}‘" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index e44038b60..9478dbe97 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -22,7 +22,7 @@ "Language": "Sprache", "CloneCondition": "Bedingung klonen", "DeleteCondition": "Bedingung löschen", - "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{0}' löschen willst?", + "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?", "DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?", "RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?", "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?", @@ -2138,5 +2138,14 @@ "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Sobald dieser benutzerdefinierte Formatwert erreicht ist, wird {appName} keine Episoden-Releases mehr herunterladen", "UpgradesAllowedHelpText": "Wenn deaktiviert, werden Qualitäten nicht aktualisiert.", "VideoDynamicRange": "Video-Dynamikbereich", - "Warning": "Warnung" + "Warning": "Warnung", + "ReleasePush": "Veröffentlichung-Push", + "ReleaseSource": "Veröffentlichungsquelle", + "MetadataKometaDeprecatedSetting": "Veraltet", + "MetadataKometaDeprecated": "Kometa-Dateien werden nicht mehr erstellt, die Unterstützung wird in Version 5 vollständig entfernt", + "NotificationsTelegramSettingsIncludeInstanceName": "Instanzname im Titel einfügen", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Optional den Instanznamen in die Benachrichtigung einfügen", + "IndexerSettingsFailDownloadsHelpText": "Beim Verarbeiten abgeschlossener Downloads behandelt {appName} diese ausgewählten Dateitypen als fehlgeschlagene Downloads.", + "UserInvokedSearch": "Benutzerinitiierte Suche", + "IndexerSettingsFailDownloads": "Fehlgeschlagene Downloads" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 309e74fad..c80d577d6 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -4,15 +4,15 @@ "Added": "Lisäysaika", "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää", "DownloadClientSortingHealthCheckMessage": "Latauspalvelun \"{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ä {appName} sen vuoksi löydä sillä tuloksia.", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS-synkronoinnille ei ole määritetty hakupalveluita, eikä {appName} tämän vuoksi kaappaa uusia julkaisuja automaattisesti.", + "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalihaulle ei ole määritetty hakupalveluita, eikä {appName} sen vuoksi löydä sillä tuloksia.", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} ei voinut tuoda jaksoja. Katso tarkemmat tiedot lokista.", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta {appName} ei näe sitä. Kansion käyttöoikeuksia on ehkä muokattava.", - "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattihakua varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName}in automaattihaku ei löydä tuloksia.", + "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattihaulle ei ole määritetty hakupalveluita, eikä {appName}in automaattihaku tämän vuoksi löydä tuloksia.", "AgeWhenGrabbed": "Ikä (kaappaushetkellä)", - "GrabId": "Kaappauksen tunniste", + "GrabId": "Kaappauksen ID", "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", "BrowserReloadRequired": "Vaatii selaimen sivupäivityksen (F5).", "CustomFormatHelpText": "Julkaisut pisteytetään niitä vastaavien mukautettujen muotojen pisteiden yhteenlaskun summalla. {appName} tallentaa julkaisun, jos se parantaa arvosanaa nykyisellä laadulla tai parempaa.", @@ -182,8 +182,8 @@ "IndexerSettingsApiUrl": "Rajapinnan URL", "IndexerSettingsCookie": "Eväste", "IndexerSettingsPasskey": "Suojausavain", - "IndexerTagSeriesHelpText": "Tietolähdettä käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", - "IndexerValidationQuerySeasonEpisodesNotSupported": "Tietolähde ei tue nykyistä kyselyä. Tarkista tukeeko se kategorioita ja kausien/jaksojen etsintää.", + "IndexerTagSeriesHelpText": "Hakupalvelua käytetään vain vähintään yhdellä täsmäävällä tunnisteella merkityille sarjoille. Käytä kaikille jättämällä tyhjäksi.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Hakupalvelu ei tue nykyistä kyselyä. Tarkista tukeeko se kategorioita ja kausien/jaksojen etsintää.", "InfoUrl": "Tietojen URL", "InstanceName": "Instanssin nimi", "InteractiveImportLoadError": "Manuaalituonnin kohteiden lataus epäonnistui", @@ -200,7 +200,7 @@ "IndexerSettingsWebsiteUrl": "Verkkosivuston URL", "IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", "IndexersLoadError": "Tietolähteiden lataus epäonnistui", - "IndexersSettingsSummary": "Tietolähteet ja niiden asetukset.", + "IndexersSettingsSummary": "Hakupalvelut ja julkaisurajoitukset.", "Indexers": "Tietolähteet", "KeyboardShortcutsFocusSearchBox": "Kohdista hakukenttä", "LastExecution": "Edellinen suoritus", @@ -320,7 +320,7 @@ "StandardEpisodeTypeFormat": "Kausien ja jaksojen numerointi ({format})", "StartupDirectory": "Käynnistyskansio", "Started": "Alkoi", - "SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä tietolähteistä painamalla niiden ohessa olevia lisätietopainikkeita.", + "SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä palveluista painamalla niiden ohessa olevia lisätietopainikkeita.", "Status": "Tila", "SupportedListsSeries": "{appName} tukee useita listoja, joiden avulla sarjoja voidaan tuoda tietokantaan.", "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.", @@ -362,7 +362,7 @@ "Monitored": "Valvonta", "ApplyTagsHelpTextHowToApplyDownloadClients": "Tunnisteiden käyttö valituille latauspalveluille", "ApplyTagsHelpTextHowToApplyImportLists": "Tunnisteiden käyttö valituille tuontilistoille", - "ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituille sarjoille", + "ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituille sarjoille:", "LogFiles": "Lokitiedostot", "None": "Ei mitään", "RemoveSelectedItems": "Poista valitut kohteet", @@ -429,7 +429,7 @@ "EditDownloadClientImplementation": "Muokataan latauspalvelua – {implementationName}", "EditImportListImplementation": "Muokataan tuontilistaa – {implementationName}", "EndedOnly": "Vain päättyneet", - "EnableInteractiveSearchHelpTextWarning": "Tämä tietolähde ei tue hakua.", + "EnableInteractiveSearchHelpTextWarning": "Tämä hakupalvelu ei tue hakua.", "Episode": "Jakso", "EpisodeCount": "Jaksomäärä", "EpisodeAirDate": "Jakson esitysaika", @@ -700,7 +700,7 @@ "FileBrowserPlaceholderText": "Kirjoita sijainti tai selaa se alta", "FeatureRequests": "Kehitysehdotukset", "IndexerPriority": "Tietolähteiden painotus", - "IndexerOptionsLoadError": "Tietolähdeasetusten lataus epäonnistui", + "IndexerOptionsLoadError": "Hakupalveluasetusten lataus epäonnistui", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", "RemotePathMappings": "Etäsijaintien kohdistukset", @@ -749,7 +749,7 @@ "DownloadClientOptionsLoadError": "Latauspalveluasetusten lataus epäonnistui", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).", - "IndexerValidationUnableToConnectHttpError": "Tietolähteeseen ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", + "IndexerValidationUnableToConnectHttpError": "Hakupalveluun ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", "BypassDelayIfHighestQualityHelpText": "Ohitusviive kun julkaisun laatu vastaa laatuprofiilin korkeinta käytössä olevaa laatua halutulla protokollalla.", "IndexerHDBitsSettingsCategoriesHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", "IndexerHDBitsSettingsCategories": "Kategoriat", @@ -919,7 +919,7 @@ "IndexerSettings": "Tietolähdeasetukset", "IncludeHealthWarnings": "Sisällytä kuntovaroitukset", "ListsLoadError": "Listojen lataus epäonnistui", - "IndexerValidationUnableToConnect": "Tietolähteeseen ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", + "IndexerValidationUnableToConnect": "Hakupalveluun ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", "MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.", "MetadataSourceSettingsSeriesSummary": "Tietoja siitä, mistä {appName} saa sarjojen ja jaksojen tiedot.", @@ -1156,7 +1156,7 @@ "ReleaseSceneIndicatorMappedNotRequested": "Valittu jakso ei sisältynyt tähän hakuun.", "ReplaceWithSpaceDash": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki\"", "ReplaceWithSpaceDashSpace": "Korvaa yhdistelmällä \"välilyönti yhdysmerkki välilyönti\"", - "SearchIsNotSupportedWithThisIndexer": "Tämä tietolähde ei tue hakua.", + "SearchIsNotSupportedWithThisIndexer": "Tämä hakupalvelu ei tue hakua.", "DownloadClientDownloadStationProviderMessage": "{appName} ei voi muodostaa yhteyttä Download Stationiin, jos DSM-tili on määritetty käyttämään kaksivaiheista tunnistautumista.", "UnsavedChanges": "Muutoksia ei ole tallennettu", "VideoDynamicRange": "Videon dynaaminen alue", @@ -1310,8 +1310,8 @@ "Ok": "Ok", "General": "Yleiset", "Folders": "Kansiot", - "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat tietolähteet eivät ole hiljattaisten tietolähdevirheiden vuoksi tilapaisesti käytettävissä.", - "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Hakua tukevat tietolähteet eivät ole hiljattaisten tietolähdevirheiden vuoksi tilapaisesti käytettävissä.", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Hakua tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", "IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.", "IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika", "KeyboardShortcutsSaveSettings": "Tallenna asetukset", @@ -1403,7 +1403,7 @@ "NotSeasonPack": "Ei ole kausikooste", "OpenBrowserOnStartHelpText": " Avaa {appName}in verkkokäyttöliittymä verkkoselaimeen käynnistyksen yhteydessä.", "Password": "Salasana", - "ReleaseProfileIndexerHelpText": "Määritä mitä tietolähdettä profiili koskee.", + "ReleaseProfileIndexerHelpText": "Määritä mitä hakupalvelua profiili koskee.", "SeasonCount": "Kausien määrä", "SeasonNumberToken": "Kausi {seasonNumber}", "SeasonNumber": "Kauden numero", @@ -1441,7 +1441,7 @@ "SetReleaseGroup": "Aseta julkaisuryhmä", "NotificationsTraktSettingsRefreshToken": "Päivitä tunniste", "IndexerSettingsAdditionalNewznabParametersHelpText": "Huomioi, että kategorian vaihdon jälkeen alaryhmiin on lisättävä pakolliset/rajoitetut aliryhmäsäännöt vieraskielisten julkaisujen välttämiseksi.", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {indexerNames}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Hakupalvelut eivät ole käytettävissä yli kuusi tuntia kestäneiden virheiden vuoksi: {indexerNames}.", "FullSeason": "Koko kausi", "ShowRelativeDates": "Käytä suhteellisia päiväyksiä", "ProxyPasswordHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", @@ -1630,7 +1630,7 @@ "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa latauksen ja sen tiedostot.", "UnableToLoadAutoTagging": "Automaattimerkinnän lataus epäonnistui", "IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa", - "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin tietolähteiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin hakupalveluiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", "NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä", "NotificationsSlackSettingsIconHelpText": "Muuta Slack-julkaisuissa käytettävää kuvaketta (emoji tai URL-osoite).", "NotificationsTwitterSettingsConsumerKey": "Kuluttajan avain", @@ -1671,7 +1671,7 @@ "FilterLessThanOrEqual": "on pienempi kuin tai sama", "FilterNotInLast": "ei kuluneina", "FilterNotInNext": "ei seuraavina", - "IndexerValidationNoResultsInConfiguredCategories": "Kysely onnistui, mutta tietolähteesi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "IndexerValidationNoResultsInConfiguredCategories": "Kysely onnistui, mutta hakupalvelusi ei palauttanut tuloksia määrietystyistä kategorioista. Tämä voi johtua palvelun ongelmasta tai sille määritetyistä kategoria-asetuksista.", "TvdbId": "TheTVDB ID", "DownloadClientSettingsInitialState": "Aloitustila", "DownloadClientSettingsRecentPriority": "Uusien painotus", @@ -1712,7 +1712,7 @@ "MaximumSingleEpisodeAgeHelpText": "Täysiä tuotantokausia etsittäessä hyväksytään vain kausipaketit, joiden uusin jakso on tätä asetusta vanhempi. Koskee vain vakiosarjoja. Poista käytöstä asettamalla arvoksi \"0\" (nolla).", "FailedToLoadSystemStatusFromApi": "Järjestelmän tilan lataus rajapinnasta epäonnistui", "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Sinun on poistettava televisiojärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", - "IndexerValidationNoRssFeedQueryAvailable": "RSS-syötekyselyä ei ole käytettävissä. Tämä voi johtua tietolähteen ongelmasta tai tietolähteelle määritetyistä kategoria-asetuksista.", + "IndexerValidationNoRssFeedQueryAvailable": "RSS-syötekyselyitä ei ole käytettävissä. Tämä voi johtua hakupalvelun ongelmasta tai palvelulle määritetyistä kategoria-asetuksista.", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sinun on poistettava päiväysjärjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Sinun on poistettava elokuvien järjestely käytöstä {appName}in käyttämältä kategorialta tuontiongelmien välttämiseksi. Korjaa tämä SABnzb:stä.", "DownloadClientSettingsCategoryHelpText": "Luomalla {appName}ille oman kategorian, erottuvat sen lataukset muiden lähteiden latauksista. Kategorian määritys on valinnaista, mutta erittäin suositeltavaa.", @@ -1739,7 +1739,7 @@ "Repack": "Uudelleenpaketoitu", "SupportedAutoTaggingProperties": "{appName} tukee automaattimerkinnän säännöissä seuraavia arvoja", "RegularExpressionsCanBeTested": "Säännöllisiä lausekkeita voidaan testata [täällä]({url}).", - "RssSyncIntervalHelpTextWarning": "Tämä koskee kaikkia tietolähteitä. Noudata niiden asettamia sääntöjä.", + "RssSyncIntervalHelpTextWarning": "Tämä koskee kaikkia hakupalveluita. Noudata niiden asettamia sääntöjä.", "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla, esim. \"{url}\". Oletus on \"{defaultApiUrl}\".", "DownloadClientFreeboxSettingsHostHelpText": "Freeboxin isäntänimi tai IP-osoite. Oletus on \"{url}\" (toimii vain samassa verkossa).", "DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on {port}.", @@ -1804,7 +1804,7 @@ "ClickToChangeIndexerFlags": "Muuta tietolähteen lippuja painamalla tästä", "CustomFormatsSpecificationFlag": "Lippu", "SelectIndexerFlags": "Valitse tietolähteen liput", - "SetIndexerFlagsModalTitle": "{modalTitle} – Aseta tietolähteen liput", + "SetIndexerFlagsModalTitle": "{modalTitle} – Aseta hakupalvelun liput", "CustomFilter": "Mukautettu suodatin", "Label": "Nimi", "ShowTagsHelpText": "Näytä tunnisteet julisteen alla.", @@ -2027,7 +2027,7 @@ "ReleaseRejected": "Julkaisu hylättiin", "ReleaseSceneIndicatorSourceMessage": "{message} julkaisua on numeroitu epäselvästi, eikä jaksoa voida tunnistaa luotettavasti.", "RetentionHelpText": "Vain Usenet: määritä rajoittamaton säilytys asettamalla arvoksi 0.", - "SupportedIndexers": "{appName} tukee kaikkien Newznab-yhteensopivien tietolähteiden ohella myös monia muita alla listattuja tietolähteitä.", + "SupportedIndexers": "{appName} tukee kaikkien Newznab-yhteensopivien hakupalveluiden ohella myös monia muita alla listattuja palveluita.", "TagIsNotUsedAndCanBeDeleted": "Tunniste ei ole käytössä ja voidaan poistaa.", "Umask775Description": "{octal} – Omistajalla ja ryhmällä kirjoitus, muilla luku", "SeasonsMonitoredPartial": "Osittainen", @@ -2111,23 +2111,23 @@ "MarkAsFailed": "Merkitse epäonnistuneeksi", "ImportListsSonarrSettingsFullUrlHelpText": "Tuonnin lähteenä olevan {appName}-instanssin täydellinen URL-osoite portteineen.", "ImportListsValidationTestFailed": "Testi keskeytettiin virheen vuoksi: {exceptionMessage}", - "IndexerSettingsRssUrlHelpText": "Syötä tietolähteen {indexer} kanssa toimivan RSS-syötteen URL-osoite.", + "IndexerSettingsRssUrlHelpText": "Syötä hakupalvelun {indexer} kanssa toimivan RSS-syötteen URL-osoite.", "ImportListsSonarrValidationInvalidUrl": "{appName} URL on virheellinen. Puuttuuko URL-perusta?", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ota valmistuneiden latausten käsittely käyttöön, jos mahdollista (ei tue useiden tietokoneiden ympäristöä).", "InvalidFormat": "Virheellinen muoto", - "IndexerValidationUnableToConnectTimeout": "Tietolähteeseen ei voitu muodostaa yhteyttä, mahdollisesti aikakatkaisun vuoksi. yritä uudelleen tai tarkista verkkoasetukset. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Hakupalveluun ei voitu muodostaa yhteyttä, mahdollisesti aikakatkaisun vuoksi. yritä uudelleen tai tarkista verkkoasetukset. {exceptionMessage}.", "IndexerSettingsCookieHelpText": "Jos sivusto vaatii RSS-syötteen käyttöön kirjautumisevästeen, on se noudettava selaimen avulla.", - "IndexerValidationUnableToConnectServerUnavailable": "Tietolähteeseen ei voitu muodostaa yhteyttä, koska sen palvelin ei ole tavoitettavissa. Yritä myöhemmin uudelleen. {exceptionMessage}.", - "IndexerValidationJackettAllNotSupportedHelpText": "Jackettin \"all\"-päätettä ei tueta. Lisää tietolähteet yksitellen.", - "IndexerValidationUnableToConnectInvalidCredentials": "Tietolähteeseen ei voitu muodostaa yhteyttä virheellisten käyttäjätietojen vuoksi. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Hakupalveluun ei voitu muodostaa yhteyttä, koska sen palvelin ei ole tavoitettavissa. Yritä myöhemmin uudelleen. {exceptionMessage}.", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackettin \"all\"-päätettä ei tueta. Lisää hakupalvelut yksitellen.", + "IndexerValidationUnableToConnectInvalidCredentials": "Hakupalveluun ei voitu muodostaa yhteyttä virheellisten käyttäjätietojen vuoksi. {exceptionMessage}.", "IndexerSettingsRssUrl": "RSS-syötteen URL", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Synkronoi sarjojen valvonta", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Synkronoi kausien valvontatila {appName}-instanssista. Tämän ollessa käytössä omaa valvontatilaa ei huomioida.", "ImportListsValidationUnableToConnectException": "Tuontilistaan ei voitu muodostaa yhteyttä: {exceptionMessage}. Saat lisätietoja virheen lähellä olevista lokimerkinnöistä.", "IndexerIPTorrentsSettingsFeedUrlHelpText": "IPTorrentsin luoma täysi RSS-syöte, joka käyttää vain valitsemiasi kategorioita (HD, SD, x264, yms...).", - "IndexerJackettAllHealthCheckMessage": "Jackettin ei-tuettua \"all\"-päätettä käyttävät tietolähteet: {indexerNames}.", + "IndexerJackettAllHealthCheckMessage": "Jackettin ei-tuettua \"all\"-päätettä käyttävät hakupalvelut: {indexerNames}.", "IndexerSettingsAllowZeroSizeHelpText": "Sallii syötteet, jotka eivät ilmoita julkaisujen kokoa. Huomioi, ettei kokoon liittyviä tarkistuksia tällöin suoriteta.", - "IndexerValidationSearchParametersNotSupported": "Tietolähde ei tue vaadittuja hakuparametreja.", + "IndexerValidationSearchParametersNotSupported": "Hakupalvelu ei tue vaadittuja hakuparametreja.", "ImportListsSonarrSettingsFullUrl": "Täysi URL", "ImportListsSonarrSettingsTagsHelpText": "Lähdeinstanssin tunnisteet, joilla tuodaan.", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Ota valmistuneiden latausten käsittely käyttöön, jos mahdollista.", @@ -2138,11 +2138,14 @@ "IndexerSettingsMinimumSeeders": "Jakajien vähimmäismäärä", "IndexerSettingsMinimumSeedersHelpText": "Kaappaukseen vaadittava jakajien vähimmäismäärä.", "IndexerValidationCloudFlareCaptchaRequired": "Sivusto on suojattu CloudFlare CAPTCHA:lla ja se vaatii kelvollisen CAPTCHA-tunnisteen.", - "IndexerValidationFeedNotSupported": "Tietolähteen syötettä ei tueta: {exceptionMessage}", - "IndexerValidationJackettAllNotSupported": "Jackettin \"all\"-päätettä ei tueta. Lisää tietolähteet yksitellen.", + "IndexerValidationFeedNotSupported": "Hakupalvelun syötettä ei tueta: {exceptionMessage}", + "IndexerValidationJackettAllNotSupported": "Jackettin \"all\"-päätettä ei tueta. Lisää hakupalvelut yksitellen.", "KeepAndUnmonitorSeries": "Säilytä sarja ja lopeta sen valvonta", "KeepAndTagSeries": "Säilytä sarja ja merkitse se tunnisteella", "IndexerValidationRequestLimitReached": "Pyyntörajoitus on saavutettu: {exceptionMessage}", "IndexerValidationTestAbortedDueToError": "Testi keskeytettiin virheen vuoksi: {exceptionMessage}", - "IndexerValidationUnableToConnectResolutionFailure": "Tietolähteeseen ei voitu muodostaa yhteyttä. Tarkista yhteytesi tietolähteen palvelimeen ja DNS:ään. {exceptionMessage}." + "IndexerValidationUnableToConnectResolutionFailure": "Hakupalveluun ei voitu muodostaa yhteyttä. Tarkista yhteytesi hakupalvelun palvelimeen ja DNS:ään. {exceptionMessage}.", + "ReleaseSource": "Julkaisulähde", + "UserInvokedSearch": "Käyttäjä herätti haun", + "ReleasePush": "Julkaisun työntö" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 132a133c5..299b8f9e2 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -55,5 +55,207 @@ "AddCondition": "조건 추가", "AddIndexerError": "새 인덱서를 추가 할 수 없습니다. 다시 시도해주세요.", "TorrentBlackholeTorrentFolder": "토렌트 폴더", - "UseSsl": "SSL 사용" + "UseSsl": "SSL 사용", + "AddListExclusion": "목록 예외 추가", + "BeforeUpdate": "업데이트 전", + "BindAddress": "바인드 주소", + "RemotePathMappingsInfo": "원격 경로 매핑은 거의 필요하지 않습니다. {appName}와(과) 다운로드 클라이언트가 동일한 시스템에 있는 경우 경로를 일치시키는 것이 좋습니다. 상세 내용은 [위키]({wikiLink})를 참조하세요.", + "RemoveSelectedBlocklistMessageText": "블랙리스트에서 선택한 항목을 제거 하시겠습니까?", + "RemoveSelectedItemQueueMessageText": "대기열에서 {0} 항목 {1}을 제거하시겠습니까?", + "RssSync": "RSS 동기화", + "RssSyncInterval": "RSS 동기화 간격", + "SelectLanguages": "언어 선택", + "SizeOnDisk": "디스크 상 크기", + "SslCertPassword": "SSL 인증서 비밀번호", + "SslCertPasswordHelpText": "pfx 파일의 비밀번호", + "SslCertPath": "SSL 인증서 경로", + "SupportedIndexersMoreInfo": "개별 인덱서에 대한 상세 내용을 보려면 정보 버튼을 클릭하세요.", + "SupportedListsMoreInfo": "개별 가져오기 목록에 대한 상세 내용을 보려면 정보 버튼을 클릭하세요.", + "UiLanguage": "UI 언어", + "UiSettings": "UI 설정", + "UpgradeUntil": "품질까지 업그레이드", + "Uppercase": "대문자", + "AutoTaggingSpecificationStatus": "상태", + "BuiltIn": "내장", + "ChangeFileDate": "파일 날짜 변경", + "Delete": "삭제", + "DeleteSelectedIndexers": "인덱서 삭제", + "DownloadClientSettings": "클라이언트 설정 다운로드", + "GrabId": "ID 잡아", + "ImportLists": "기울기", + "LocalPath": "로컬 경로", + "AnalyticsEnabledHelpText": "익명의 사용 및 오류 정보를 {appName}의 서버에 보냅니다. 여기에는 브라우저에 대한 정보, 사용하는 {appName} WebUI 페이지, 오류 보고, OS 및 런타임 버전이 포함됩니다. 이 정보를 사용하여 기능 및 버그 수정의 우선 순위를 지정합니다.", + "Connection": "연결", + "Dates": "날짜", + "DownloadClientSettingsRecentPriority": "클라이언트 우선 순위", + "Day": "일", + "Debug": "디버그", + "EditIndexerImplementation": "인덱서 추가 - {implementationName}", + "Clone": "닫기", + "CleanLibraryLevel": "정리 라이브러리 수준", + "ClickToChangeQuality": "품질을 변경하려면 클릭", + "ClientPriority": "클라이언트 우선 순위", + "Component": "구성 요소", + "DeleteEmptyFolders": "빈 폴더 삭제", + "MinimumCustomFormatScore": "최소 사용자 정의 형식 점수", + "MinimumFreeSpaceHelpText": "사용 가능한 디스크 공간을 이보다 적게 남겨 둘 경우 가져오기 방지", + "MoveAutomatically": "빠른 가져오기", + "No": "아니", + "IndexerSettingsRejectBlocklistedTorrentHashes": "동기화 중 차단 목록에 있는 토렌트 해시 거부", + "NotificationsSimplepushSettingsEvent": "이벤트", + "AutomaticSearch": "자동 검색", + "CustomFormat": "사용자 정의 형식", + "DelayProfile": "지연 프로필", + "DelayProfiles": "지연 프로필", + "EnableRss": "RSS 활성화", + "ChangeFileDateHelpText": "가져오기 / 재검색시 파일 날짜 변경", + "CreateGroup": "그룹 만들기", + "RecyclingBinHelpText": "영화 파일은 영구적으로 삭제되지 않고 삭제되면 여기로 이동합니다", + "IconForCutoffUnmetHelpText": "컷오프가 충족되지 않은 경우 파일 아이콘 표시", + "RemotePath": "원격 경로", + "ChmodFolder": "chmod 폴더", + "RemotePathMappingHostHelpText": "원격 다운로드 클라이언트에 지정한 것과 동일한 호스트", + "RemotePathMappingRemotePathHelpText": "다운로드 클라이언트가 액세스하는 디렉토리의 루트 경로", + "RemoveFromBlocklist": "블랙리스트에서 제거", + "RequiredHelpText": "이 {implementationName} 조건은 적용 할 맞춤 형식에 대해 일치해야합니다. 그렇지 않으면 단일 {implementationName} 일치로 충분합니다.", + "ShowRelativeDates": "상대 날짜 표시", + "ShowRelativeDatesHelpText": "상대 (오늘 / 어제 / 기타) 또는 절대 날짜 표시", + "TablePageSize": "페이지 크기", + "TablePageSizeHelpText": "각 페이지에 표시 할 항목 수", + "TagCannotBeDeletedWhileInUse": "사용 중에는 삭제할 수 없습니다", + "AddImportListExclusionError": "새 목록 제외를 추가 할 수 없음 재시도해주세요.", + "AddQualityProfile": "품질 프로필 추가", + "AddReleaseProfile": "지연 프로필 편집", + "AutoRedownloadFailedHelpText": "다른 출시를 자동으로 검색하고 다운로드 시도", + "BypassProxyForLocalAddresses": "로컬 주소에 대한 프록시 우회", + "CancelPendingTask": "이 보류 중인 작업을 취소하시겠습니까?", + "CancelProcessing": "처리 취소", + "Certification": "인증", + "SslPort": "SSL 포트", + "CustomFormatsSettings": "사용자 정의 형식 설정", + "CustomFormatsSpecificationReleaseGroup": "출시 그룹", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb 폴더", + "CopyToClipboard": "클립 보드에 복사", + "DeleteQualityProfile": "품질 프로필 삭제", + "DetailedProgressBarHelpText": "진행률 표시줄에 텍스트 표시", + "Disabled": "비활성화됨", + "DisabledForLocalAddresses": "로컬 주소에 대해 비활성화됨", + "DiskSpace": "디스크 공간", + "Filters": "필터", + "ImportListExclusions": "제외 목록", + "AddRemotePathMapping": "원격 경로 매핑 추가", + "AppUpdatedVersion": "{appName}이 버전 `{version}`으로 업데이트되었습니다. 최신 변경 사항을 받으려면 {appName}을 재로드해야 합니다 ", + "BackupIntervalHelpText": "자동 백업 간격", + "BackupNow": "지금 백업", + "Backups": "백업", + "ChmodFolderHelpText": "8 진수, 미디어 폴더 및 파일로 가져오기 / 이름 변경 중에 적용됨 (실행 비트 없음)", + "ChooseAnotherFolder": "다른 폴더 선택", + "CompletedDownloadHandling": "완료된 다운로드 처리", + "CopyUsingHardlinksHelpTextWarning": "간혹 파일 잠금으로 인해 시드중인 파일의 이름을 바꾸지 못할 수 있습니다. 일시적으로 시드를 비활성화하고 {appName}의 이름 바꾸기 기능을 해결 방법으로 사용할 수 있습니다.", + "CurrentlyInstalled": "현재 설치됨", + "CustomFormatsSettingsSummary": "사용자 정의 형식 및 설정", + "DeleteIndexer": "인덱서 삭제", + "DeleteNotification": "알림 삭제", + "DeleteRemotePathMapping": "원격 경로 매핑 편집", + "DeleteTag": "태그 삭제", + "Download": "다운로드", + "DownloadClientRootFolderHealthCheckMessage": "다운로드 클라이언트 {downloadClientName} 은(는) 루트 폴더 {rootFolderPath}에 다운로드를 저장합니다. 루트 폴더에 다운로드해서는 안됩니다.", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "해시에 의해 토렌트가 차단된 경우 일부 인덱서의 RSS/검색 중에 토렌트가 제대로 거부되지 않을 수 있습니다. 이 기능을 활성화하면 토렌트를 가져온 후 클라이언트로 전송하기 전에 토렌트를 거부할 수 있습니다.", + "DoneEditingGroups": "그룹 편집 완료", + "DotNetVersion": ".NET", + "EditImportListExclusion": "목록 제외 편집", + "EnableSsl": "SSL 활성화", + "Conditions": "조건", + "ConnectSettings": "연결 설정", + "AddImportListExclusion": "목록 예외 추가", + "AllFiles": "모든 파일", + "Always": "항상", + "Connect": "연결", + "ConnectionLostReconnect": "Radarr가 자동으로 연결을 시도하거나 아래에서 새로고침을 클릭할 수 있습니다.", + "CustomFormats": "사용자 정의 형식", + "Ui": "UI", + "DockerUpdater": "Docker 컨테이너를 업데이트하여 업데이트를 받으세요", + "EditConnectionImplementation": "애플리케이션 추가 - {implementationName}", + "From": "부터", + "BranchUpdate": "{appName} 업데이트에 사용할 파생 버전", + "DeleteBackup": "백업 삭제", + "DeleteSelectedDownloadClients": "다운로드 클라이언트 삭제", + "AgeWhenGrabbed": "연령 (잡았을 때)", + "AnalyseVideoFiles": "비디오 파일 분석", + "ApiKey": "API 키", + "AptUpdater": "apt를 사용하여 업데이트 설치", + "BranchUpdateMechanism": "외부 업데이트 메커니즘에서 사용하는 파생 버전", + "DeleteSpecification": "알림 삭제", + "Deleted": "삭제됨", + "Clear": "지우기", + "CloneProfile": "프로필 복제", + "CloneIndexer": "인덱서 복제", + "Cutoff": "중단점", + "Date": "날짜", + "DeleteDelayProfileMessageText": "이 지연 프로필을 삭제하시겠습니까?", + "DoNotBlocklistHint": "차단 목록에 추가하지 않고 제거", + "DoNotPrefer": "선호하지 않음", + "EnableHelpText": "이 메타데이터 유형에 대한 메타데이터 파일 생성 활성화", + "Docker": "Docker", + "Donations": "기부", + "ImportList": "기울기", + "ImportListSettings": "목록 설정", + "Add": "추가", + "Apply": "적용", + "ApplyTags": "태그 적용", + "Cancel": "취소", + "EditDownloadClientImplementation": "다운로드 클라이언트 추가 - {implementationName}", + "EditReleaseProfile": "지연 프로필 편집", + "AddRootFolder": "루트 폴더 추가", + "ExtraFileExtensionsHelpText": "가져올 추가 파일의 쉼표로 구분 된 목록 (.nfo는 .nfo-orig로 가져옴)", + "AddDelayProfile": "지연 프로필 추가", + "AddDownloadClient": "다운로드 클라이언트 추가", + "AddExclusion": "예외 추가", + "AddNewRestriction": "새로운 제한 추가", + "AuthForm": "양식 (로그인 페이지)", + "AuthBasic": "기본 (브라우저 팝업)", + "Authentication": "인증", + "Automatic": "자동", + "CertificateValidation": "인증서 검증", + "Close": "닫기", + "Connections": "연결", + "DeleteImportListExclusion": "가져오기 목록 제외 삭제", + "DoNotUpgradeAutomatically": "자동 업그레이드 안함", + "DownloadPropersAndRepacksHelpText": "Propers / Repacks로 자동 업그레이드할지 여부", + "NoIssuesWithYourConfiguration": "구성에 문제 없음", + "IncludeHealthWarnings": "건강 경고 포함", + "DownloadClientsSettingsSummary": "클라이언트 다운로드, 다운로드 처리 및 원격 경로 매핑", + "DownloadFailed": "다운로드 실패함", + "Downloaded": "다운로드됨", + "Downloading": "다운로드 중", + "ICalShowAsAllDayEvents": "종일 이벤트로 표시", + "Rating": "등급", + "RegularExpression": "일반 표현", + "Activity": "활동", + "Backup": "백업", + "AudioInfo": "오디오 정보", + "DownloadClient": "클라이언트 다운로드", + "ShortDateFormat": "짧은 날짜 형식", + "Yes": "예", + "Actions": "동작", + "About": "정보", + "EditConditionImplementation": "연결 추가 - {implementationName}", + "ICalLink": "iCal 링크", + "Agenda": "일정", + "DownloadClients": "클라이언트 다운로드", + "QualityProfile": "품질 프로필", + "Rss": "RSS", + "WhatsNew": "새로운 소식?", + "MaximumSize": "최대 크기", + "DeleteDownloadClient": "다운로드 클라이언트 삭제", + "DeleteImportListExclusionMessageText": "이 가져오기 목록 제외를 삭제하시겠습니까?", + "DeleteDelayProfile": "지연 프로필 삭제", + "DestinationRelativePath": "대상 상대 경로", + "File": "파일", + "ImportListsTraktSettingsGenres": "장르", + "InteractiveSearchModalHeader": "대화형 검색", + "ManualImportItemsLoadError": "수동 가져오기 항목을 로드할 수 없습니다", + "LongDateFormat": "긴 날짜 형식", + "Lowercase": "소문자", + "KeyboardShortcutsSaveSettings": "설정 저장" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 5dc4067f8..8a5119c3f 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1,7 +1,7 @@ { "ApplyChanges": "Aplicar mudanças", "AutomaticAdd": "Adição automática", - "CountSeasons": "{count} Temporadas", + "CountSeasons": "{count} temporadas", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Nenhum cliente de download está disponível", "DownloadClientStatusAllClientHealthCheckMessage": "Todos os clientes de download estão indisponíveis devido a falhas", "EditSelectedDownloadClients": "Editar clientes de download selecionados", @@ -28,7 +28,7 @@ "PreviousAiring": "Exibição Anterior", "Priority": "Prioridade", "RemoveFailedDownloads": "Remover downloads com falha", - "QualityProfile": "Perfil de Qualidade", + "QualityProfile": "Perfil de qualidade", "RefreshSeries": "Atualizar Séries", "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Você está usando o docker; o cliente de download {downloadClientName} coloca os downloads em {path}, mas este diretório parece não existir dentro do contêiner. Revise seus mapeamentos de caminho remoto e configurações de volume de contêiner.", "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "O {appName} pode ver, mas não acessar o episódio baixado {path}. Provável erro de permissão.", @@ -42,7 +42,7 @@ "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "O cliente de download remoto {downloadClientName} relatou arquivos em {path}, mas este diretório parece não existir. Provavelmente faltando mapeamento de caminho remoto.", "RemovedSeriesMultipleRemovedHealthCheckMessage": "A série {series} foi removida do TheTVDB", "RemovedSeriesSingleRemovedHealthCheckMessage": "As séries {series} foram removidas do TheTVDB", - "RootFolder": "Pasta Raiz", + "RootFolder": "Pasta raiz", "RootFolderMissingHealthCheckMessage": "Pasta raiz ausente: {rootFolderPath}", "RootFolderMultipleMissingHealthCheckMessage": "Faltam várias pastas raiz: {rootFolderPaths}", "SearchForMonitoredEpisodes": "Pesquisar episódios monitorados", @@ -86,15 +86,15 @@ "RemotePathMappingWrongOSPathHealthCheckMessage": "O cliente de download remoto {downloadClientName} coloca os downloads em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' não pode ser gravada pelo usuário '{userName}'.", "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' está em uma pasta de translocação do App.", - "BlocklistReleases": "Lançamentos na lista de bloqueio", + "BlocklistReleases": "Bloquear lançamentos", "CloneCondition": "Clonar condição", "CloneCustomFormat": "Clonar formato personalizado", "Close": "Fechar", "Delete": "Excluir", "DeleteCondition": "Excluir condição", - "DeleteConditionMessageText": "Tem certeza de que deseja excluir a condição '{name}'?", + "DeleteConditionMessageText": "Tem certeza de que deseja excluir a condição \"{name}\"?", "DeleteCustomFormat": "Excluir formato personalizado", - "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado '{name}'?", + "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado \"{name}\"?", "ExportCustomFormat": "Exportar formato personalizado", "Negated": "Negado", "Remove": "Remover", @@ -104,7 +104,7 @@ "RemoveSelectedItems": "Remover Itens Selecionados", "RemoveSelectedItemsQueueMessageText": "Tem certeza de que deseja remover {selectedCount} itens da fila?", "Required": "Requerido", - "BlocklistRelease": "Lançamento na lista de bloqueio", + "BlocklistRelease": "Bloquear lançamento", "Add": "Adicionar", "AddingTag": "Adicionar etiqueta", "Apply": "Aplicar", @@ -169,7 +169,7 @@ "Calendar": "Calendário", "Connect": "Conectar", "CustomFormats": "Formatos personalizados", - "CutoffUnmet": "Corte não atingido", + "CutoffUnmet": "Limite não atingido", "DownloadClients": "Clientes de download", "Events": "Eventos", "General": "Geral", @@ -185,7 +185,7 @@ "ApplyTagsHelpTextHowToApplyDownloadClients": "Como aplicar etiquetas aos clientes de download selecionados", "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar etiquetas às listas de importação selecionadas", "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", - "ApplyTagsHelpTextHowToApplySeries": "Como aplicar tags à série selecionada", + "ApplyTagsHelpTextHowToApplySeries": "Como aplicar etiquetas à série selecionada", "EpisodeInfo": "Info do Episódio", "EpisodeNumbers": "Número(s) do(s) Episódio(s)", "FullSeason": "Temporada Completa", @@ -217,7 +217,7 @@ "Clear": "Limpar", "CurrentlyInstalled": "Atualmente instalado", "DeleteBackup": "Excluir backup", - "DeleteBackupMessageText": "Tem certeza de que deseja excluir o backup '{name}'?", + "DeleteBackupMessageText": "Tem certeza de que deseja excluir o backup \"{name}\"?", "Discord": "Discord", "DiskSpace": "Espaço em disco", "Docker": "Docker", @@ -281,10 +281,10 @@ "Scheduled": "Agendado", "SeriesEditor": "Editor de séries", "Size": "Tamanho", - "Source": "Fonte", + "Source": "Origem", "Started": "Iniciado", "StartupDirectory": "Diretório de Inicialização", - "Status": "Estado", + "Status": "Status", "TestAll": "Testar Tudo", "TheLogLevelDefault": "O nível de registro é padronizado como 'Info' e pode ser alterado em [Configurações Gerais](/settings/general)", "Time": "Horário", @@ -369,14 +369,14 @@ "Conditions": "Condições", "CloneAutoTag": "Clonar etiqueta automática", "DeleteAutoTag": "Excluir etiqueta automática", - "DeleteAutoTagHelpText": "Tem certeza de que deseja excluir a etiqueta automática '{name}'?", + "DeleteAutoTagHelpText": "Tem certeza de que deseja excluir a etiqueta automática \"{name}\"?", "EditAutoTag": "Editar etiqueta automática", "Negate": "Negar", "Save": "Salvar", "AddRootFolder": "Adicionar pasta raiz", "AutoTagging": "Etiquetas automáticas", "DeleteRootFolder": "Excluir pasta raiz", - "DeleteRootFolderMessageText": "Tem certeza de que deseja excluir a pasta raiz '{path}'?", + "DeleteRootFolderMessageText": "Tem certeza de que deseja excluir a pasta raiz \"{path}\"?", "RemoveTagsAutomaticallyHelpText": "Remover tags automaticamente se as condições não forem encontradas", "RootFolders": "Pastas Raiz", "AllResultsAreHiddenByTheAppliedFilter": "Todos os resultados estão ocultos pelo filtro aplicado", @@ -409,8 +409,8 @@ "AddImportListExclusionError": "Não foi possível adicionar uma nova exclusão à lista de importação. Tente novamente.", "AddIndexer": "Adicionar indexador", "AddIndexerError": "Não foi possível adicionar um novo indexador. Tente novamente.", - "AddList": "Adicionar Lista", - "AddListError": "Não foi possível adicionar uma nova lista, tente novamente.", + "AddList": "Adicionar lista", + "AddListError": "Não foi possível adicionar uma nova lista. Tente novamente.", "AddListExclusionError": "Não foi possível adicionar uma nova exclusão à lista. Tente novamente.", "AddNewRestriction": "Adicionar nova restrição", "AddNotificationError": "Não foi possível adicionar uma nova notificação. Tente novamente.", @@ -424,7 +424,7 @@ "AnalyseVideoFiles": "Analisar arquivos de vídeo", "Analytics": "Análises", "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas da interface Web do {appName} você usa, relatórios de erros, a versão do sistema operacional e do tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", - "AnimeEpisodeFormat": "Formato do Episódio de Anime", + "AnimeEpisodeFormat": "Formato do episódio de anime", "ApiKey": "Chave da API", "ApplicationURL": "URL do aplicativo", "ApplicationUrlHelpText": "A URL externa deste aplicativo, incluindo http(s)://, porta e URL base", @@ -442,13 +442,13 @@ "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão excluídos automaticamente", "BackupsLoadError": "Não foi possível carregar os backups", "BindAddress": "Vincular endereço", - "BindAddressHelpText": "Endereço IP válido, localhost ou '*' para todas as interfaces", + "BindAddressHelpText": "Endereço IP válido, localhost ou \"*\" para todas as interfaces", "BlocklistLoadError": "Não foi possível carregar a lista de bloqueio", "Branch": "Ramificação", "BranchUpdate": "Ramificação para atualizar o {appName}", "BranchUpdateMechanism": "Ramificação usada pelo mecanismo externo de atualização", "BrowserReloadRequired": "É necessário recarregar o navegador", - "BuiltIn": "Embutido", + "BuiltIn": "Integrado", "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação do formato personalizado", "BypassDelayIfAboveCustomFormatScoreHelpText": "Ignorar quando o lançamento tiver uma pontuação mais alta que a pontuação mínima configurada do formato personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação mínima do formato personalizado", @@ -476,24 +476,24 @@ "ColonReplacementFormatHelpText": "Mude como o {appName} lida com a substituição do dois-pontos", "CompletedDownloadHandling": "Gerenciamento de downloads concluídos", "Condition": "Condição", - "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões Regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam de escape com um `\\`", + "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de Expressões regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam de escape com uma `\\`", "ConnectSettings": "Configurações de conexão", - "ConnectSettingsSummary": "Notificações, conexões com servidores/players de mídia e scripts personalizados", + "ConnectSettingsSummary": "Notificações, conexões com servidores/reprodutores de mídia e scripts personalizados", "Connections": "Conexões", "CopyToClipboard": "Copiar para a área de transferência", "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo semeados. Você pode desabilitar temporariamente a semeadura e usar a função de renomeação do {appName} como uma solução alternativa.", - "CreateEmptySeriesFolders": "Criar Pastas de Séries Vazias", - "CreateEmptySeriesFoldersHelpText": "Crie pastas de série ausentes durante a verificação de disco", + "CreateEmptySeriesFolders": "Criar pastas de séries vazias", + "CreateEmptySeriesFoldersHelpText": "Crie pastas de séries ausentes durante a verificação de disco", "CreateGroup": "Criar grupo", "Custom": "Personalizado", "CustomFormat": "Formato personalizado", - "CustomFormatUnknownCondition": "Condição de formato personalizado '{implementation}' desconhecida", - "CustomFormatUnknownConditionOption": "Opção '{key}' desconhecida para a condição '{implementation}'", + "CustomFormatUnknownCondition": "Condição de formato personalizado \"{implementation}\" desconhecida", + "CustomFormatUnknownConditionOption": "Opção \"{key}\" desconhecida para a condição \"{implementation}\"", "CustomFormatsLoadError": "Não foi possível carregar os formatos personalizados", "CustomFormatsSettings": "Configurações de formatos personalizados", "CustomFormatsSettingsSummary": "Formatos personalizados e configurações", "DailyEpisodeFormat": "Formato do episódio diário", - "Cutoff": "Corte", + "Cutoff": "Limite", "Dash": "Traço", "Dates": "Datas", "Debug": "Depuração", @@ -501,30 +501,30 @@ "DelayMinutes": "{delay} minutos", "DelayProfile": "Perfil de atraso", "DefaultCase": "Padrão maiúscula ou minúscula", - "DelayProfileSeriesTagsHelpText": "Aplica-se a séries com pelo menos uma tag correspondente", + "DelayProfileSeriesTagsHelpText": "Aplica-se a séries com pelo menos uma etiqueta correspondente", "DelayProfiles": "Perfis de atraso", "DelayProfilesLoadError": "Não foi possível carregar os perfis de atraso", "DeleteDelayProfile": "Excluir perfil de atraso", "DeleteDownloadClient": "Excluir cliente de download", - "DeleteDownloadClientMessageText": "Tem certeza de que deseja excluir o cliente de download '{name}'?", + "DeleteDownloadClientMessageText": "Tem certeza de que deseja excluir o cliente de download \"{name}\"?", "DeleteEmptyFolders": "Excluir pastas vazias", "DeleteEmptySeriesFoldersHelpText": "Excluir pastas vazias de séries e temporadas durante a verificação de disco e quando os arquivos de episódios são excluídos", "DeleteImportList": "Excluir lista de importação", "DeleteImportListExclusion": "Excluir exclusão da lista de importação", - "DeleteImportListExclusionMessageText": "Tem certeza de que deseja excluir esta exclusão da lista de importação?", + "DeleteImportListExclusionMessageText": "Tem certeza de que deseja remover esta exclusão da lista de importação?", "DeleteIndexer": "Excluir indexador", - "DeleteIndexerMessageText": "Tem certeza de que deseja excluir o indexador '{name}'?", + "DeleteIndexerMessageText": "Tem certeza de que deseja excluir o indexador \"{name}\"?", "DeleteNotification": "Excluir notificação", - "DeleteNotificationMessageText": "Tem certeza de que deseja excluir a notificação '{name}'?", + "DeleteNotificationMessageText": "Tem certeza de que deseja excluir a notificação \"{name}\"?", "DeleteQualityProfile": "Excluir perfil de qualidade", - "DeleteQualityProfileMessageText": "Tem certeza de que deseja excluir o perfil de qualidade '{name}'?", + "DeleteQualityProfileMessageText": "Tem certeza de que deseja excluir o perfil de qualidade \"{name}\"?", "DeleteReleaseProfile": "Excluir perfil de lançamento", "DeleteRemotePathMapping": "Excluir mapeamento de caminho remoto", "DeleteRemotePathMappingMessageText": "Tem certeza de que deseja excluir este mapeamento de caminho remoto?", "DeleteSpecification": "Excluir especificação", - "DeleteSpecificationHelpText": "Tem certeza de que deseja excluir a especificação '{name}'?", + "DeleteSpecificationHelpText": "Tem certeza de que deseja excluir a especificação \"{name}\"?", "DeleteTag": "Excluir etiqueta", - "DeleteTagMessageText": "Tem certeza de que deseja excluir a etiqueta '{label}'?", + "DeleteTagMessageText": "Tem certeza de que deseja excluir a etiqueta \"{label}\"?", "DisabledForLocalAddresses": "Desabilitado para endereços locais", "DoNotPrefer": "Não preferir", "DoNotUpgradeAutomatically": "Não atualizar automaticamente", @@ -558,7 +558,7 @@ "EnableInteractiveSearchHelpText": "Será usado com a pesquisa interativa", "EnableColorImpairedMode": "Habilitar modo para daltonismo", "EnableInteractiveSearchHelpTextWarning": "A pesquisa não é compatível com este indexador", - "EnableMediaInfoHelpText": "Extraia informações do vídeo, como resolução, duração e informações do codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as verificações.", + "EnableMediaInfoHelpText": "Extraia informações do vídeo, como resolução, duração e informações do codec de arquivos. Isso requer que o {appName} leia partes do arquivo, o que pode causar alta atividade no disco ou na rede durante as verificações.", "EnableMetadataHelpText": "Habilitar criação de arquivo de metadados para este tipo de metadados", "EnableProfile": "Habilitar perfil", "EnableProfileHelpText": "Marque para habilitar o perfil de lançamento", @@ -733,17 +733,17 @@ "RecyclingBinCleanupHelpText": "Defina como 0 para desativar a limpeza automática", "RecyclingBinCleanupHelpTextWarning": "Os arquivos na lixeira mais antigos do que o número de dias selecionado serão limpos automaticamente", "RecyclingBinHelpText": "Os arquivos irão para cá quando excluídos, em vez de serem excluídos permanentemente", - "AbsoluteEpisodeNumber": "Número Absoluto do Episódio", + "AbsoluteEpisodeNumber": "Número absoluto do episódio", "AddAutoTagError": "Não foi possível adicionar uma nova etiqueta automática, tente novamente.", "AnalyseVideoFilesHelpText": "Extraia informações do vídeo, como resolução, duração e informações do codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as verificações.", "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, o {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação para endereços locais.", - "CopyUsingHardlinksSeriesHelpText": "Os links rígidos permitem que o {appName} importe torrents de propagação para a pasta da série sem ocupar espaço extra em disco ou copiar todo o conteúdo do arquivo. Links rígidos só funcionarão se a origem e o destino estiverem no mesmo volume", + "CopyUsingHardlinksSeriesHelpText": "Os links rígidos permitem que o {appName} importe torrents que estão sendo semeados para a pasta da série sem ocupar espaço adicional em disco ou copiar todo o conteúdo do arquivo. Links rígidos só funcionarão se a origem e o destino estiverem no mesmo volume", "CustomFormatHelpText": "O {appName} pontua cada lançamento usando a soma das pontuações para formatos personalizados correspondentes. Se um novo lançamento tiver melhor pontuação, com a mesma qualidade ou melhor, o {appName} o obterá.", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DeleteDelayProfileMessageText": "Tem certeza de que deseja excluir este perfil de atraso?", - "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista '{name}'?", - "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir o perfil de lançamento '{name}'?", + "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista \"{name}\"?", + "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir o perfil de lançamento \"{name}\"?", "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", "EpisodeTitleRequiredHelpText": "Impeça a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for TBA", "External": "Externo", @@ -823,7 +823,7 @@ "SeriesID": "ID da Série", "SeriesLoadError": "Não foi possível carregar a série", "SeriesTitleToExcludeHelpText": "O nome da série a excluir", - "SeriesType": "Tipo de Série", + "SeriesType": "Tipo de série", "SeriesTypes": "Tipos de Séries", "SetPermissions": "Definir Permissões", "SetPermissionsLinuxHelpTextWarning": "Se você não tiver certeza do que essas configurações fazem, não as altere.", @@ -927,17 +927,17 @@ "WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)", "UnmonitorSpecialEpisodes": "Não Monitorar Especiais", "MonitorAllEpisodes": "Todos os Episódios", - "AddNewSeries": "Adicionar Novas Séries", - "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja adicionar.", - "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos cortes não foram atendidos", - "AddNewSeriesSearchForMissingEpisodes": "Iniciar a busca por episódios perdidos", + "AddNewSeries": "Adicionar nova série", + "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja acrescentar.", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos limites não foram atendidos", + "AddNewSeriesSearchForMissingEpisodes": "Iniciar a busca por episódios ausentes", "AddSeriesWithTitle": "Adicionar {title}", "AllSeriesInRootFolderHaveBeenImported": "Todas as séries em {path} foram importadas", "AlreadyInYourLibrary": "Já está na sua biblioteca", "Anime": "Anime", "CancelProcessing": "Cancelar processamento", "ChooseAnotherFolder": "Escolha outra pasta", - "CouldNotFindResults": "Não foi possível encontrar nenhum resultado para '{term}'", + "CouldNotFindResults": "Não foi possível encontrar nenhum resultado para \"{term}\"", "Existing": "Existente", "ImportCountSeries": "Importar {selectedCount} Séries", "ImportErrors": "Erros de importação", @@ -971,7 +971,7 @@ "StartProcessing": "Iniciar Processamento", "Upcoming": "Por vir", "AddNewSeriesError": "Falha ao carregar os resultados da pesquisa. Tente novamente.", - "AddNewSeriesRootFolderHelpText": "A subpasta '{folder}' será criada automaticamente", + "AddNewSeriesRootFolderHelpText": "A subpasta \"{folder}\" será criada automaticamente", "AnimeEpisodeTypeDescription": "Episódios lançados usando um número de episódio absoluto", "DailyEpisodeTypeDescription": "Episódios lançados diariamente ou com menos frequência que usam ano-mês-dia (2023-08-04)", "LibraryImportTipsSeriesUseRootFolder": "Aponte o {appName} para a pasta que contém todas as suas séries, não uma específica. Por exemplo. \"`{goodFolderExample}`\" e não \"`{badFolderExample}`\". Além disso, cada série deve estar em sua própria pasta dentro da pasta raiz/biblioteca.", @@ -984,8 +984,8 @@ "Today": "Hoje", "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 usando {appName} manualmente ou por outra ferramenta por meio da API", + "DeletedReasonEpisodeMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então ele foi desvinculado do episódio no banco de dados", + "DeletedReasonManual": "O arquivo foi excluído usando o {appName}, manualmente ou por outra ferramenta por meio da API", "DownloadFailed": "Download Falhou", "DestinationRelativePath": "Caminho de destino relativo", "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", @@ -1035,19 +1035,19 @@ "SpecialEpisode": "Episódio Especial", "Agenda": "Programação", "AnEpisodeIsDownloading": "Um episódio está baixando", - "CalendarLegendEpisodeMissingTooltip": "O episódio foi ao ar e está faltando no disco", + "CalendarLegendEpisodeMissingTooltip": "O episódio foi ao ar e está ausente no disco", "CalendarFeed": "Feed de calendário do {appName}", "CalendarLegendEpisodeDownloadedTooltip": "O episódio foi baixado e classificado", - "CalendarLegendEpisodeDownloadingTooltip": "O episódio está sendo baixado no momento", + "CalendarLegendEpisodeDownloadingTooltip": "O episódio está sendo baixado", "CalendarLegendSeriesFinaleTooltip": "Final de série ou temporada", - "CalendarLegendEpisodeOnAirTooltip": "Episódio está sendo exibido no momento", + "CalendarLegendEpisodeOnAirTooltip": "O episódio está sendo exibido no momento", "CalendarLegendSeriesPremiereTooltip": "Estreia de série ou temporada", - "CalendarLegendEpisodeUnairedTooltip": "Episódio ainda não foi ao ar", + "CalendarLegendEpisodeUnairedTooltip": "O episódio ainda não foi ao ar", "CalendarLegendEpisodeUnmonitoredTooltip": "Episódio não monitorado", "CalendarOptions": "Opções do calendário", "CheckDownloadClientForDetails": "verifique o cliente de download para saber mais", - "CollapseMultipleEpisodes": "Agrupar Múltiplos Episódios", - "CollapseMultipleEpisodesHelpText": "Agrupar múltiplos episódios que vão ao ar no mesmo dia", + "CollapseMultipleEpisodes": "Agrupar vários episódios", + "CollapseMultipleEpisodesHelpText": "Agrupar vários episódios que vão ao ar no mesmo dia", "Day": "Dia", "DeletedReasonUpgrade": "O arquivo foi excluído para importar uma atualização", "DestinationPath": "Caminho de destino", @@ -1073,7 +1073,7 @@ "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Programação", "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", - "QualityCutoffNotMet": "Corte da Qualidade ainda não foi alcançado", + "QualityCutoffNotMet": "Limite da qualidade ainda não foi alcançado", "QueueLoadError": "Falha ao carregar a fila", "RemoveQueueItem": "Remover - {sourceTitle}", "RemoveQueueItemConfirmation": "Tem certeza de que deseja remover '{sourceTitle}' da fila?", @@ -1083,7 +1083,7 @@ "AddImportListImplementation": "Adicionar lista de importação - {implementationName}", "AddANewPath": "Adicionar um novo caminho", "AddConnectionImplementation": "Adicionar conexão - {implementationName}", - "AddCustomFilter": "Adicionar Filtro Personalizado", + "AddCustomFilter": "Adicionar filtro personalizado", "AddDownloadClientImplementation": "Adicionar cliente de download - {implementationName}", "AddIndexerImplementation": "Adicionar indexador - {implementationName}", "AddToDownloadQueue": "Adicionar à fila de download", @@ -1094,10 +1094,10 @@ "AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName} ", "ChooseImportMode": "Escolha o modo de importação", "ClickToChangeEpisode": "Clique para alterar o episódio", - "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.", + "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente, ou você pode clicar em Recarregar abaixo.", "CountSelectedFiles": "{selectedCount} arquivos selecionados", "DefaultNotFoundMessage": "Você deve estar perdido, nada para ver aqui.", - "DeleteEpisodeFile": "Excluir Arquivo do Episódio", + "DeleteEpisodeFile": "Excluir arquivo do episódio", "DeleteSelectedEpisodeFilesHelpText": "Tem certeza de que deseja excluir os arquivos de episódios selecionados?", "EditConditionImplementation": "Editar condição - {implementationName}", "EditIndexerImplementation": "Editar indexador - {implementationName}", @@ -1128,18 +1128,18 @@ "ClickToChangeLanguage": "Clique para alterar o idioma", "ClickToChangeQuality": "Clique para alterar a qualidade", "ClickToChangeReleaseGroup": "Clique para alterar o grupo de lançamento", - "ClickToChangeSeason": "Clique para mudar a temporada", - "ClickToChangeSeries": "Clique para mudar de série", + "ClickToChangeSeason": "Clique para alterar a temporada", + "ClickToChangeSeries": "Clique para alterar a série", "ConnectionLost": "Conexão perdida", - "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisa ser recarregado para restaurar a funcionalidade.", "Continuing": "Continuando", "CountSelectedFile": "{selectedCount} arquivo selecionado", "CustomFilters": "Filtros personalizados", "DailyEpisodeTypeFormat": "Data ({format})", "Default": "Padrão", - "DeleteEpisodeFileMessage": "Tem certeza de que deseja excluir '{path}'?", + "DeleteEpisodeFileMessage": "Tem certeza de que deseja excluir \"{path}\"?", "DeleteEpisodeFromDisk": "Excluir episódio do disco", - "DeleteSelectedEpisodeFiles": "Excluir Arquivos de Episódios Selecionados", + "DeleteSelectedEpisodeFiles": "Excluir arquivos de episódios selecionados", "Donate": "Doar", "EditConnectionImplementation": "Editar Conexão - {implementationName}", "EditDownloadClientImplementation": "Editar cliente de download - {implementationName}", @@ -1340,13 +1340,13 @@ "SeriesIndexFooterMissingUnmonitored": "Episódios Ausentes (Série não monitorada)", "DefaultNameCopiedProfile": "{name} - Cópia", "DefaultNameCopiedSpecification": "{name} - Cópia", - "DeleteEpisodesFilesHelpText": "Excluir os arquivos do episódio e a pasta da série", - "DeleteSelectedSeries": "Excluir Séries Selecionadas", - "DeleteSeriesFolder": "Excluir Pasta da Série", + "DeleteEpisodesFilesHelpText": "Excluir os arquivos de episódios e a pasta da série", + "DeleteSelectedSeries": "Excluir séries selecionadas", + "DeleteSeriesFolder": "Excluir pasta da série", "DeleteSeriesFolderConfirmation": "A pasta da série `{path}` e todo o seu conteúdo serão excluídos.", - "DeleteSeriesFolderCountConfirmation": "Tem certeza de que deseja excluir {count} séries selecionadas?", - "DeleteSeriesFolderHelpText": "Exclua a pasta da série e seu conteúdo", - "DeleteSeriesFolders": "Excluir Pastas das Séries", + "DeleteSeriesFolderCountConfirmation": "Tem certeza de que deseja excluir as {count} séries selecionadas?", + "DeleteSeriesFolderHelpText": "Excluir a pasta da série e seu conteúdo", + "DeleteSeriesFolders": "Excluir pastas das séries", "DeleteSeriesModalHeader": "Excluir - {title}", "DeletedSeriesDescription": "A série foi excluída do TheTVDB", "DetailedProgressBarHelpText": "Mostrar texto na barra de progresso", @@ -1418,13 +1418,13 @@ "DeleteEpisodesFiles": "Excluir {episodeFileCount} arquivos de episódios", "NoMonitoredEpisodes": "Nenhum episódio monitorado nesta série", "ShowBanners": "Mostrar Banners", - "DeleteSeriesFolderCountWithFilesConfirmation": "Tem certeza de que deseja excluir {count} séries selecionadas e todos os conteúdos?", + "DeleteSeriesFolderCountWithFilesConfirmation": "Tem certeza de que deseja excluir as {count} séries selecionadas e todos os conteúdos?", "NoSeriesFoundImportOrAdd": "Nenhuma série encontrada. Para começar, você deseja importar sua série existente ou adicionar uma nova série.", "ShowBannersHelpText": "Mostrar banners em vez de títulos", - "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} arquivos de episódios totalizando {size}", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} arquivos de episódios, totalizando {size}", "OrganizeSelectedSeriesModalAlert": "Dica: para visualizar uma renomeação, selecione 'Cancelar', selecione qualquer título de série e use este ícone:", "ShowQualityProfileHelpText": "Mostrar perfil de qualidade abaixo do pôster", - "DeleteSeriesFoldersHelpText": "Excluir as pastas da série e todo o seu conteúdo", + "DeleteSeriesFoldersHelpText": "Excluir as pastas das séries e todos os conteúdos", "Total": "Total", "DetailedProgressBar": "Barra de progresso detalhada", "EndedSeriesDescription": "Não se espera mais episódios ou temporadas", @@ -1437,7 +1437,7 @@ "AuthenticationRequiredPasswordHelpTextWarning": "Digite uma nova senha", "AuthenticationRequiredUsernameHelpTextWarning": "Digite um novo nome de usuário", "CollapseAll": "Recolher tudo", - "ContinuingSeriesDescription": "Espera-se mais episódio ou outra temporada", + "ContinuingSeriesDescription": "Espera-se mais episódios ou outra temporada", "CountSeriesSelected": "{count} séries selecionadas", "MissingLoadError": "Erro ao carregar itens ausentes", "MissingNoItems": "Nenhum item ausente", @@ -1450,8 +1450,8 @@ "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atingido", "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar {appName} ou desabilitar todos os seus indexadores.", "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", - "SearchForCutoffUnmetEpisodes": "Pesquise todos os episódios que o corte não foi atingido", - "SearchForCutoffUnmetEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios de {totalRecords} corte não atingido?", + "SearchForCutoffUnmetEpisodes": "Pesquisar todos os episódios com limite não atingido", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Tem certeza que deseja pesquisar todos os episódios de {totalRecords} com limite não atingido?", "FormatAgeDay": "dia", "FormatAgeHours": "horas", "FormatDateTime": "{formattedDate} {formattedTime}", @@ -1488,7 +1488,7 @@ "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é \"{port}\"", "DownloadClientFreeboxUnableToReachFreeboxApi": "Não foi possível acessar a API do Freebox. Verifique o URL base e a versão na configuração \"URL da API\".", "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração KeepHistory do NzbGet deve ser menor que 25.000", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0, que impede que o {appName} veja os downloads concluídos.", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0, o que impede que o {appName} veja os downloads concluídos.", "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL do {clientName}, como {url}", "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se ao {clientName}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar os downloads, deixe em branco para usar o local padrão do Transmission", @@ -1504,7 +1504,7 @@ "UnknownDownloadState": "Estado de download desconhecido: {state}", "UsenetBlackhole": "Blackhole para Usenet", "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que torrents forçados não obedecem às restrições de semeadura", - "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download base do cliente, é possível que \"Manter pasta de nível superior\" esteja desabilitado para este torrent ou \"Layout de conteúdo de torrent\" NÃO esteja definido como 'Original' ou 'Criar subpasta'?", + "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download base do cliente, é possível que \"Manter pasta de nível superior\" esteja desabilitado para este torrent ou \"Layout de conteúdo de torrent\" NÃO esteja definido como \"Original\" ou \"Criar subpasta\"?", "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Não há categorias até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "O qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "O {appName} não poderá realizar o tratamento de download concluído conforme configurado. Para corrigir isso, no qBittorrent, acesse \"Ferramentas -> Opções... -> BitTorrent -> Limites de Semeadura\", e altere a opção de \"Remover\" para \"Parar\"", @@ -1527,7 +1527,7 @@ "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporte às versões de {minVersion} a {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "A pasta não existe", "DownloadClientDownloadStationValidationNoDefaultDestination": "Nenhum destino padrão", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do Download Station em BT/HTTP/FTP/NZB -> Localização.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do Download Station em BT/HTTP/FTP/NZB -> Location (Local).", "DownloadClientDownloadStationValidationSharedFolderMissing": "A pasta compartilhada não existe", "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionais", "DownloadClientFloodSettingsPostImportTags": "Etiquetas pós-importação", @@ -1543,15 +1543,15 @@ "DownloadClientFreeboxSettingsAppToken": "Token do aplicativo", "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API do Freebox (ou seja, \"app_token\")", "DownloadClientFreeboxSettingsHostHelpText": "Nome ou endereço IP do host do Freebox, o padrão é \"{url}\" (só funcionará se estiver na mesma rede)", - "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API do Freebox. Verifique as configurações \"Host\", \"Porta\" ou \"Usar SSL\". (Erro: {exceptionMessage})", + "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API do Freebox. Verifique as configurações \"Host\", \"Port\" (Porta) ou \"Use SSL\" (Usar SSL). (Erro: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "O download contém vários arquivos e não está em uma pasta de trabalho: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos a versão 16.0 do NzbGet", "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de rótulo não ativado", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "A configuração KeepHistory do NzbGet está muito alta.", "DownloadClientNzbgetValidationKeepHistoryZero": "A configuração KeepHistory do NzbGet deve ser maior que 0", - "DownloadClientPneumaticSettingsNzbFolder": "Pasta Nzb", - "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisará estar acessível no XBMC", - "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", + "DownloadClientPneumaticSettingsNzbFolder": "Pasta do Nzb", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisa estar acessível no XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Pasta do Strm", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Priorizar o primeiro e o último", "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última partes antes (qBittorrent 4.1.0+)", @@ -1620,7 +1620,7 @@ "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Em vez de mover arquivos, isso instruirá {appName} a copiar ou vincular (dependendo das configurações/configuração do sistema)", "TorrentBlackholeTorrentFolder": "Pasta do Torrent", "UseSsl": "Usar SSL", - "UsenetBlackholeNzbFolder": "Pasta Nzb", + "UsenetBlackholeNzbFolder": "Pasta do Nzb", "IndexerIPTorrentsSettingsFeedUrl": "URL do Feed", "IndexerIPTorrentsSettingsFeedUrlHelpText": "A URL completa do feed RSS gerado pelo IPTorrents, usando apenas as categorias que você selecionou (HD, SD, x264, etc...)", "IndexerSettingsAdditionalParameters": "Parâmetros Adicionais", @@ -1875,23 +1875,23 @@ "ImportListsTraktSettingsWatchedListFilter": "Filtrar Lista de Assistido", "ImportListsValidationUnableToConnectException": "Não foi possível conectar-se à lista de importação: {exceptionMessage}. Verifique o log em torno desse erro para obter detalhes.", "AutoTaggingSpecificationGenre": "Gênero(s)", - "AutoTaggingSpecificationMaximumYear": "Ano Máximo", - "AutoTaggingSpecificationMinimumYear": "Ano Mínimo", + "AutoTaggingSpecificationMaximumYear": "Ano máximo", + "AutoTaggingSpecificationMinimumYear": "Ano mínimo", "AutoTaggingSpecificationOriginalLanguage": "Idioma", - "AutoTaggingSpecificationQualityProfile": "Perfil de Qualidade", - "AutoTaggingSpecificationRootFolder": "Pasta Raiz", - "AutoTaggingSpecificationSeriesType": "Tipo de Série", - "AutoTaggingSpecificationStatus": "Estado", + "AutoTaggingSpecificationQualityProfile": "Perfil de qualidade", + "AutoTaggingSpecificationRootFolder": "Pasta raiz", + "AutoTaggingSpecificationSeriesType": "Tipo de série", + "AutoTaggingSpecificationStatus": "Status", "CustomFormatsSpecificationLanguage": "Idioma", - "CustomFormatsSpecificationMaximumSize": "Tamanho Máximo", + "CustomFormatsSpecificationMaximumSize": "Tamanho máximo", "CustomFormatsSpecificationMaximumSizeHelpText": "O lançamento deve ser menor ou igual a este tamanho", - "CustomFormatsSpecificationMinimumSize": "Tamanho Mínimo", + "CustomFormatsSpecificationMinimumSize": "Tamanho mínimo", "CustomFormatsSpecificationMinimumSizeHelpText": "O lançamento deve ser maior que esse tamanho", "CustomFormatsSpecificationRegularExpression": "Expressão regular (regex)", "CustomFormatsSpecificationRegularExpressionHelpText": "O regex do formato personalizado não diferencia maiúsculas e minúsculas", - "CustomFormatsSpecificationReleaseGroup": "Grupo do Lançamento", + "CustomFormatsSpecificationReleaseGroup": "Grupo do lançamento", "CustomFormatsSpecificationResolution": "Resolução", - "CustomFormatsSpecificationSource": "Fonte", + "CustomFormatsSpecificationSource": "Origem", "ImportListsAniListSettingsAuthenticateWithAniList": "Autenticar com AniList", "ImportListsAniListSettingsImportCancelled": "Importação Cancelada", "ImportListsAniListSettingsImportCancelledHelpText": "Mídia: Série foi cancelada", @@ -2008,7 +2008,7 @@ "BlocklistMultipleOnlyHint": "Adicionar à lista de bloqueio sem procurar por substitutos", "BlocklistOnly": "Apenas adicionar à lista de bloqueio", "BlocklistAndSearchMultipleHint": "Iniciar pesquisas por substitutos após adicionar à lista de bloqueio", - "BlocklistReleaseHelpText": "Impede que esta versão seja baixada novamente pelo {appName} via RSS ou Pesquisa automática", + "BlocklistReleaseHelpText": "Impede que este lançamento seja baixado novamente pelo {appName} via RSS ou Pesquisa automática", "ChangeCategoryHint": "Altera o download para a \"Categoria pós-importação\" do cliente de download", "ChangeCategoryMultipleHint": "Altera os downloads para a \"Categoria pós-importação' do cliente de download", "DatabaseMigration": "Migração de banco de dados", @@ -2031,24 +2031,24 @@ "ListSyncTag": "Etiqueta de Sincronização de Lista", "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", "LogOnly": "Só registro em log", - "CleanLibraryLevel": "Limpar nível da biblioteca", + "CleanLibraryLevel": "Nível de limpeza da biblioteca", "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente.", "ClickToChangeIndexerFlags": "Clique para alterar os sinalizadores do indexador", "SelectIndexerFlags": "Selecionar Sinalizadores do Indexador", "SetIndexerFlagsModalTitle": "{modalTitle} - Definir Sinalizadores do Indexador", - "CustomFormatsSpecificationFlag": "Sinalizar", + "CustomFormatsSpecificationFlag": "Sinalizador", "IndexerFlags": "Sinalizadores do indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado", - "CustomFilter": "Filtro Personalizado", + "CustomFilter": "Filtro personalizado", "Filters": "Filtros", "Label": "Rótulo", "LabelIsRequired": "Rótulo é requerido", "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", "ReleaseType": "Tipo de Lançamento", - "DownloadClientDelugeSettingsDirectory": "Diretório de Download", - "DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído", + "DownloadClientDelugeSettingsDirectory": "Diretório de download", + "DownloadClientDelugeSettingsDirectoryCompleted": "Diretório para mover 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", @@ -2067,7 +2067,7 @@ "IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?", "AutoTaggingSpecificationTag": "Etiqueta", "IndexerSettingsMultiLanguageRelease": "Multi Idiomas", - "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está relatando arquivos perdidos", + "DownloadClientQbittorrentTorrentStateMissingFiles": "O qBittorrent está relatando arquivos ausentes", "BlocklistFilterHasNoItems": "O filtro selecionado para a lista de bloqueio não contém itens", "DayOfWeekAt": "{day} às {time}", "TodayAt": "Hoje às {time}", @@ -2075,12 +2075,12 @@ "HasUnmonitoredSeason": "Tem Temporada Não Monitorada", "YesterdayAt": "Ontem às {time}", "UnableToImportAutomatically": "Não foi possível importar automaticamente", - "CustomColonReplacement": "Substituto de Dois Pontos Personalizado", - "CustomColonReplacementFormatHint": "Caractere válido do sistema de arquivos, como dois pontos (letra)", + "CustomColonReplacement": "Personalizar substituto do dois-pontos", + "CustomColonReplacementFormatHint": "Caractere válido do sistema de arquivos, como dois-pontos (letra)", "NotificationsPlexSettingsServerHelpText": "Selecione o servidor da conta plex.tv após a autenticação", "OnFileImport": "Ao Importar o Arquivo", "OnImportComplete": "Ao Completar Importação", - "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição aos dois pontos", + "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição ao sinal de dois-pontos", "NotificationsPlexSettingsServer": "Servidor", "OnFileUpgrade": "Ao Atualizar o Arquivo", "CountVotes": "{votes} votos", @@ -2099,8 +2099,8 @@ "NoBlocklistItems": "Sem itens na lista de bloqueio", "NotificationsTelegramSettingsMetadataLinks": "Links de Metadados", "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações", - "DeleteSelected": "Excluir Selecionado", - "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja excluir as listas de importação selecionadas das exclusões?", + "DeleteSelected": "Excluir selecionado(s)", + "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja remover as exclusões de lista de importação selecionadas?", "LogSizeLimit": "Limite de Tamanho do Registro", "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.", "DeleteSelectedCustomFormats": "Excluir formato(s) personalizado(s)", @@ -2111,8 +2111,8 @@ "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)", "LastSearched": "Última Pesquisa", "SkipFreeSpaceCheckHelpText": "Usar quando {appName} não consegue detectar espaço livre em sua pasta raiz", - "CustomFormatsSpecificationExceptLanguage": "Exceto Idioma", - "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponde se qualquer idioma diferente do idioma selecionado estiver presente", + "CustomFormatsSpecificationExceptLanguage": "Exceto idioma", + "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponde se qualquer idioma diferente do selecionado estiver presente", "MinimumCustomFormatScoreIncrement": "Incremento Mínimo da Pontuação de Formato Personalizado", "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que {appName} considere isso uma atualização", "NotificationsGotifySettingsMetadataLinks": "Links de metadados", @@ -2132,9 +2132,9 @@ "FavoriteFolderRemove": "Remover Pasta Favorita", "FavoriteFolders": "Pastas Favoritas", "Fallback": "Reserva", - "CutoffNotMet": "Corte Não Alcançado", + "CutoffNotMet": "Limite não atingido", "Premiere": "Estreia", - "Completed": "Completado", + "Completed": "Concluído", "Menu": "Menu", "NotificationsSettingsWebhookHeaders": "Cabeçalhos", "UpdatePath": "Caminho da Atualização", @@ -2144,5 +2144,8 @@ "IndexerSettingsFailDownloads": "Downloads com Falhas", "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará esses tipos de arquivos selecionados como downloads com falha.", "NotificationsTelegramSettingsIncludeInstanceName": "Incluir nome da instância no título", - "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcionalmente, inclua o nome da instância na notificação" + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcionalmente, inclua o nome da instância na notificação", + "ReleasePush": "Impulsionar Lançamento", + "ReleaseSource": "Fonte do Lançamento", + "UserInvokedSearch": "Pesquisa Invocada pelo Usuário" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index e7edf7d80..94b981dca 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -958,7 +958,7 @@ "DownloadClientRootFolderHealthCheckMessage": "İndirme istemcisi {downloadClientName}, indirmeleri kök klasöre yerleştirir {rootFolderPath}. Bir kök klasöre indirmemelisiniz.", "DeleteBackup": "Yedeklemeyi Sil", "CustomColonReplacementFormatHelpText": "İki nokta üst üste yerine kullanılacak karakterler", - "CustomColonReplacement": "Özel Kolon Değişimi", + "CustomColonReplacement": "İki Nokta Üst Üste İşareti İçin Özel Değiştirme", "CustomColonReplacementFormatHint": "İki Nokta (Harf) gibi geçerli dosya sistemi karakteri", "CustomFormatsSpecificationReleaseGroup": "Yayın Grubu", "CustomFormatsSpecificationResolution": "Çözünürlük", @@ -1188,7 +1188,7 @@ "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} bölüm dosyası toplamı {size}", "DestinationPath": "Hedef yol", "Disabled": "Devre dışı", - "ColonReplacementFormatHelpText": "{appName}'ın kolon değişimini nasıl işlediğini değiştirin", + "ColonReplacementFormatHelpText": "{appName} uygulamasının iki nokta üst üste işaretini değiştirme ayarı", "DeleteTag": "Etiketi Sil", "NamingSettingsLoadError": "Adlandırma ayarları yüklenemiyor", "NotificationTriggers": "Bildirim Tetikleyicileri", From a840bb542362d58006b6cc27affd58ee6b965b80 Mon Sep 17 00:00:00 2001 From: jcassette <julien.cassette@gmail.com> Date: Sat, 18 Jan 2025 04:55:37 +0100 Subject: [PATCH 741/762] New: reflink support for ZFS --- src/NzbDrone.Common/Disk/DiskTransferService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index 2da930a78..f89c31b21 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -341,10 +341,11 @@ namespace NzbDrone.Common.Disk var isCifs = targetDriveFormat == "cifs"; var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs"; + var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs"; if (mode.HasFlag(TransferMode.Copy)) { - if (isBtrfs) + if (isBtrfs || isZfs) { if (_diskProvider.TryCreateRefLink(sourcePath, targetPath)) { @@ -358,7 +359,7 @@ namespace NzbDrone.Common.Disk if (mode.HasFlag(TransferMode.Move)) { - if (isBtrfs) + if (isBtrfs || isZfs) { if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath)) { From fe8478f42ab6b90089e6fc66170117b6f676e83b Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:33:15 +0200 Subject: [PATCH 742/762] Fix translation key for RSS in History Details --- frontend/src/Activity/History/Details/HistoryDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Activity/History/Details/HistoryDetails.tsx b/frontend/src/Activity/History/Details/HistoryDetails.tsx index ae2ec4a66..f460ec433 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.tsx +++ b/frontend/src/Activity/History/Details/HistoryDetails.tsx @@ -61,7 +61,7 @@ function HistoryDetails(props: HistoryDetailsProps) { releaseSourceMessage = translate('Unknown'); break; case 'Rss': - releaseSourceMessage = translate('RSS'); + releaseSourceMessage = translate('Rss'); break; case 'Search': releaseSourceMessage = translate('Search'); From 87934c77614a60e2f53cf9dbeb553b0a4928977a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 13 Jan 2025 03:05:27 +0200 Subject: [PATCH 743/762] Fix typo in logging for custom format score --- .../Specifications/CustomFormatAllowedByProfileSpecification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs index 9a5023977..eede06198 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return DownloadSpecDecision.Reject(DownloadRejectionReason.CustomFormatMinimumScore, "Custom Formats {0} have score {1} below Series profile minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore); } - _logger.Trace("Custom Format Score of {0} [{1}] above Series profile minumum {2}", score, subject.CustomFormats.ConcatToString(), minScore); + _logger.Trace("Custom Format Score of {0} [{1}] above Series profile minimum {2}", score, subject.CustomFormats.ConcatToString(), minScore); return DownloadSpecDecision.Accept(); } From 6dae2f0d8411dc1e1366a4cc0104533ff4b704a5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 18 Jan 2025 05:57:13 +0200 Subject: [PATCH 744/762] Fixed: Images after series are updated via Series Editor --- src/Sonarr.Api.V3/Series/SeriesController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 73c95aafd..bbbcdd0d5 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -329,7 +329,7 @@ namespace Sonarr.Api.V3.Series { foreach (var series in message.Series) { - BroadcastResourceChange(ModelAction.Deleted, series.ToResource()); + BroadcastResourceChange(ModelAction.Deleted, GetSeriesResource(series, false)); } } @@ -344,7 +344,7 @@ namespace Sonarr.Api.V3.Series { foreach (var series in message.Series) { - BroadcastResourceChange(ModelAction.Updated, series.ToResource()); + BroadcastResourceChange(ModelAction.Updated, GetSeriesResource(series, false)); } } From c69db1ff92a54dfefd03d1be2dbdbb7358fe9856 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 18 Jan 2025 05:57:52 +0200 Subject: [PATCH 745/762] New: Parsing titles with AKA separating multiple titles Closes #7576 --- src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 18259eaff..8a96e7a98 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -96,6 +96,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")] [TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")] + [TestCase("Босх: Спадок AKA Series: Legacy S02 1080p NF WEB-DL Dual- Audio DD+ 5.1 Atmos H.264-APEX", "Босх: Спадок", "Series: Legacy")] + [TestCase("Босх.Спадок.AKA.Series.Legacy.S02.1080p.NF.WEB-DL.DUAL.DDP5.1.Atmos.H.264-APEX", "Босх Спадок", "Series Legacy")] public void should_parse_multiple_series_titles(string postTitle, params string[] titles) { var seriesTitleInfo = Parser.Parser.ParseTitle(postTitle).SeriesTitleInfo; diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 8cf934131..f94aa081a 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -575,7 +575,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex TitleComponentsRegex = new Regex(@"^(?:(?<title>.+?) \((?<title>.+?)\)|(?<title>.+?) \| (?<title>.+?))$", + private static readonly Regex TitleComponentsRegex = new Regex(@"^(?:(?<title>.+?) \((?<title>.+?)\)|(?<title>.+?) \| (?<title>.+?)|(?<title>.+?) AKA (?<title>.+?))$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex PartRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled); From 2ac139ab4db930b53a55a9194565d21f95b2523b Mon Sep 17 00:00:00 2001 From: kephasdev <160031725+kephasdev@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:32:09 -0500 Subject: [PATCH 746/762] Fixed: Augmenting languages for releases with MULTI and other languages (cherry picked from commit d58135bf1754b6185eef19a2f4069b27a918d01e) --- .../Aggregators/AggregateLanguagesFixture.cs | 44 +++++++++++++++++++ .../Aggregators/AggregateLanguages.cs | 11 ++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs index ac23ade33..0f2759bd2 100644 --- a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -168,6 +168,50 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); } + [Test] + public void should_return_multi_languages_when_release_as_specified_language_and_indexer_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.VFF.VFQ.1080p.BluRay.DTS.HDMA.x264-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Find(1)) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.French }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_when_release_as_other_language_and_indexer_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.GERMAN.1080p.BluRay.DTS.HDMA.x264-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock<IIndexerFactory>() + .Setup(v => v.Find(1)) + .Returns(indexerDefinition); + + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.German }, releaseTitle); + _remoteEpisode.Release.IndexerId = 1; + _remoteEpisode.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French, Language.German }); + Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once()); + Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls(); + } + [Test] public void should_return_original_when_indexer_has_no_multi_languages_configuration() { diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs index afa96dd1c..f08b5c7a5 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators languages = languages.Except(languagesToRemove).ToList(); } - if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo?.Title?.IsNotNullOrWhiteSpace() == true) + if (releaseInfo?.Title?.IsNotNullOrWhiteSpace() == true) { IndexerDefinition indexer = null; @@ -93,7 +93,14 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators if (indexer?.Settings is IIndexerSettings settings && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(releaseInfo.Title)) { // Use indexer setting for Multi-languages - languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) + { + languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + } + else + { + languages.AddRange(settings.MultiLanguages.Select(i => (Language)i).Except(languages).ToList()); + } } } From 970df1a1d897723054e9e3d24386e6556639c22c Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 24 Jan 2025 19:33:24 +0000 Subject: [PATCH 747/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dani Talens <databio@gmail.com> Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com> Co-authored-by: Georgi Panov <darkfella91@gmail.com> Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/bg/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/bg.json | 107 ++- src/NzbDrone.Core/Localization/Core/ca.json | 10 +- src/NzbDrone.Core/Localization/Core/fi.json | 88 +- .../Localization/Core/pt_BR.json | 818 +++++++++--------- src/NzbDrone.Core/Localization/Core/ru.json | 6 +- 5 files changed, 547 insertions(+), 482 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index 278700c60..aec80ef49 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -5,7 +5,7 @@ "AddDownloadClientImplementation": "Добави клиент за изтегляне - {implementationName}", "AddExclusion": "Добави изключение", "AddImportList": "Добави списък за импортиране", - "AddIndexer": "Добави индексатор", + "AddIndexer": "Добавете индексатор", "AirsTomorrowOn": "Утре от {time} по {networkLabel}", "AddedToDownloadQueue": "Добавен към опашката за изтегляне", "AfterManualRefresh": "След ръчно опресняване", @@ -17,10 +17,10 @@ "Any": "Всякакви", "AddConditionImplementation": "Добави условие - {implementationName}", "AddConnectionImplementation": "Добави връзка - {implementationName}", - "AddListExclusion": "Добави изключение от списъка", + "AddListExclusion": "Добавете изключение от списъка", "AddImportListExclusion": "Добави изключение от списъка за импортиране", "AddImportListExclusionError": "Не може да се добави ново изключение от списъка за импортиране, моля, опитайте отново.", - "AddListExclusionSeriesHelpText": "Предотврати добавянето на сериили в {appName} посредством списъци", + "AddListExclusionSeriesHelpText": "Предотвратете добавянето на сериали в {appName} чрез списъци", "AnimeEpisodeTypeFormat": "Абсолютен номер на епизода ({format})", "AddRootFolderError": "Не може да се добави основна папка, моля, опитайте отново", "AnimeEpisodeTypeDescription": "Епизоди, издадени с абсолютен номер на епизод", @@ -28,63 +28,116 @@ "AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките към сървърите на {appName}. Това включва информация за вашия браузър, кои страници на уеб интерфейса на {appName} използвате, докладваните грешки, както и версията на операционната система и изпълнителната среда. Ще използваме тази информация, за да приоритизираме нови функции и поправки на бъгове.", "Anime": "Аниме", "AddIndexerError": "Не може да се добави нов индексатор, моля, опитайте отново.", - "AddIndexerImplementation": "Добави индексатор - {implementationName}", + "AddIndexerImplementation": "Добавете индексатор - {implementationName}", "AddDelayProfileError": "Не може да се добави нов профил за забавяне, моля, опитайте отново.", "AddNotificationError": "Не може да се добави ново известие, моля, опитайте отново.", "AddImportListImplementation": "Добави списък за импортиране - {implementationName}", - "AddList": "Добави списък", - "AddNewSeriesSearchForMissingEpisodes": "Започни търсене на липсващи епизоди", - "AddRemotePathMapping": "Добави мапиране към отдалечен път", + "AddList": "Добавете списък", + "AddNewSeriesSearchForMissingEpisodes": "Започнете търсене на липсващи епизоди", + "AddRemotePathMapping": "Добавете мапиране към отдалечен път", "AddRemotePathMappingError": "Не може да се добави ново мапиране към отдалечен път, моля, опитайте отново.", - "AddToDownloadQueue": "Добави към опашката за изтегляне", + "AddToDownloadQueue": "Добавете към опашката за изтегляне", "AlreadyInYourLibrary": "Вече е във вашата библиотека", "AnEpisodeIsDownloading": "Изтегля се епизод", - "AnimeEpisodeFormat": "Аниме Епизод Формат", + "AnimeEpisodeFormat": "Формат на Аниме епизодите", "ApiKey": "API ключ", "Added": "Добавен", - "ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте ключа си за API, за да бъде с дължина най-малко {length} знака. Може да направите това чрез настройките или конфигурационния файл", + "ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте вашия API ключ, за да бъде с дължина най-малко {length} знака. Може да направите това чрез настройките или конфигурационния файл", "AddConditionError": "Не може да се добави новo условие, моля, опитайте отново.", "AddAutoTagError": "Не може да се добави нов автоматичен таг, моля, опитайте отново.", "AddConnection": "Добави връзка", - "AddCustomFormat": "Добави персонализиран формат", + "AddCustomFormat": "Добавете персонализиран формат", "AddCustomFormatError": "Не може да се добави нов персонализиран формат, моля, опитайте отново.", - "AddDelayProfile": "Добави профил за забавяне", + "AddDelayProfile": "Добавете профил за забавяне", "AddDownloadClient": "Добави клиент за изтегляне", "AddDownloadClientError": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.", "AddListExclusionError": "Не може да се добави ново изключение от списъка, моля, опитайте отново.", - "AddNewRestriction": "Добави новo ограничение", + "AddNewRestriction": "Добавете новo ограничение", "AddListError": "Не може да се добави нов списък, моля, опитайте отново.", - "AddQualityProfile": "Добави профил за качество", + "AddQualityProfile": "Добавете профил за качество", "AddQualityProfileError": "Не може да се добави нов профил за качество, моля, опитайте отново.", - "AddReleaseProfile": "Добави профил за издания", + "AddReleaseProfile": "Добавете профил за издания", "Always": "Винаги", - "AnalyseVideoFiles": "Анализирай видео файлове", + "AnalyseVideoFiles": "Анализирайте видео файловете", "Analytics": "Анализ", "AgeWhenGrabbed": "Възраст (при грабване)", - "AddAutoTag": "Добави автоматичен етикет", + "AddAutoTag": "Добави автоматичен таг", "AddCondition": "Добави условие", "AirDate": "Ефирна дата", "AllTitles": "Всички заглавия", - "AddRootFolder": "Добави основна папка", - "Add": "Добави", - "AddingTag": "Добавяне на етикет", + "AddRootFolder": "Добавете основна папка", + "Add": "Добавяне", + "AddingTag": "Добавяне на таг", "Age": "Възраст", "All": "Всички", - "Activity": "Активност", - "AddNew": "Добави нов", + "Activity": "Дейност", + "AddNew": "Добавете нов", "Actions": "Действия", "About": "Относно", "Agenda": "Агенда", - "AddNewSeries": "Добави нов сериал", + "AddNewSeries": "Добавете нов сериал", "AddNewSeriesError": "Неуспешно зареждане на резултатите от търсенето, моля, опитайте отново.", - "AddNewSeriesHelpText": "Добавянето на нови сериали е лесно, започнете, като напишете името на сериала, който искате да добавите.", - "AddNewSeriesRootFolderHelpText": "'{folder}' подпапка ще бъде създадена автоматично", + "AddNewSeriesHelpText": "Лесно е да добавите нов сериал, просто започнете да въвеждате името на сериала, който искате да добавите.", + "AddNewSeriesRootFolderHelpText": "Подпапката '{folder}' ще бъде създадена автоматично", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Започни търсене на епизоди, които не са достигнали максималното качество за надграждане", - "AddSeriesWithTitle": "Добави {title}", + "AddSeriesWithTitle": "Добавете {title}", "Absolute": "Абсолютен", "AllSeriesAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", "AllSeriesInRootFolderHaveBeenImported": "Всички сериали в {path} са импортирани", "AbsoluteEpisodeNumber": "Абсолютен епизоден номер", "AllResultsAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", - "AppDataDirectory": "Директория на AppData" + "AppDataDirectory": "Директория на приложението", + "SeasonFolder": "Папка ( Сезони )", + "SeasonDetails": "Детайли за сезона", + "SeasonCount": "Брой сезони", + "SslPort": "SSL порт", + "AppDataLocationHealthCheckMessage": "Актуализирането няма да бъде възможно, за да се предотврати изтриването на папката на приложението по време на актуализацията", + "AppUpdated": "{appName} Актуализиран", + "ApplyTagsHelpTextReplace": "Замяна: Заменете таговете с въведените тагове (не въвеждайте тагове, за да изчистите всички тагове)", + "AudioLanguages": "Аудио езици", + "AuthBasic": "Основно (изскачащ прозорец на браузъра)", + "AuthForm": "Формуляри (Страница за вход)", + "AuthenticationMethodHelpText": "Изисквайте потребителско име и парола за достъп до {appName}", + "AuthenticationRequiredPasswordHelpTextWarning": "Въведете нова парола", + "ApplicationURL": "URL адрес на приложението", + "AuthenticationMethodHelpTextWarning": "Моля, изберете валиден метод за удостоверяване", + "AuthenticationRequiredUsernameHelpTextWarning": "Въведете ново потребителско име", + "AuthenticationRequiredWarning": "За да предотврати отдалечен достъп без удостоверяване, {appName} вече изисква удостоверяването да бъде активирано. По желание можете да деактивирате удостоверяването от локални адреси.", + "SeriesFolderFormat": "Формат на папката ( Сериали )", + "ApplyChanges": "Прилагане на промените", + "AutoTaggingLoadError": "Не може да се зареди автоматичното маркиране", + "SeasonFinale": "Финал на сезона", + "AppUpdatedVersion": "{appName} е актуализиран до версия `{version}`, за да получите най-новите промени, ще трябва да презаредите {appName} ", + "ApplicationUrlHelpText": "Външният URL на това приложение, включително http(s)://, порт и базов URL", + "AutoTagging": "Автоматично маркиране", + "Apply": "Приложете", + "ApplyTags": "Прилагане на тагове", + "AutoAdd": "Автоматично добавяне", + "ApplyTagsHelpTextHowToApplyImportLists": "Как да добавите тагове към избраните списъци за импортиране", + "ApplyTagsHelpTextHowToApplySeries": "Как да добавите тагове към избраните сериали", + "SeasonFolderFormat": "Формат на папката ( Сезони )", + "AudioInfo": "Аудио информация", + "Season": "Сезон", + "ApplyTagsHelpTextAdd": "Добавяне: Добавете маркерите към съществуващия списък с маркери", + "ApplyTagsHelpTextRemove": "Премахване: Премахнете въведените тагове", + "RenameEpisodesHelpText": "{appName} ще използва съществуващото име на файла, ако преименуването е деактивирано", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Как да приложите тагове към избраните приложения за сваляне", + "Authentication": "Удостоверяване", + "AuthenticationRequiredHelpText": "Променете за кои заявки се изисква удостоверяване. Не променяйте, освен ако не разбирате рисковете.", + "AutoRedownloadFailed": "Неуспешно повторно изтегляне", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматично търсене и опит за изтегляне на различна версия, когато неуспешната версия е била взета от интерактивно търсене", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Потвърдете новата парола", + "AutoTaggingNegateHelpText": "Ако е отметнато, правилото за автоматично маркиране няма да се приложи, ако това условие {implementationName} съвпада.", + "AutoRedownloadFailedFromInteractiveSearch": "Неуспешно повторно изтегляне от интерактивното търсене", + "AutoRedownloadFailedHelpText": "Автоматично търсене и опит за сваляне на различна версия", + "AptUpdater": "Използвайте apt, за да инсталирате актуализацията", + "ApplyTagsHelpTextHowToApplyIndexers": "Как да добавите тагове към избраните индексатори", + "AuthenticationMethod": "Метод за удостоверяване", + "AuthenticationRequired": "Изисква се удостоверяване", + "RenameEpisodes": "Преименуване на епизоди", + "Standard": "Стандартен", + "StandardEpisodeFormat": "Формат на епизода ( Стандартен )", + "SslCertPathHelpText": "Път до \"pfx\" файл", + "EpisodeNaming": "Именуване на епизоди", + "Close": "Затвори" } diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 2e2aab539..e006ffbd5 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -750,5 +750,13 @@ "Organize": "Organitza", "Search": "Cerca", "SelectDropdown": "Seleccioneu...", - "Shutdown": "Apaga" + "Shutdown": "Apaga", + "ClickToChangeReleaseType": "Feu clic per canviar el tipus de llançament", + "BlocklistFilterHasNoItems": "El filtre de la llista de bloqueig seleccionat no conté elements", + "CustomColonReplacement": "Reemplaçament personalitzat de dos punts", + "CountVotes": "{votes} vots", + "Completed": "Completat", + "ContinuingOnly": "Només en emissió", + "CleanLibraryLevel": "Neteja el nivell de la llibreria", + "CountCustomFormatsSelected": "{count} format(s) personalitzat(s) seleccionat(s)" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index c80d577d6..509e81afe 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -77,7 +77,7 @@ "AddImportListExclusion": "Lisää tuontilistapoikkeus", "AddDownloadClientError": "Latauspalvelun lisääminen epäonnistui. Yritä uudelleen.", "AddExclusion": "Lisää poikkeussääntö", - "AddIndexerError": "Virhe lisättäessä tietolähdettä. Yritä uudelleen.", + "AddIndexerError": "Virhe lisättäessä hakupalvelua. Yritä uudelleen.", "AddList": "Lisää lista", "AddIndexer": "Lisää tietolähde", "AddCondition": "Lisää ehto", @@ -93,7 +93,7 @@ "AddANewPath": "Lisää uusi polku", "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", "AddDownloadClientImplementation": "Lisätään latauspalvelua – {implementationName}", - "AddImportListExclusionError": "Virhe lisättäessä tuontilistapokkeusta. Yritä uudelleen.", + "AddImportListExclusionError": "Virhe lisättäessä listapoikkeusta. Yritä uudelleen.", "AddIndexerImplementation": "Lisätään tietolähdettä – {implementationName}", "CalendarOptions": "Kalenterin asetukset", "BlocklistReleases": "Lisää julkaisut estolistalle", @@ -102,7 +102,7 @@ "ImportMechanismHandlingDisabledHealthCheckMessage": "Käytä valmistuneiden latausten käsittelyä", "Remove": "Poista", "RemoveFromDownloadClient": "Poista latauspalvelusta", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Viestintä latauspalvelun \"{downloadClientName}\" kanssa epäonnistui. {errorMessage}", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Virhe viestittäessä latauspalvelun \"{downloadClientName}\" kanssa. {errorMessage}.", "AnimeEpisodeFormat": "Animejaksojen kaava", "CheckDownloadClientForDetails": "katso lisätietoja latauspalvelusta", "Donations": "Lahjoitukset", @@ -142,7 +142,7 @@ "BypassDelayIfHighestQuality": "Ohita, jos korkein laatu", "CancelPendingTask": "Haluatko varmasti perua odottavan tehtävän?", "Clear": "Tyhjennä", - "CollectionsLoadError": "Kokoelmien lataus epäonnistui", + "CollectionsLoadError": "Virhe ladattaessa kokoelmia.", "CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot", "CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.", "CustomFormatsLoadError": "Mukautettujen muotojen lataus epäonnistui", @@ -199,7 +199,7 @@ "IndexerSettingsSeedRatio": "Jakosuhde", "IndexerSettingsWebsiteUrl": "Verkkosivuston URL", "IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", - "IndexersLoadError": "Tietolähteiden lataus epäonnistui", + "IndexersLoadError": "Virhe ladattaessa hakupalveluita.", "IndexersSettingsSummary": "Hakupalvelut ja julkaisurajoitukset.", "Indexers": "Tietolähteet", "KeyboardShortcutsFocusSearchBox": "Kohdista hakukenttä", @@ -219,7 +219,7 @@ "Missing": "Puuttuu", "MonitorMissingEpisodes": "Puuttuvat jaksot", "MissingEpisodes": "Puuttuvia jaksoja", - "MonitorNewSeasons": "Valvo uusia kausia", + "MonitorNewSeasons": "Uusien kausien valvonta", "MonitorLastSeasonDescription": "Valvo kaikkia viimeisen kauden jaksoja.", "MonitorNewSeasonsHelpText": "Uusien kausien automaattivalvonnan käytäntö.", "MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjakansiot kohteeseen \"{destinationRootFolder}\"?", @@ -239,7 +239,7 @@ "ReleaseSceneIndicatorUnknownSeries": "Tuntematon jakso tai sarja.", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", "RemoveCompletedDownloads": "Poista valmistuneet lataukset", - "RemotePathMappingsLoadError": "Etäsijaintien kohdistusten lataus epäonnistui", + "RemotePathMappingsLoadError": "Virhe ladattaessa etäsijaintien kohdistuksia.", "RemoveFailedDownloads": "Poista epäonnistuneet lataukset", "RemoveFailed": "Poisto epäonnistui", "RemoveFromBlocklist": "Poista estolistalta", @@ -247,7 +247,7 @@ "RemoveFromQueue": "Poista jonosta", "RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, {appName} käyttää nykyistä tiedostonimeä.", "RestoreBackup": "Palauta varmuuskopio", - "RestrictionsLoadError": "Rajoitusten lataus epäonnistui", + "RestrictionsLoadError": "Virhe ladattaessa rajoituksia.", "SceneInfo": "Kohtaustiedot", "SceneInformation": "Kohtaustiedot", "SelectFolderModalTitle": "{modalTitle} – Valitse kansio", @@ -271,7 +271,7 @@ "OnLatestVersion": "Uusin {appName}-versio on jo asennettu", "OnSeriesDelete": "Kun sarja poistetaan", "PrioritySettings": "Painotus: {priority}", - "QualitiesLoadError": "Laatujen lataus epäonnistui", + "QualitiesLoadError": "Virhe ladattaessa laatuja.", "QualityProfiles": "Laatuprofiilit", "QualityProfileInUseSeriesListCollection": "Sarjaan, listaan tai kokoelmaan liitettyä laatuprofiilia ei ole mahdollista poistaa.", "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", @@ -320,11 +320,11 @@ "StandardEpisodeTypeFormat": "Kausien ja jaksojen numerointi ({format})", "StartupDirectory": "Käynnistyskansio", "Started": "Alkoi", - "SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä palveluista painamalla niiden ohessa olevia lisätietopainikkeita.", + "SupportedIndexersMoreInfo": "Saat lisätietoja yksittäisistä palveluista niiden ohessa olevilla painikkeilla.", "Status": "Tila", "SupportedListsSeries": "{appName} tukee useita listoja, joiden avulla sarjoja voidaan tuoda tietokantaan.", "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.", - "TagsLoadError": "Tunnisteiden lataus epäonnistui", + "TagsLoadError": "Virhe ladattaessa tunnisteita.", "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tomorrow": "Huomenna", "TestParsing": "Testaa jäsennystä", @@ -429,7 +429,7 @@ "EditDownloadClientImplementation": "Muokataan latauspalvelua – {implementationName}", "EditImportListImplementation": "Muokataan tuontilistaa – {implementationName}", "EndedOnly": "Vain päättyneet", - "EnableInteractiveSearchHelpTextWarning": "Tämä hakupalvelu ei tue hakua.", + "EnableInteractiveSearchHelpTextWarning": "Tämä hakupalvelu ei tue hakutoimintoa.", "Episode": "Jakso", "EpisodeCount": "Jaksomäärä", "EpisodeAirDate": "Jakson esitysaika", @@ -458,10 +458,10 @@ "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.", "IndexerStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi", "LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä latausten tuontiin latauspalvelulta. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.", - "LibraryImportTips": "Muutama vinkki, joilla homma sujuu:", + "LibraryImportTips": "Muutama vinkki, joiden avulla homma sujuu:", "ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein", - "ListExclusionsLoadError": "Listapoikkeusten lataus epäonnistui", - "ManualImportItemsLoadError": "Manuaalituonnin kohteiden lataus epäonnistui", + "ListExclusionsLoadError": "Virhe ladattaessa listapoikkeuksia.", + "ManualImportItemsLoadError": "Virhe ladattaessa manuaalisesti tuotavia kohteita.", "MediaManagementSettingsSummary": "Tiedostojen nimeämis- ja hallinta-asetukset, sekä kirjaston juurikansiot.", "Message": "Viesti", "MetadataSettings": "Metatietoasetukset", @@ -483,7 +483,7 @@ "MonitorPilotEpisodeDescription": "Valvo vain ensimmäisen kauden ensimmäistä jaksoa.", "Name": "Nimi", "NamingSettings": "Nimeämisasetukset", - "NoEpisodeHistory": "Jaksohistoriaa ei ole", + "NoEpisodeHistory": "Jaksolle ei ole historiaa.", "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, kooltaan yhteensä {size}.", "DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?", "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.", @@ -559,7 +559,7 @@ "Tags": "Tunnisteet", "ToggleUnmonitoredToMonitored": "Ei valvota (aloita painamalla)", "TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).", - "UiSettingsLoadError": "Käyttöliittymäasetusten lataus epäonnistui", + "UiSettingsLoadError": "Virhe ladattaessa käyttöliittymäasetuksia.", "UnableToUpdateSonarrDirectly": "{appName}ia ei voida päivittää suoraan,", "UnmonitoredOnly": "Vain valvomattomat", "UnmonitorDeletedEpisodes": "Lopeta poistettujen jaksojen valvonta", @@ -568,7 +568,7 @@ "UpdateAll": "Päivitä kaikki", "UpcomingSeriesDescription": "Sarja on julkistettu, mutta tarkka esitysaika ei ole vielä tiedossa.", "UnselectAll": "Tyhjennä valinnat", - "UpdateMonitoring": "Vaihda valvontatilaa", + "UpdateMonitoring": "Muuta valvontaa", "UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,", "UpgradeUntilThisQualityIsMetOrExceeded": "Päivitä kunnes tämä laatu on savutettu tai ylitetty", "Updates": "Päivitykset", @@ -583,7 +583,7 @@ "Wanted": "Halutut", "Warn": "Varoita", "AirsDateAtTimeOn": "{date} klo {time} kanavalla {networkLabel}", - "AirsTbaOn": "TBA kanavalla {networkLabel}", + "AirsTbaOn": "Tulossa kanavalle {networkLabel}", "AirsTimeOn": "{time} kanavalla {networkLabel}", "DownloadClientDownloadStationValidationFolderMissing": "Kansiota ei ole olemassa", "DownloadClientDownloadStationValidationNoDefaultDestination": "Oletussijaintia ei ole", @@ -614,18 +614,18 @@ "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvalvojan oikeuksilla.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeInfo": "Jakson tiedot", - "EpisodeFilesLoadError": "Jaksotiedostojen lataus epäonnistui", + "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja.", "EpisodeIsNotMonitored": "Jaksoa ei valvota", "EpisodeIsDownloading": "Jaksoa ladataan", "EpisodeMissingFromDisk": "Jaksoa ei ole levyllä", - "EpisodeSearchResultsLoadError": "Tämän jaksohaun tulosten lataus epäonnistui. Yritä myöhemmin uudelleen.", + "EpisodeSearchResultsLoadError": "Virhe ladattaessa tämän jaksohaun tuloksia. Yritä myöhemmin uudelleen.", "EpisodeTitle": "Jakson nimi", "EpisodeTitleRequired": "Jakson nimi on pakollinen.", "Episodes": "Jaksot", "ErrorLoadingContents": "Virhe ladattaessa sisältöjä", - "EpisodesLoadError": "Jaksojen lataus epäonnistui", + "EpisodesLoadError": "Virhe ladattaessa jaksoja.", "ErrorLoadingContent": "Virhe ladattaessa tätä sisältöä", - "FailedToLoadCustomFiltersFromApi": "Suodatinmukautusten lataus rajapinnasta epäonnistui", + "FailedToLoadCustomFiltersFromApi": "Omien suodattimien lataus rajapinnalta epäonnistui.", "FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus rajapinnasta epäonnistui", "CalendarFeed": "{appName}in kalenterisyöte", "Agenda": "Agenda", @@ -681,8 +681,8 @@ "Quality": "Laatu", "PortNumber": "Portin numero", "QualitySettings": "Laatuasetukset", - "QuickSearch": "Pikahaku", - "QualityProfilesLoadError": "Laatuprofiilien lataus epäonnistui", + "QuickSearch": "Etsi automaattisesti", + "QualityProfilesLoadError": "Virhe ladattaessa laatuprofiileja.", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} jaksotiedostoa", "SeriesEditor": "Sarjojen muokkaus", "SeriesIndexFooterMissingUnmonitored": "Jaksoja puuttuu (sarjaa ei valvota)", @@ -744,9 +744,9 @@ "SeriesDetailsGoTo": "Avaa {title}", "SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.", "WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?", - "SeriesLoadError": "Sarjojen lataus epäonnistui", + "SeriesLoadError": "Virhe ladattaessa sarjoja.", "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.", - "DownloadClientOptionsLoadError": "Latauspalveluasetusten lataus epäonnistui", + "DownloadClientOptionsLoadError": "Virhe ladattaessa latauspalveluasetuksia.", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).", "IndexerValidationUnableToConnectHttpError": "Hakupalveluun ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", @@ -762,7 +762,7 @@ "MonitoringOptions": "Valvonta-asetukset", "MonitoredOnly": "Vain valvotut", "MonitorSelected": "Valvo valittuja", - "MonitorSeries": "Valvo sarjaa", + "MonitorSeries": "Sarjojen valvonta", "New": "Uutta", "NoHistoryFound": "Historiaa ei löytynyt", "NoEpisodesInThisSeason": "Kaudelle ei ole jaksoja", @@ -888,7 +888,7 @@ "EnableAutomaticSearch": "Käytä automaattihakua", "EndedSeriesDescription": "Uusia jaksoja tai kausia ei tiettävästi ole tulossa", "EditSelectedSeries": "Muokkaa valittuja sarjoja", - "EpisodeHistoryLoadError": "Jaksohistorian lataus epäonnistui", + "EpisodeHistoryLoadError": "Virhe ladattaessa jakson historiatietoja.", "Ended": "Päättynyt", "ExistingSeries": "Olemassa olevat sarjat", "FreeSpace": "Vapaa tila", @@ -918,7 +918,7 @@ "Logout": "Kirjaudu ulos", "IndexerSettings": "Tietolähdeasetukset", "IncludeHealthWarnings": "Sisällytä kuntovaroitukset", - "ListsLoadError": "Listojen lataus epäonnistui", + "ListsLoadError": "Virhe ladattaessa listoja.", "IndexerValidationUnableToConnect": "Hakupalveluun ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", "MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.", @@ -941,7 +941,7 @@ "OrganizeLoadError": "Virhe ladattaessa esikatseluita", "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": "Laatumääritysten lataus epäonnistui", + "QualityDefinitionsLoadError": "Virhe ladattaessa laatumäärityksiä.", "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", "RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon latauspalvelukohtaisiin asetuksiin.", @@ -965,7 +965,7 @@ "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.", "ListTagsHelpText": "Tunnisteet, joilla tältä tuontilistalta lisätyt kohteet merkitään.", "ManageEpisodes": "Jaksojen hallinta", - "ManageEpisodesSeason": "Hallitse tuotantokauden jaksotiedostoja", + "ManageEpisodesSeason": "Tuotantokauden jaksotiedostojen hallinta", "ManageIndexers": "Hallitse tietolähteitä", "LocalPath": "Paikallinen sijainti", "NoChanges": "Muutoksia ei ole", @@ -1011,7 +1011,7 @@ "Seasons": "Kaudet", "SearchAll": "Etsi kaikkia", "SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB-tunnisteilla (esim. \"tvdb:71663\").", - "RootFoldersLoadError": "Juurikansioiden lataus epäonnistui", + "RootFoldersLoadError": "Virhe ladattaessa juurikansioita.", "SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.", "Year": "Vuosi", "WeekColumnHeader": "Viikkosarakkeen otsikko", @@ -1029,7 +1029,7 @@ "MonitoredEpisodesHelpText": "Lataa tämän sarjan valvotut jaksot.", "MoveSeriesFoldersDontMoveFiles": "En, siirrän tiedostot itse", "MoveSeriesFoldersMoveFiles": "Kyllä, siirrä tiedostot", - "MonitorNewItems": "Valvo uusia kohteita", + "MonitorNewItems": "Uusien kausien valvonta", "MonitorSpecialEpisodesDescription": "Valvo kaikkia erikoisjaksoja muuttamatta muiden jaksojen tilaa.", "MonitorNoNewSeasons": "Ei uusia kausia", "OpenSeries": "Avaa sarja", @@ -1040,7 +1040,7 @@ "DeleteCondition": "Poista ehto", "Delete": "Poista", "ApiKey": "Rajapinnan avain", - "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muta, jos et ymmärrä riskejä.", + "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muuta, jos et ymmärrä riskejä.", "Certification": "Varmennus", "ChangeFileDate": "Muuta tiedoston päiväys", "DelayingDownloadUntil": "Lataus on lykätty alkamaan {date} klo {time}", @@ -1310,8 +1310,8 @@ "Ok": "Ok", "General": "Yleiset", "Folders": "Kansiot", - "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", - "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Hakua tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat hakupalvelut eivät ole tilapäisesti käytettävissä hiljattaisten palveluvirheiden vuoksi.", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Mitkään hakua tukevat hakupalvelut eivät ole tilapäisesti käytettävissä hiljattaisten palveluvirheiden vuoksi.", "IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.", "IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika", "KeyboardShortcutsSaveSettings": "Tallenna asetukset", @@ -1619,8 +1619,8 @@ "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", "NotificationsTraktSettingsAccessToken": "Käyttötunniste", "NotificationsTraktSettingsAuthUser": "Todennettu käyttäjä", - "NotificationsValidationUnableToSendTestMessage": "Testiviestin lähetys ei onnistu: {exceptionMessage}", - "NotificationsValidationUnableToSendTestMessageApiResponse": "Testiviestin lähetys ei onnistu. API vastasi: {error}", + "NotificationsValidationUnableToSendTestMessage": "Virhe lähetettäessä testiviestiä: {exceptionMessage}.", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Virhe lähetettäessä testiviestiä. API vastasi: {error}.", "NotificationsEmailSettingsUseEncryption": "Käytä salausta", "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimen ja näyttämään sen tiedot.", "ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.", @@ -1628,7 +1628,7 @@ "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", "RemoveMultipleFromDownloadClientHint": "Poistaa lataukset ja tiedostot latauspalvelusta.", "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa latauksen ja sen tiedostot.", - "UnableToLoadAutoTagging": "Automaattimerkinnän lataus epäonnistui", + "UnableToLoadAutoTagging": "Virhe ladattaessa automaattimerkintää.", "IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin hakupalveluiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", "NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä", @@ -1782,10 +1782,10 @@ "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Älä järjestele sarjoja", "EnableAutomaticAddSeriesHelpText": "Lisää tämän listan sarjat {appName}iin kun synkronointi suoritetaan manuaalisesti käyttöliittymästä tai {appName}in toimesta.", "DownloadClientValidationTestNzbs": "NZB-listauksen nouto epäonnistui: {exceptionMessage}.", - "DownloadClientValidationUnableToConnect": "Latauspalveluun {clientName} ei voida muodostaa yhteyttä", + "DownloadClientValidationUnableToConnect": "Latauspalveluun {clientName} ei voida muodostaa yhteyttä.", "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.", "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.", - "AddDelayProfileError": "Virhe lisättäessä viiveporofiilia. Yritä uudelleen.", + "AddDelayProfileError": "Virhe lisättäessä viiveprofiilia. Yritä uudelleen.", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää latauspalvelun {clientName} RPC-URL-osoitteeseen etuliitteen, esim. \"{url}\". Oletus on \"{defaultUrl}\".", @@ -1827,7 +1827,7 @@ "PublishedDate": "Julkaisupäivä", "DeleteSelectedCustomFormatsMessageText": "Haluatko varmasti poistaa valitut {count} mukautettua muotoa?", "DownloadClientValidationGroupMissingDetail": "Syötettyä ryhmää ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.", - "ImportListsAniListSettingsImportCancelled": "Tuonti peruttiin", + "ImportListsAniListSettingsImportCancelled": "Tuo lopetetut", "ImportListsAniListSettingsImportCancelledHelpText": "Media: sarja on lopetettu", "FolderNameTokens": "Kansionimimuuttujat", "Delay": "Viive", @@ -2068,7 +2068,7 @@ "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Vuosikohtaiset sarjasuositukset", "ImportListsTraktSettingsPopularListTypeTopYearShows": "Vuosikohtaisesti katselluimmat sarjat", "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Kaikkien aikojen katselluimmat sarjat", - "ImportListsTraktSettingsPopularListTypeTrendingShows": "Nousevat sarjat", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trendaavat sarjat", "ImportListsTraktSettingsUserListName": "Trakt-käyttäjän listat", "ImportListsTraktSettingsUsernameHelpText": "Tuotavan listan käyttäjätunnus", "ImportListsTraktSettingsWatchedListTypeAll": "Kaikki", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8a5119c3f..b73298e64 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -7,15 +7,15 @@ "EditSelectedDownloadClients": "Editar clientes de download selecionados", "EditSelectedImportLists": "Editar listas de importação selecionadas", "Enabled": "Habilitado", - "Ended": "Terminou", + "Ended": "Finalizado", "HideAdvanced": "Ocultar opções avançadas", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão ausentes nas listas de importação: {rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Várias pastas raiz estão ausentes para listas de importação: {rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Todas as listas estão indisponíveis devido a falhas", "ImportMechanismHandlingDisabledHealthCheckMessage": "Habilitar gerenciamento de download concluído", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com a sincronização RSS habilitada, o {appName} não capturará novos lançamentos automaticamente", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com a sincronização RSS habilitada, o {appName} não obterá novos lançamentos automaticamente", "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa automática habilitada, o {appName} não fornecerá nenhum resultado de pesquisa automática", - "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa Interativa habilitada, {appName} não fornecerá resultados de pesquisa interativas", + "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa interativa habilitada, o {appName} não fornecerá resultados de pesquisa interativas", "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", "Language": "Idioma", @@ -58,22 +58,22 @@ "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na Atualização", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {downloadClientName}. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads na pasta raiz {rootFolderPath}. Você não deve baixar para uma pasta raiz.", - "DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria {appName}. Você deve desativar a classificação em seu cliente de download para evitar problemas de importação.", + "DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria do {appName}. Você deve desabilitar essa classificação em seu cliente de download para evitar problemas de importação.", "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de download indisponíveis devido a falhas: {downloadClientNames}", "EditSelectedIndexers": "Editar indexadores selecionados", - "EditSeries": "Editar Série", + "EditSeries": "Editar série", "EnableAutomaticSearch": "Ativar a pesquisa automática", "EnableInteractiveSearch": "Ativar pesquisa interativa", "HiddenClickToShow": "Oculto, clique para mostrar", "ImportListRootFolderMissingRootHealthCheckMessage": "Pasta raiz ausente para lista(s) de importação: {rootFolderInfo}", "ImportListStatusUnavailableHealthCheckMessage": "Listas indisponíveis devido a falhas: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível", - "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível (Multi-computador não suportado)", - "IndexerJackettAllHealthCheckMessage": "Indexadores que usam o ponto de extremidade \"all\" incompatível do Jackett: {indexerNames}", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível (não há suporte para vários computadores)", + "IndexerJackettAllHealthCheckMessage": "Indexadores que usam o ponto de extremidade \"all\" (tudo) incompatível do Jackett: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas por mais de 6 horas", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos os indexadores compatíveis com RSS estão temporariamente indisponíveis devido a erros recentes do indexador", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos os indexadores com capacidade de pesquisa estão temporariamente indisponíveis devido a erros recentes do indexador", - "NextAiring": "Próxima Exibição", + "NextAiring": "Próxima exibição", "OriginalLanguage": "Idioma Original", "ProxyBadRequestHealthCheckMessage": "Falha ao testar o proxy. Código de estado: {statusCode}", "ProxyFailedToTestHealthCheckMessage": "Falha ao testar o proxy: {url}", @@ -147,7 +147,7 @@ "LogFiles": "Arquivos de log", "MediaManagement": "Gerenciamento de mídia", "Metadata": "Metadados", - "MetadataSource": "Fonte de Metadados", + "MetadataSource": "Fonte de metadados", "Missing": "Ausente", "Profiles": "Perfis", "Quality": "Qualidade", @@ -174,7 +174,7 @@ "Events": "Eventos", "General": "Geral", "History": "Histórico", - "ImportLists": "Importar listas", + "ImportLists": "Listas de importação", "Indexers": "Indexadores", "AbsoluteEpisodeNumbers": "Número(s) absoluto(s) do episódio", "AirDate": "Data de exibição", @@ -186,13 +186,13 @@ "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar etiquetas às listas de importação selecionadas", "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", "ApplyTagsHelpTextHowToApplySeries": "Como aplicar etiquetas à série selecionada", - "EpisodeInfo": "Info do Episódio", - "EpisodeNumbers": "Número(s) do(s) Episódio(s)", - "FullSeason": "Temporada Completa", + "EpisodeInfo": "Informações do episódio", + "EpisodeNumbers": "Número(s) do(s) episódio(s)", + "FullSeason": "Temporada completa", "Languages": "Idiomas", - "MatchedToEpisodes": "Correspondente aos Episódios", - "MatchedToSeason": "Correspondente a Temporada", - "MatchedToSeries": "Correspondente à Série", + "MatchedToEpisodes": "Correspondente aos episódios", + "MatchedToSeason": "Correspondente à temporada", + "MatchedToSeries": "Correspondente à série", "MultiSeason": "Várias temporadas", "PartialSeason": "Temporada Parcial", "Proper": "Proper", @@ -229,7 +229,7 @@ "ErrorRestoringBackup": "Erro ao restaurar o backup", "Exception": "Exceção", "ExternalUpdater": "O {appName} está configurado para usar um mecanismo de atualização externo", - "FailedToFetchUpdates": "Falha ao buscar atualizações", + "FailedToFetchUpdates": "Falha ao obter atualizações", "FeatureRequests": "Solicitação de recursos", "Filename": "Nome do arquivo", "Fixed": "Corrigido", @@ -248,20 +248,20 @@ "LastWriteTime": "Hora da última gravação", "Location": "Localização", "LogFilesLocation": "Os arquivos de log estão localizados em: {location}", - "Logs": "Registros", - "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commit do Github para saber mais", + "Logs": "Logs", + "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commits do Github para saber mais", "Manual": "Manual", "Message": "Mensagem", "Mode": "Modo", "MoreInfo": "Mais informações", "New": "Novo", - "NextExecution": "Próxima Execução", + "NextExecution": "Próxima execução", "NoBackupsAreAvailable": "Não há backups disponíveis", "NoEventsFound": "Nenhum evento encontrado", - "NoIssuesWithYourConfiguration": "Sem problemas com sua configuração", - "NoLeaveIt": "Não, deixe-o", + "NoIssuesWithYourConfiguration": "Nenhum problema com sua configuração", + "NoLeaveIt": "Não, deixe", "NoLogFiles": "Nenhum arquivo de log", - "NoUpdatesAreAvailable": "Nenhuma atualização está disponível", + "NoUpdatesAreAvailable": "Nenhuma atualização disponível", "Options": "Opções", "PackageVersion": "Versão do pacote", "PackageVersionInfo": "{packageVersion} por {packageAuthor}", @@ -313,10 +313,10 @@ "Date": "Data", "Deleted": "Excluído", "DownloadClient": "Cliente de download", - "EndedOnly": "Terminado Apenas", + "EndedOnly": "Finalizado apenas", "Episode": "Episódio", "EpisodeAirDate": "Data de exibição do episódio", - "EpisodeCount": "Número do Episódio", + "EpisodeCount": "Número do episódio", "EpisodeProgress": "Progresso do episódio", "EpisodeTitle": "Título do episódio", "Error": "Erro", @@ -328,7 +328,7 @@ "Imported": "Importado", "IncludeUnmonitored": "Incluir não monitorados", "Indexer": "Indexador", - "LatestSeason": "Última temporada", + "LatestSeason": "Temporada mais recente", "MissingEpisodes": "Episódios ausentes", "MonitoredOnly": "Somente monitorados", "OutputPath": "Caminho de saída", @@ -356,9 +356,9 @@ "Age": "Tempo de vida", "Episodes": "Episódios", "Failed": "Falhou", - "HasMissingSeason": "Está faltando temporada", + "HasMissingSeason": "Faltam temporadas", "Info": "Informações", - "NotSeasonPack": "Não Pacote de Temporada", + "NotSeasonPack": "Não é pacote de temporada", "Peers": "Pares", "Protocol": "Protocolo", "RejectionCount": "Número de rejeição", @@ -483,7 +483,7 @@ "CopyToClipboard": "Copiar para a área de transferência", "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo semeados. Você pode desabilitar temporariamente a semeadura e usar a função de renomeação do {appName} como uma solução alternativa.", "CreateEmptySeriesFolders": "Criar pastas de séries vazias", - "CreateEmptySeriesFoldersHelpText": "Crie pastas de séries ausentes durante a verificação de disco", + "CreateEmptySeriesFoldersHelpText": "Criar pastas de séries ausentes durante a verificação de disco", "CreateGroup": "Criar grupo", "Custom": "Personalizado", "CustomFormat": "Formato personalizado", @@ -541,16 +541,16 @@ "EditCustomFormat": "Editar formato personalizado", "EditDelayProfile": "Editar perfil de atraso", "EditGroups": "Editar grupos", - "EditImportListExclusion": "Editar exclusão de lista de importação", + "EditImportListExclusion": "Editar exclusão da lista de importação", "EditListExclusion": "Editar exclusão da lista", - "EditMetadata": "Editar {metadataType} Metadados", + "EditMetadata": "Editar metadados {metadataType}", "EditQualityProfile": "Editar perfil de qualidade", "EditReleaseProfile": "Editar perfil de lançamento", "EditRemotePathMapping": "Editar mapeamento de caminho remoto", "EditRestriction": "Editar restrição", "Enable": "Habilitar", "EnableAutomaticAdd": "Habilitar adição automática", - "EnableAutomaticAddSeriesHelpText": "Adicione séries desta lista ao {appName} quando as sincronizações forem realizadas por meio da interface do usuário ou pelo {appName}", + "EnableAutomaticAddSeriesHelpText": "Adicione séries desta lista ao {appName} ao sincronizar pela interface ou pelo {appName}", "EnableAutomaticSearchHelpText": "Será usado ao realizar pesquisas automáticas pela interface ou pelo {appName}", "EnableAutomaticSearchHelpTextWarning": "Será usado com a pesquisa interativa", "EnableCompletedDownloadHandlingHelpText": "Importar automaticamente downloads concluídos do cliente de download", @@ -565,16 +565,16 @@ "EnableRss": "Habilitar RSS", "EnableRssHelpText": "Será usado quando o {appName} procurar periodicamente por lançamentos via RSS Sync", "EnableSsl": "Habilitar SSL", - "EnableSslHelpText": "Requer reinicialização em execução como administrador para entrar em vigor", - "EpisodeNaming": "Nomenclatura do Episódio", + "EnableSslHelpText": "Requer reinicialização e execução como administrador para entrar em vigor", + "EpisodeNaming": "Nomenclatura do episódio", "EpisodeSearchResultsLoadError": "Não foi possível carregar os resultados da pesquisa deste episódio. Tente mais tarde", - "EpisodeTitleRequired": "Título do Episódio Obrigatório", + "EpisodeTitleRequired": "Título do episódio obrigatório", "Example": "Exemplo", "Extend": "Estender", "ExtraFileExtensionsHelpTextsExamples": "Exemplos: \".sub, .nfo\" ou \"sub,nfo\"", - "FileManagement": "Gerenciamento de arquivo", + "FileManagement": "Gerenciamento de arquivos", "FileNameTokens": "Tokens de nome de arquivo", - "FileNames": "Nomes de arquivo", + "FileNames": "Nomes de arquivos", "FirstDayOfWeek": "Primeiro dia da semana", "Folders": "Pastas", "GeneralSettingsLoadError": "Não foi possível carregar as configurações gerais", @@ -590,13 +590,13 @@ "Host": "Host", "Hostname": "Nome do host", "ImportExtraFiles": "Importar arquivos adicionais", - "ImportExtraFilesEpisodeHelpText": "Importar arquivos extras correspondentes (legendas, nfo, etc) após importar um arquivo de episódio", - "ImportList": "Importar lista", - "ImportListExclusions": "Importar exclusões de lista", + "ImportExtraFilesEpisodeHelpText": "Importar arquivos adicionais correspondentes (legendas, nfo, etc.) após importar um arquivo de episódio", + "ImportList": "Listas de importação", + "ImportListExclusions": "Exclusões da lista de importação", "ImportListExclusionsLoadError": "Não foi possível carregar as exclusões da lista de importação", - "ImportListSettings": "Configurações de Importar listas", - "ImportListsLoadError": "Não foi possível carregar Importar listas", - "ImportListsSettingsSummary": "Importe de outra instância do {appName} ou listas do Trakt e gerencie exclusões de listas", + "ImportListSettings": "Configurações de listas de importação", + "ImportListsLoadError": "Não foi possível carregar as listas de importação", + "ImportListsSettingsSummary": "Importar de outra instância do {appName} ou listas do Trakt e gerenciar exclusões de listas", "ImportScriptPath": "Caminho para script de importação", "ImportScriptPathHelpText": "O caminho para o script a ser usado para importar", "ImportUsingScript": "Importar usando script", @@ -608,7 +608,7 @@ "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 um desempate ao capturar lançamentos, o {appName} ainda usará todos os indexadores habilitados para a sincronização RSS e pesquisa", + "IndexerPriorityHelpText": "Prioridade do indexador, de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado como um desempate ao obter lançamentos, o {appName} ainda usará todos os indexadores habilitados para sincronização RSS e pesquisa", "IndexerSettings": "Configurações do indexador", "IndexersLoadError": "Não foi possível carregar os indexadores", "IndexersSettingsSummary": "Indexadores e opções de indexador", @@ -618,10 +618,10 @@ "InvalidFormat": "Formato inválido", "LanguagesLoadError": "Não foi possível carregar os idiomas", "ListExclusionsLoadError": "Não foi possível carregar as exclusões de lista", - "ListOptionsLoadError": "Não foi possível carregar as opções de lista", + "ListOptionsLoadError": "Não foi possível carregar as opções da lista", "ListQualityProfileHelpText": "Os itens da lista de Perfil de qualidade serão adicionados com", - "ListRootFolderHelpText": "Os itens da lista da pasta raiz serão adicionados a", - "ListTagsHelpText": "Tags que serão adicionadas ao importar esta lista", + "ListRootFolderHelpText": "Os itens da lista da Pasta raiz serão adicionados a", + "ListTagsHelpText": "Etiquetas que serão adicionadas ao importar esta lista", "ListWillRefreshEveryInterval": "A lista será atualizada a cada {refreshInterval}", "ListsLoadError": "Não foi possível carregar as listas", "LocalAirDate": "Data de exibição local", @@ -634,15 +634,15 @@ "ManualImportItemsLoadError": "Não foi possível carregar itens de importação manual", "Max": "Máx.", "MaximumLimits": "Limites máximos", - "MaximumSingleEpisodeAge": "Idade máxima de episódio único", - "MaximumSingleEpisodeAgeHelpText": "Durante uma pesquisa de temporada completa, apenas os pacotes de temporada serão permitidos quando o último episódio da temporada for mais antigo do que esta configuração. Somente série padrão. Use 0 para desabilitar.", + "MaximumSingleEpisodeAge": "Tempo de vida máximo do episódio único", + "MaximumSingleEpisodeAgeHelpText": "Durante uma pesquisa de temporada completa, apenas os pacotes de temporada serão permitidos quando o último episódio da temporada for mais antigo do que esta configuração. Somente para séries padrão. Use 0 para desabilitar.", "MaximumSize": "Tamanho máximo", "MaximumSizeHelpText": "Tamanho máximo, em MB, para obter um lançamento. Zero significa ilimitado", "Mechanism": "Mecanismo", "MediaInfo": "Informações da mídia", "MediaManagementSettings": "Configurações de gerenciamento de mídia", "MediaManagementSettingsLoadError": "Não foi possível carregar as configurações de gerenciamento de mídia", - "MediaManagementSettingsSummary": "Nomenclatura, configurações de gerenciamento de arquivos e pastas raiz", + "MediaManagementSettingsSummary": "Configurações de nomenclatura, gerenciamento de arquivos e pastas raiz", "MegabytesPerMinute": "Megabytes por minuto", "MetadataLoadError": "Não foi possível carregar os metadados", "MetadataSettings": "Configurações de metadados", @@ -650,8 +650,8 @@ "MetadataSourceSettings": "Configurações da fonte de metadados", "MetadataSourceSettingsSeriesSummary": "Informações sobre onde o {appName} obtém informações sobre séries e episódios", "Min": "Mín.", - "MinimumAge": "Idade mínima", - "MinimumAgeHelpText": "Somente Usenet: idade mínima, em minutos, dos NZBs antes de serem obtidos. Use esta opção para dar aos novos lançamentos tempo para propagarem para seu provedor de Usenet.", + "MinimumAge": "Tempo de vida mínimo", + "MinimumAgeHelpText": "Somente Usenet: tempo de vida mínimo, em minutos, dos NZBs antes de serem obtidos. Use esta opção para dar aos novos lançamentos tempo para propagarem para seu provedor de Usenet.", "MinimumCustomFormatScore": "Pontuação mínima do formato personalizado", "MinimumCustomFormatScoreHelpText": "Pontuação mínima de formato personalizado permitida para download", "MinimumFreeSpace": "Mínimo de espaço livre", @@ -672,11 +672,11 @@ "NamingSettingsLoadError": "Não foi possível carregar as configurações de nomenclatura", "Never": "Nunca", "NoChanges": "Sem alterações", - "NoDelay": "Sem Atraso", + "NoDelay": "Sem atraso", "NoLinks": "Sem links", - "NoTagsHaveBeenAddedYet": "Nenhuma tag foi adicionada ainda", + "NoTagsHaveBeenAddedYet": "Nenhuma etiqueta foi adicionada ainda", "None": "Nenhum", - "NotificationTriggers": "Gatilhos de Notificação", + "NotificationTriggers": "Acionadores de notificação", "NotificationTriggersHelpText": "Selecione quais eventos devem acionar esta notificação", "NotificationsLoadError": "Não foi possível carregar as notificações", "OnApplicationUpdate": "Na Atualização do Aplicativo", @@ -744,19 +744,19 @@ "DeleteDelayProfileMessageText": "Tem certeza de que deseja excluir este perfil de atraso?", "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista \"{name}\"?", "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir o perfil de lançamento \"{name}\"?", - "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", - "EpisodeTitleRequiredHelpText": "Impeça a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for TBA", + "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma etiqueta correspondente. Deixe em branco para usar com todas as séries.", + "EpisodeTitleRequiredHelpText": "Impedir a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for \"TBA\"", "External": "Externo", "ExtraFileExtensionsHelpText": "Lista separada por vírgulas de arquivos adicionais a importar (.nfo será importado como .nfo-orig)", "HistoryLoadError": "Não foi possível carregar o histórico", - "IndexerTagSeriesHelpText": "Usar este indexador apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", - "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages suporta um sufixo `:EN+DE` permitindo filtrar os idiomas incluídos no nome do arquivo. Use `-DE` para excluir idiomas específicos. Anexar `+` (por exemplo, `:EN+`) resultará em `[EN]`/`[EN+--]`/`[--]` dependendo dos idiomas excluídos. Por exemplo, `{MediaInfo Full:EN+DE}`.", + "IndexerTagSeriesHelpText": "Usar este indexador apenas para séries com pelo menos uma etiqueta correspondente. Deixe em branco para usar com todas as séries.", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages suporta um sufixo `:EN+DE`, permitindo filtrar os idiomas inclusos no nome do arquivo. Use `-DE` para excluir idiomas específicos. Anexar `+` (por exemplo, `:EN+`) resultará em `[EN]`/`[EN+--]`/`[--]`, dependendo dos idiomas excluídos. Por exemplo, `{MediaInfo Full:EN+DE}`.", "MinimumFreeSpaceHelpText": "Impedir a importação se deixar menos do que esta quantidade de espaço em disco disponível", - "MustContainHelpText": "O lançamento deve conter pelo menos um destes termos (não diferenciar maiúsculas e minúsculas)", - "NegateHelpText": "Se marcado, o formato personalizado não será aplicado se esta condição {implementationName} corresponder.", + "MustContainHelpText": "O lançamento deve conter pelo menos um destes termos (não diferencia maiúsculas e minúsculas)", + "NegateHelpText": "Se marcado, o formato personalizado não será aplicado se a condição {implementationName} corresponder.", "NoLimitForAnyRuntime": "Sem limite para qualquer duração", "NoMinimumForAnyRuntime": "Sem mínimo para qualquer duração", - "NotificationsTagsSeriesHelpText": "Envie notificações apenas para séries com pelo menos uma tag correspondente", + "NotificationsTagsSeriesHelpText": "Enviar notificações apenas para séries com pelo menos uma etiqueta correspondente", "OnManualInteractionRequired": "Na Interação Manual Necessária", "PendingChangesMessage": "Você tem alterações não salvas. Tem certeza de que deseja sair desta página?", "ProtocolHelpText": "Escolha qual(is) protocolo(s) usar e qual é o preferido ao escolher entre laçamentos iguais", @@ -813,7 +813,7 @@ "Script": "Script", "ScriptPath": "Caminho do Script", "Scene": "Cena", - "SearchIsNotSupportedWithThisIndexer": "Pesquisa não é compatível com este indexador", + "SearchIsNotSupportedWithThisIndexer": "A pesquisa não é compatível com este indexador", "SeasonFolderFormat": "Formato da Pasta da Temporada", "Security": "Segurança", "SendAnonymousUsageData": "Enviar dados de uso anônimos", @@ -926,7 +926,7 @@ "VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ", "WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)", "UnmonitorSpecialEpisodes": "Não Monitorar Especiais", - "MonitorAllEpisodes": "Todos os Episódios", + "MonitorAllEpisodes": "Todos os episódios", "AddNewSeries": "Adicionar nova série", "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja acrescentar.", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos limites não foram atendidos", @@ -939,21 +939,21 @@ "ChooseAnotherFolder": "Escolha outra pasta", "CouldNotFindResults": "Não foi possível encontrar nenhum resultado para \"{term}\"", "Existing": "Existente", - "ImportCountSeries": "Importar {selectedCount} Séries", + "ImportCountSeries": "Importar {selectedCount} séries", "ImportErrors": "Erros de importação", - "ImportExistingSeries": "Importar Série Existente", - "ImportSeries": "Importar Séries", + "ImportExistingSeries": "Importar série existente", + "ImportSeries": "Importar séries", "LibraryImportSeriesHeader": "Importar as séries que você já possui", "LibraryImportTips": "Algumas dicas para garantir que a importação ocorra sem problemas:", "LibraryImportTipsDontUseDownloadsFolder": "Não use para importar downloads de seu cliente. Isso aplica-se apenas a bibliotecas organizadas existentes, e não a arquivos desorganizados.", "LibraryImportTipsQualityInEpisodeFilename": "Certifique-se de que seus arquivos incluam a qualidade nos nomes de arquivo. Por exemplo: \"episódio.s02e15.bluray.mkv\"", "Monitor": "Monitorar", "MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais", - "MonitorExistingEpisodes": "Episódios Existentes", - "MonitorFirstSeason": "Primeira Temporada", + "MonitorExistingEpisodes": "Episódios existentes", + "MonitorFirstSeason": "Primeira temporada", "MonitorFirstSeasonDescription": "Monitorar todos os episódios da primeira temporada. As demais temporadas serão ignoradas", - "MonitorFutureEpisodes": "Futuros Episódios", - "MonitorFutureEpisodesDescription": "Monitorar episódios que não foram exibidos", + "MonitorFutureEpisodes": "Futuros episódios", + "MonitorFutureEpisodesDescription": "Monitorar episódios que ainda não foram exibidos", "MonitorMissingEpisodes": "Episódios ausentes", "MonitorMissingEpisodesDescription": "Monitora os episódios que não possuem arquivos ou ainda não foram ao ar", "MonitorNoEpisodes": "Nenhum", @@ -986,21 +986,21 @@ "DelayingDownloadUntil": "Atrasando o download até {date} às {time}", "DeletedReasonEpisodeMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então ele foi desvinculado do episódio no banco de dados", "DeletedReasonManual": "O arquivo foi excluído usando o {appName}, manualmente ou por outra ferramenta por meio da API", - "DownloadFailed": "Download Falhou", + "DownloadFailed": "Falha no download", "DestinationRelativePath": "Caminho de destino relativo", - "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", + "DownloadIgnoredEpisodeTooltip": "Download do episódio ignorado", "DownloadFailedEpisodeTooltip": "O download do episódio falhou", "DownloadIgnored": "Download ignorado", "DownloadWarning": "Aviso de download: {warningMessage}", "Downloading": "Baixando", "Downloaded": "Baixado", - "EpisodeFileDeleted": "Arquivo do Episódio Excluído", + "EpisodeFileDeleted": "Arquivo do episódio excluído", "EpisodeFileRenamedTooltip": "Arquivo do episódio renomeado", "EpisodeFileDeletedTooltip": "Arquivo do episódio excluído", - "EpisodeFileRenamed": "Arquivo do Episódio Renomeado", + "EpisodeFileRenamed": "Arquivo do episódio renomeado", "GrabId": "Obter ID", - "GrabSelected": "Baixar selecionado", - "EpisodeGrabbedTooltip": "Episódio retirado de {indexer} e enviado para {downloadClient}", + "GrabSelected": "Obter selecionado", + "EpisodeGrabbedTooltip": "Episódio obtido de {indexer} e enviado para {downloadClient}", "ImportedTo": "Importado para", "InfoUrl": "URL de informações", "MarkAsFailed": "Marcar como falha", @@ -1064,15 +1064,15 @@ "ICalSeasonPremieresOnlyHelpText": "Apenas o primeiro episódio de uma temporada estará no feed", "ICalShowAsAllDayEventsHelpText": "Os eventos aparecerão como eventos de dia inteiro em seu calendário", "IconForCutoffUnmet": "Ícone para limite não atendido", - "IconForCutoffUnmetHelpText": "Mostrar ícone para arquivos quando o limite não foi atingido", - "IconForFinales": "Ícone para Finais", - "IconForSpecials": "Ícone para Especiais", + "IconForCutoffUnmetHelpText": "Mostrar ícone para arquivos cujo limite não foi atingido", + "IconForFinales": "Ícone para finais", + "IconForSpecials": "Ícone para especiais", "IconForSpecialsHelpText": "Mostrar ícone para episódios especiais (temporada 0)", "ImportFailed": "Falha na importação: {sourceTitle}", "EpisodeMissingAbsoluteNumber": "O episódio não tem um número de episódio absoluto", "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Programação", - "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", - "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", + "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma etiqueta correspondente", + "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações disponíveis do episódio", "QualityCutoffNotMet": "Limite da qualidade ainda não foi alcançado", "QueueLoadError": "Falha ao carregar a fila", "RemoveQueueItem": "Remover - {sourceTitle}", @@ -1103,21 +1103,21 @@ "EditIndexerImplementation": "Editar indexador - {implementationName}", "ErrorLoadingItem": "Ocorreu um erro ao carregar este item", "FailedToLoadQualityProfilesFromApi": "Falha ao carregar perfis de qualidade da API", - "FailedToLoadUiSettingsFromApi": "Falha ao carregar as configurações de IU da API", + "FailedToLoadUiSettingsFromApi": "Falha ao carregar as configurações de interface da API", "FilterEpisodesPlaceholder": "Filtrar episódios por título ou número", - "FilterIsAfter": "está depois", + "FilterIsAfter": "está depois de", "Grab": "Obter", "GrabReleaseUnknownSeriesOrEpisodeMessageText": "O {appName} não conseguiu determinar para qual série e episódio é este lançamento. O {appName} pode não conseguir importar automaticamente este lançamento. Deseja obter \"{title}\"?", "ICalFeed": "Feed do iCal", "ICalLink": "Link do iCal", "InteractiveImportLoadError": "Não foi possível carregar itens de importação manual", "InteractiveImportNoFilesFound": "Nenhum arquivo de vídeo encontrado na pasta selecionada", - "InteractiveImportNoSeason": "A temporada deve ser escolhida para cada arquivo selecionado", + "InteractiveImportNoSeason": "Escolha uma temporada para cada arquivo selecionado", "InteractiveSearchResultsSeriesFailedErrorMessage": "A pesquisa falhou porque {message}. Tente atualizar as informações da série e verifique se as informações necessárias estão presentes antes de pesquisar novamente.", "KeyboardShortcutsFocusSearchBox": "Selecionar a caixa de pesquisa", "KeyboardShortcutsSaveSettings": "Salvar configurações", "LocalStorageIsNotSupported": "O armazenamento local não é compatível ou está desabilitado. Um plugin ou a navegação privada pode tê-lo desativado.", - "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis quando executadas como um serviço do Windows. Consulte as [FAQ]({url}) para obter mais informações.", + "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis quando executadas como um serviço do Windows. Consulte as [Perguntas frequentes]({url}) para saber mais.", "AirsTbaOn": "A ser anunciado em {networkLabel}", "AirsTimeOn": "{time} em {networkLabel}", "AirsTomorrowOn": "Amanhã às {time} em {networkLabel}", @@ -1141,70 +1141,70 @@ "DeleteEpisodeFromDisk": "Excluir episódio do disco", "DeleteSelectedEpisodeFiles": "Excluir arquivos de episódios selecionados", "Donate": "Doar", - "EditConnectionImplementation": "Editar Conexão - {implementationName}", + "EditConnectionImplementation": "Editar conexão - {implementationName}", "EditDownloadClientImplementation": "Editar cliente de download - {implementationName}", "EditImportListImplementation": "Editar lista de importação - {implementationName}", - "EpisodeDownloaded": "Episódio Baixado", + "EpisodeDownloaded": "Episódio baixado", "EpisodeHasNotAired": "Episódio não foi ao ar", - "EpisodeHistoryLoadError": "Não foi possível carregar o histórico de episódios", - "EpisodeIsNotMonitored": "Episódio não é monitorado", - "EpisodeMissingFromDisk": "Episódio faltando no disco", + "EpisodeHistoryLoadError": "Não foi possível carregar o histórico do episódio", + "EpisodeIsNotMonitored": "Episódio não está monitorado", + "EpisodeMissingFromDisk": "Episódio ausente do disco", "EpisodesLoadError": "Não foi possível carregar os episódios", "ErrorLoadingContent": "Ocorreu um erro ao carregar este conteúdo", "ErrorLoadingContents": "Erro ao carregar o conteúdo", "ErrorLoadingPage": "Ocorreu um erro ao carregar esta página", - "ExistingSeries": "Série Existente", + "ExistingSeries": "Série existente", "FailedToLoadCustomFiltersFromApi": "Falha ao carregar filtros personalizados da API", "FailedToLoadSeriesFromApi": "Falha ao carregar a série da API", - "FailedToLoadSonarr": "Falha ao carregar {appName}", + "FailedToLoadSonarr": "Falha ao carregar o {appName}", "FailedToLoadSystemStatusFromApi": "Falha ao carregar o status do sistema da API", - "FailedToLoadTagsFromApi": "Falha ao carregar tags da API", + "FailedToLoadTagsFromApi": "Falha ao carregar etiquetas da API", "FailedToLoadTranslationsFromApi": "Falha ao carregar as traduções da API", "False": "Falso", "File": "Arquivo", - "FileBrowser": "Navegador de Arquivos", + "FileBrowser": "Navegador de arquivos", "FileBrowserPlaceholderText": "Comece a digitar ou selecione um caminho abaixo", "Filter": "Filtro", "FilterContains": "contém", "FilterDoesNotContain": "não contém", "FilterDoesNotEndWith": "não termina com", "FilterDoesNotStartWith": "não começa com", - "FilterEndsWith": "não começa com", - "FilterEqual": "igual", - "FilterGreaterThan": "maior do que", - "FilterGreaterThanOrEqual": "maior ou igual", - "FilterInLast": "no ultimo", - "FilterInNext": "na próxima", + "FilterEndsWith": "termina com", + "FilterEqual": "igual a", + "FilterGreaterThan": "maior que", + "FilterGreaterThanOrEqual": "maior ou igual a", + "FilterInLast": "no(a) último(a)", + "FilterInNext": "no(a) próximo(a)", "FilterIs": "é", - "FilterIsBefore": "é antes", + "FilterIsBefore": "está antes de", "FilterIsNot": "não é", "FilterLessThan": "menor que", - "FilterLessThanOrEqual": "menor ou igual", - "FilterNotEqual": "não igual", - "FilterNotInLast": "não no último", - "FilterNotInNext": "não no próximo", + "FilterLessThanOrEqual": "menor ou igual a", + "FilterNotEqual": "não é igual a", + "FilterNotInLast": "não no(a) último(a)", + "FilterNotInNext": "não no(a) próximo(a)", "FilterSeriesPlaceholder": "Filtrar séries", "FilterStartsWith": "começa com", - "GrabRelease": "Baixar lançamento", + "GrabRelease": "Obter lançamento", "HardlinkCopyFiles": "Criar hardlink/Copiar arquivos", "InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado", "InteractiveImportNoImportMode": "Selecione um modo de importação", - "InteractiveImportNoLanguage": "Selecione um idioma para cada arquivo selecionado", + "InteractiveImportNoLanguage": "Selecione pelo menos um idioma para cada arquivo selecionado", "InteractiveImportNoQuality": "Selecione a qualidade para cada arquivo selecionado", - "InteractiveImportNoSeries": "A série deve ser escolhida para cada arquivo selecionado", + "InteractiveImportNoSeries": "Escolha uma série para cada arquivo selecionado", "KeyboardShortcuts": "Atalhos do teclado", "KeyboardShortcutsCloseModal": "Fechar pop-up atual", - "KeyboardShortcutsConfirmModal": "Aceitar o pop-up de confirmação", + "KeyboardShortcutsConfirmModal": "Pop-up Aceitar confirmação", "KeyboardShortcutsOpenModal": "Abrir este pop-up", "Local": "Local", "Logout": "Sair", - "ManualGrab": "Baixar manualmente", + "ManualGrab": "Obter manualmente", "ManualImport": "Importação manual", "Mapping": "Mapeamento", "MarkAsFailedConfirmation": "Tem certeza de que deseja marcar \"{sourceTitle}\" como em falha?", - "MidseasonFinale": "Final da Meia Temporada", + "MidseasonFinale": "Final da meia temporada", "More": "Mais", - "MyComputer": "Meu Computador", + "MyComputer": "Meu computador", "NoEpisodeHistory": "Sem histórico de episódios", "NoEpisodeOverview": "Sem resumo do episódio", "NotificationStatusAllClientHealthCheckMessage": "Todas as notificações estão indisponíveis devido a falhas", @@ -1241,7 +1241,7 @@ "Mixed": "Misturado", "MoveFiles": "Mover arquivos", "MultiLanguages": "Vários idiomas", - "NoEpisodesFoundForSelectedSeason": "Nenhum episódio foi encontrado para a temporada selecionada", + "NoEpisodesFoundForSelectedSeason": "Nenhum episódio encontrado para a temporada selecionada", "NotificationStatusSingleClientHealthCheckMessage": "Notificações indisponíveis devido a falhas: {notificationNames}", "Or": "ou", "Organize": "Organizar", @@ -1313,12 +1313,12 @@ "AddListExclusion": "Adicionar exclusão à lista", "AddListExclusionSeriesHelpText": "Impedir que o {appName} adicione séries por listas", "EditSeriesModalHeader": "Editar - {title}", - "EditSelectedSeries": "Editar Séries Selecionadas", + "EditSelectedSeries": "Editar séries selecionadas", "HideEpisodes": "Ocultar episódios", "MoveSeriesFoldersMoveFiles": "Sim, mova os arquivos", - "MoveSeriesFoldersToNewPath": "Gostaria de mover os arquivos da série de '{originalPath}' para '{destinationPath}'?", + "MoveSeriesFoldersToNewPath": "Gostaria de mover os arquivos da série de \"{originalPath}\" para \"{destinationPath}\"?", "MoveSeriesFoldersDontMoveFiles": "Não, eu mesmo moverei os arquivos", - "MoveSeriesFoldersToRootFolder": "Gostaria de mover as pastas da série para '{destinationRootFolder}'?", + "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: {date}", @@ -1357,13 +1357,13 @@ "InteractiveSearchModalHeader": "Pesquisa interativa", "InteractiveSearchModalHeaderSeason": "Pesquisa interativa - {season}", "InteractiveSearchSeason": "Pesquisa interativa para todos os episódios desta temporada", - "InvalidUILanguage": "Sua interface está definida com um idioma inválido, corrija-o e salve suas configurações", + "InvalidUILanguage": "Sua interface está definida com um idioma inválido, corrija e salve suas configurações", "Large": "Grande", "Links": "Links", "ManageEpisodes": "Gerenciar episódios", "ManageEpisodesSeason": "Gerenciar arquivos de episódios nesta temporada", "Medium": "Médio", - "MonitorSeries": "Monitorar Série", + "MonitorSeries": "Monitorar série", "MonitoredEpisodesHelpText": "Baixar episódios monitorados desta série", "MonitoredStatus": "Monitorado/Status", "Monitoring": "Monitorando", @@ -1419,7 +1419,7 @@ "NoMonitoredEpisodes": "Nenhum episódio monitorado nesta série", "ShowBanners": "Mostrar Banners", "DeleteSeriesFolderCountWithFilesConfirmation": "Tem certeza de que deseja excluir as {count} séries selecionadas e todos os conteúdos?", - "NoSeriesFoundImportOrAdd": "Nenhuma série encontrada. Para começar, você deseja importar sua série existente ou adicionar uma nova série.", + "NoSeriesFoundImportOrAdd": "Nenhuma série encontrada. Para começar, importe suas séries existentes ou adicione uma nova série.", "ShowBannersHelpText": "Mostrar banners em vez de títulos", "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} arquivos de episódios, totalizando {size}", "OrganizeSelectedSeriesModalAlert": "Dica: para visualizar uma renomeação, selecione 'Cancelar', selecione qualquer título de série e use este ícone:", @@ -1444,18 +1444,18 @@ "SearchAll": "Pesquisar Todos", "UnmonitorSelected": "Não Monitorar os Selecionados", "CutoffUnmetNoItems": "Nenhum item com limite não atingido", - "MonitorSelected": "Monitorar Selecionados", + "MonitorSelected": "Monitorar selecionados", "SearchForAllMissingEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", "SearchSelected": "Pesquisar Selecionado", "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atingido", - "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar {appName} ou desabilitar todos os seus indexadores.", + "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar o {appName} ou desabilitar todos os seus indexadores.", "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", "SearchForCutoffUnmetEpisodes": "Pesquisar todos os episódios com limite não atingido", "SearchForCutoffUnmetEpisodesConfirmationCount": "Tem certeza que deseja pesquisar todos os episódios de {totalRecords} com limite não atingido?", "FormatAgeDay": "dia", "FormatAgeHours": "horas", "FormatDateTime": "{formattedDate} {formattedTime}", - "MonitorPilotEpisode": "Episódio Piloto", + "MonitorPilotEpisode": "Episódio piloto", "Tomorrow": "Amanhã", "FormatAgeDays": "dias", "FormatAgeHour": "hora", @@ -1473,8 +1473,8 @@ "AutoRedownloadFailedFromInteractiveSearchHelpText": "Procurar e tentar baixar automaticamente um lançamento diferente quando for obtido um lançamento com falha na pesquisa interativa", "AutoRedownloadFailed": "Falha no novo download", "AutoRedownloadFailedFromInteractiveSearch": "Falha no novo download usando a pesquisa interativa", - "ImportListSearchForMissingEpisodes": "Pesquisar Episódios Ausentes", - "ImportListSearchForMissingEpisodesHelpText": "Depois que a série for adicionada ao {appName}, procure automaticamente episódios ausentes", + "ImportListSearchForMissingEpisodes": "Pesquisar episódios ausentes", + "ImportListSearchForMissingEpisodesHelpText": "Depois que a série for adicionada ao {appName}, procurar automaticamente pelos episódios ausentes", "QueueFilterHasNoItems": "O filtro de fila selecionado não possui itens", "BlackholeFolderHelpText": "Pasta na qual o {appName} armazenará o arquivo {extension}", "Destination": "Destino", @@ -1493,7 +1493,7 @@ "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se ao {clientName}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar os downloads, deixe em branco para usar o local padrão do Transmission", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL de chamada remota do {clientName}, por exemplo, {url}. O padrão é \"{defaultUrl}\"", - "DownloadClientValidationAuthenticationFailureDetail": "Verifique o nome de usuário e a senha. Verifique também se o host que executa o {appName} não está impedido o acesso ao {clientName} pelas limitações da Lista de permissões na configuração do {clientName}.", + "DownloadClientValidationAuthenticationFailureDetail": "Verifique o nome de usuário e a senha. Verifique também se o host que executa o {appName} não está impedido o acesso ao {clientName} pelas limitações da lista de permissões na configuração do {clientName}.", "DownloadClientValidationSslConnectFailureDetail": "O {appName} não consegue se conectar ao {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar o {appName} e o {clientName} para não usar SSL.", "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status de descompactação: {unpackStatus} - Status de movimentação: {moveStatus} - Status do script: {scriptStatus} - Status de exclusão: {deleteStatus} - Status de marcação: {markStatus}", "PostImportCategory": "Categoria Pós-Importação", @@ -1509,10 +1509,10 @@ "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "O qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "O {appName} não poderá realizar o tratamento de download concluído conforme configurado. Para corrigir isso, no qBittorrent, acesse \"Ferramentas -> Opções... -> BitTorrent -> Limites de Semeadura\", e altere a opção de \"Remover\" para \"Parar\"", "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o ponto de extremidade do XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar \"Verificar antes do download\" afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda \"Abortar trabalhos que não podem ser concluídos\", pois é mais eficaz.", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a classificação de filmes para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar \"Check before download\" (Verificar antes do download) afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda \"Abortar trabalhos que não podem ser concluídos\", pois é mais eficaz.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desabilitar a classificação de filmes para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "O {appName} prefere que cada download tenha uma pasta separada. Com * anexado à pasta/caminho, o Sabnzbd não criará essas pastas de trabalho. Conserte isso no Sabnzbd.", - "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica ao {appName} evita conflitos com downloads não relacionados que não sejam do {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório \"[categoria]\" no diretório de saída.", + "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica ao {appName} evita conflitos com downloads não relacionados que não sejam do {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório \"[categoria]\" no diretório de destino.", "XmlRpcPath": "Caminho RPC XML", "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.", @@ -1570,16 +1570,16 @@ "DownloadClientQbittorrentValidationCategoryUnsupported": "A categoria não é suportada", "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila não habilitada", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "A fila de torrents não está habilitada nas configurações do qBittorrent. Habilite-a no qBittorrent ou selecione \"Último\" como prioridade.", - "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de semeadura. O {appName} lidará com a remoção automática de torrents com base nos critérios de semeadura atuais em Configurações -> Indexadores somente quando Remover concluído estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização do rTorrent, que pode ser usada em scripts do rTorrent para personalizar o comportamento.", + "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de semeadura. O {appName} lidará com a remoção automática de torrents com base nos critérios de semeadura atuais em Settings (Configurações) -> Indexers (Indexadores) somente quando Remove Completed (Remover concluído) estiver habilitado. · · Após a importação, ele também definirá {importedView} como uma visualização do rTorrent, que pode ser usada em scripts do rTorrent para personalizar o comportamento.", "DownloadClientRTorrentSettingsAddStopped": "Adicionar parado", "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitar esta opção adicionará os torrents e links magnéticos ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", "DownloadClientRTorrentSettingsUrlPath": "Caminho do URL", - "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção \"Verificar antes do download\" no Sabnbzd", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desabilite a opção \"Check before download\" (Verificar antes do download) no Sabnbzd", "DownloadClientSabnzbdValidationDevelopVersion": "Versão de desenvolvimento do Sabnzbd, assumindo a versão 3.0.0 ou superior.", - "DownloadClientSabnzbdValidationDevelopVersionDetail": "O {appName} pode não ser compatível com novos recursos adicionados ao SABnzbd ao executar versões de desenvolvimento.", - "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desativar classificação por data", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a classificação por data para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "O {appName} pode não ser compatível com novos recursos adicionados ao SABnzbd quando você executa versões de desenvolvimento.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desabilitar classificação por data", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desabilitar a classificação por data para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar classificação de filmes", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar classificação de séries", "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desabilitar a classificação de séries para a categoria usada pelo {appName} para evitar problemas de importação. Conserte isso no Sabnzbd.", @@ -1590,9 +1590,9 @@ "DownloadClientSettingsDestinationHelpText": "Especifica manualmente o destino do download, deixe em branco para usar o padrão", "DownloadClientSettingsInitialState": "Estado inicial", "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados ao {clientName}", - "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar há mais de 14 dias", - "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para o {appName} definir após importar o download. O {appName} não removerá torrents nessa categoria mesmo que a semeadura esteja concluída. Deixe em branco para manter a mesma categoria.", - "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar nos últimos 14 dias", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridade de uso ao obter episódios que foram ao ar há mais de 14 dias", + "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para o {appName} definir após importar o download. O {appName} não removerá torrents nessa categoria, mesmo que a semeadura esteja concluída. Deixe em branco para manter a mesma categoria.", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridade de uso ao obter episódios que foram ao ar nos últimos 14 dias", "DownloadClientSettingsOlderPriority": "Priorizar mais antigos", "DownloadClientSettingsRecentPriority": "Priorizar recentes", "DownloadClientUTorrentTorrentStateError": "O uTorrent está relatando um erro", @@ -1611,7 +1611,7 @@ "DownloadClientValidationUnableToConnectDetail": "Verifique o nome do host e a porta.", "DownloadClientValidationUnknownException": "Exceção desconhecida: {exception}", "DownloadClientValidationVerifySsl": "Verifique as configurações de SSL", - "DownloadClientValidationVerifySslDetail": "Verifique sua configuração SSL em {clientName} e {appName}", + "DownloadClientValidationVerifySslDetail": "Verifique a configuração de 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.", "DownloadStationStatusExtracting": "Extraindo: {progress}%", "TorrentBlackholeSaveMagnetFilesExtension": "Salvar Arquivos Magnet com Extensão", @@ -1621,213 +1621,213 @@ "TorrentBlackholeTorrentFolder": "Pasta do Torrent", "UseSsl": "Usar SSL", "UsenetBlackholeNzbFolder": "Pasta do Nzb", - "IndexerIPTorrentsSettingsFeedUrl": "URL do Feed", - "IndexerIPTorrentsSettingsFeedUrlHelpText": "A URL completa do feed RSS gerado pelo IPTorrents, usando apenas as categorias que você selecionou (HD, SD, x264, etc...)", - "IndexerSettingsAdditionalParameters": "Parâmetros Adicionais", - "IndexerSettingsAdditionalParametersNyaa": "Parâmetros Adicionais", + "IndexerIPTorrentsSettingsFeedUrl": "URL do feed", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "O URL completo do feed RSS gerado pelo IPTorrents, usando apenas as categorias que você selecionou (HD, SD, x264, etc...)", + "IndexerSettingsAdditionalParameters": "Parâmetros adicionais", + "IndexerSettingsAdditionalParametersNyaa": "Parâmetros adicionais", "IndexerSettingsAllowZeroSize": "Permitir tamanho zero", "IndexerSettingsAnimeCategories": "Categorias de anime", - "IndexerSettingsAnimeStandardFormatSearch": "Formato de Pesquisa Padrão para Anime", + "IndexerSettingsAnimeStandardFormatSearch": "Pesquisa de formato padrão para anime", "IndexerSettingsApiPath": "Caminho da API", "IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}", "IndexerSettingsApiUrl": "URL da API", - "IndexerSettingsApiUrlHelpText": "Não mude isso a menos que você saiba o que está fazendo. Já que sua chave API será enviada para esse host.", + "IndexerSettingsApiUrlHelpText": "Não mude isso a menos que você saiba o que está fazendo. Já que sua chave da API será enviada para esse host.", "IndexerSettingsCategories": "Categorias", - "IndexerSettingsCategoriesHelpText": "Lista suspensa, deixe em branco para desativar programas padrão/diários", + "IndexerSettingsCategoriesHelpText": "Lista suspensa, deixe em branco para desativar séries padrão/diárias", "IndexerSettingsCookie": "Cookie", - "IndexerSettingsMinimumSeeders": "Mínimo de Semeadores", - "IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semeadores necessário.", - "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsMinimumSeeders": "Mínimo de semeadores", + "IndexerSettingsMinimumSeedersHelpText": "Quantidade mínima de semeadores necessária.", + "IndexerSettingsPasskey": "Chave de acesso", "IndexerSettingsRssUrl": "URL do RSS", - "IndexerSettingsSeasonPackSeedTime": "Tempo de Seed de Pack de Temporada", - "IndexerSettingsSeedRatio": "Proporção de semeação", - "IndexerSettingsSeedTime": "Tempo de semeação", - "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser semeado antes de parar, vazio usa o padrão do cliente de download", - "IndexerSettingsWebsiteUrl": "URL do Website", - "IndexerValidationCloudFlareCaptchaExpired": "O token CloudFlare CAPTCHA expirou, atualize-o.", + "IndexerSettingsSeasonPackSeedTime": "Tempo de semeadura para pacotes de temporada", + "IndexerSettingsSeedRatio": "Proporção de semeadura", + "IndexerSettingsSeedTime": "Tempo de semeadura", + "IndexerSettingsSeedTimeHelpText": "Quanto tempo um torrent deve ser semeado antes de parar, deixe vazio para usar o padrão do cliente de download", + "IndexerSettingsWebsiteUrl": "URL do site", + "IndexerValidationCloudFlareCaptchaExpired": "O token do CloudFlare CAPTCHA expirou, atualize-o.", "IndexerValidationFeedNotSupported": "O feed do indexador não é compatível: {exceptionMessage}", - "IndexerValidationJackettAllNotSupportedHelpText": "Todos os endpoints de Jackett não são suportados. Adicione indexadores individualmente", - "IndexerValidationNoResultsInConfiguredCategories": "Consulta bem-sucedida, mas nenhum resultado nas categorias configuradas foi retornado do seu indexador. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", + "IndexerValidationJackettAllNotSupportedHelpText": "O ponto de extremidade \"all\" (tudo) do Jackett é incompatível. Adicione indexadores individualmente", + "IndexerValidationNoResultsInConfiguredCategories": "Consulta bem-sucedida, mas seu indexador não retornou nenhum resultado nas categorias configuradas. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", "IndexerValidationNoRssFeedQueryAvailable": "Nenhuma consulta de feed RSS disponível. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", "IndexerValidationRequestLimitReached": "Limite de solicitações atingido: {exceptionMessage}", - "IndexerValidationSearchParametersNotSupported": "O indexador não oferece suporte aos parâmetros de pesquisa obrigatórios", - "IndexerValidationUnableToConnectResolutionFailure": "Não é possível conectar-se à falha de conexão do indexador. Verifique sua conexão com o servidor e DNS do indexador. {exceptionMessage}.", - "IndexerValidationUnableToConnectServerUnavailable": "Não foi possível conectar-se ao indexador, o servidor do indexador não está disponível. Tente mais tarde. {exceptionMessage}.", + "IndexerValidationSearchParametersNotSupported": "O indexador não oferece suporte aos parâmetros de pesquisa necessários", + "IndexerValidationUnableToConnectResolutionFailure": "Não é possível conectar-se ao indexador devido a falha de conexão. Verifique sua conexão com o servidor e o DNS do indexador. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Não foi possível conectar-se ao indexador, o servidor do indexador está indisponível. Tente mais tarde. {exceptionMessage}.", "IndexerSettingsAdditionalNewznabParametersHelpText": "Observe que se você alterar a categoria, terá que adicionar regras obrigatórias/restritas sobre os subgrupos para evitar lançamentos em idiomas estrangeiros.", - "IndexerSettingsAllowZeroSizeHelpText": "Ativar isso permitirá que você use feeds que não especificam o tamanho do lançamento, mas tenha cuidado, pois verificações relacionadas ao tamanho não serão realizadas.", + "IndexerSettingsAllowZeroSizeHelpText": "Ativar isso permitirá que você use feeds que não especificam o tamanho do lançamento, mas tenha cuidado, pois não realizaremos verificações relacionadas ao tamanho.", "IndexerSettingsAnimeCategoriesHelpText": "Lista suspensa, deixe em branco para desativar o anime", - "IndexerSettingsAnimeStandardFormatSearchHelpText": "Pesquise também por animes usando a numeração padrão", - "IndexerSettingsCookieHelpText": "se o seu site exigir um cookie de login para acessar o RSS, você terá que recuperá-lo por meio de um navegador.", - "IndexerSettingsRssUrlHelpText": "Insira a URL de um feed RSS compatível com {indexer}", - "IndexerSettingsSeasonPackSeedTimeHelpText": "O tempo que um torrent de pack de temporada deve ser semeado antes de parar, vazio usa o padrão do cliente de download", - "IndexerSettingsSeedRatioHelpText": "A proporção que um torrent deve atingir antes de parar, vazio usa o padrão do cliente de download. A proporção deve ser de pelo menos 1,0 e seguir as regras dos indexadores", - "IndexerValidationCloudFlareCaptchaRequired": "Site protegido por CloudFlare CAPTCHA. É necessário um token CAPTCHA válido.", - "IndexerValidationInvalidApiKey": "Chave de API inválida", - "IndexerValidationJackettAllNotSupported": "Todos os endpoints de Jackett não são suportados. Adicione indexadores individualmente", - "IndexerValidationQuerySeasonEpisodesNotSupported": "O indexador não oferece suporte à consulta atual. Verifique se as categorias e/ou busca por temporadas/episódios são suportadas. Verifique o registro para mais detalhes.", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Pesquisar também por animes usando a numeração padrão", + "IndexerSettingsCookieHelpText": "Se o seu site exige um cookie de login para acessar o RSS, você terá que recuperá-lo por meio de um navegador.", + "IndexerSettingsRssUrlHelpText": "Insira o URL de um feed RSS compatível com {indexer}", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Quanto tempo um torrent de pacote de temporada deve ser semeado antes de parar, deixe vazio para usar o padrão do cliente de download", + "IndexerSettingsSeedRatioHelpText": "A proporção que um torrent deve atingir antes de parar, deixe vazio para usar o padrão do cliente de download. A proporção deve ser de pelo menos 1 e seguir as regras dos indexadores", + "IndexerValidationCloudFlareCaptchaRequired": "Site protegido por CloudFlare CAPTCHA. É necessário um token de CAPTCHA válido.", + "IndexerValidationInvalidApiKey": "Chave da API inválida", + "IndexerValidationJackettAllNotSupported": "O ponto de extremidade \"all\" (tudo) do Jackett é incompatível. Adicione indexadores individualmente", + "IndexerValidationQuerySeasonEpisodesNotSupported": "O indexador não oferece suporte à consulta atual. Verifique se as categorias e/ou busca por temporadas/episódios são suportadas. Verifique o log para saber mais.", "IndexerValidationTestAbortedDueToError": "O teste foi abortado devido a um erro: {exceptionMessage}", - "IndexerValidationUnableToConnect": "Não foi possível conectar-se ao indexador: {exceptionMessage}. Verifique o registro em torno deste erro para obter detalhes", + "IndexerValidationUnableToConnect": "Não foi possível conectar-se ao indexador: {exceptionMessage}. Verifique o log em torno deste erro para saber mais", "IndexerValidationUnableToConnectHttpError": "Não foi possível conectar-se ao indexador. Verifique suas configurações de DNS e certifique-se de que o IPv6 esteja funcionando ou desativado. {exceptionMessage}.", "IndexerValidationUnableToConnectInvalidCredentials": "Não foi possível conectar-se ao indexador, credenciais inválidas. {exceptionMessage}.", "IndexerValidationUnableToConnectTimeout": "Não foi possível conectar-se ao indexador, possivelmente devido ao tempo limite. Tente novamente ou verifique as configurações de rede. {exceptionMessage}.", "IndexerHDBitsSettingsCategories": "Categorias", - "IndexerHDBitsSettingsCategoriesHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsCategoriesHelpText": "Se não for especificado, todas as opções serão usadas.", "IndexerHDBitsSettingsCodecs": "Codecs", - "IndexerHDBitsSettingsCodecsHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsCodecsHelpText": "Se não for especificado, todas as opções serão usadas.", "IndexerHDBitsSettingsMediums": "Meios", - "IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsMediumsHelpText": "Se não for especificado, todas as opções serão usadas.", "ClearBlocklist": "Limpar lista de bloqueio", - "MonitorRecentEpisodesDescription": "Monitore episódios exibidos nos últimos 90 dias e episódios futuros", + "MonitorRecentEpisodesDescription": "Monitorar episódios exibidos nos últimos 90 dias e episódios futuros", "ClearBlocklistMessageText": "Tem certeza de que deseja limpar todos os itens da lista de bloqueio?", "PasswordConfirmation": "Confirmação Da Senha", - "MonitorPilotEpisodeDescription": "Monitore apenas o primeiro episódio da primeira temporada", - "MonitorNoNewSeasonsDescription": "Não monitore nenhuma nova temporada automaticamente", - "MonitorAllSeasons": "Todas as Temporadas", + "MonitorPilotEpisodeDescription": "Monitorar apenas o primeiro episódio da primeira temporada", + "MonitorNoNewSeasonsDescription": "Não monitorar nenhuma nova temporada automaticamente", + "MonitorAllSeasons": "Todas as temporadas", "MonitorAllSeasonsDescription": "Monitorar todas as novas temporadas automaticamente", - "MonitorLastSeason": "Última Temporada", + "MonitorLastSeason": "Última temporada", "MonitorLastSeasonDescription": "Monitorar todos os episódios da última temporada", - "MonitorNewSeasons": "Monitorar Novas Temporadas", + "MonitorNewSeasons": "Monitorar novas temporadas", "MonitorNewSeasonsHelpText": "Quais novas temporadas devem ser monitoradas automaticamente", - "MonitorRecentEpisodes": "Episódios Recentes", + "MonitorRecentEpisodes": "Episódios recentes", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirme a nova senha", - "MonitorNoNewSeasons": "Sem Novas Temporadas", - "MonitorNewItems": "Monitorar Novos Itens", + "MonitorNoNewSeasons": "Sem novas temporadas", + "MonitorNewItems": "Monitorar novos itens", "DownloadClientQbittorrentSettingsContentLayout": "Layout de conteúdo", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Se devemos usar o layout de conteúdo configurado do qBittorrent, o layout original do torrent ou sempre criar uma subpasta (qBittorrent 4.3.2+)", "AddRootFolderError": "Não foi possível adicionar a pasta raiz", - "NotificationsAppriseSettingsNotificationType": "Informar Tipo de Notificação", - "NotificationsAppriseSettingsPasswordHelpText": "Senha de Autenticação Básica HTTP", - "NotificationsAppriseSettingsServerUrl": "Informar URL do Servidor", - "NotificationsAppriseSettingsServerUrlHelpText": "Informe o URL do servidor, incluindo http(s):// e porta, se necessário", - "NotificationsAppriseSettingsStatelessUrls": "Informar URLs sem estado", - "NotificationsAppriseSettingsTags": "Informar Etiquetas", - "NotificationsAppriseSettingsTagsHelpText": "Opcionalmente, notifique apenas aqueles etiquetados de acordo.", - "NotificationsAppriseSettingsUsernameHelpText": "Nome de Usuário de Autenticação Básica HTTP", + "NotificationsAppriseSettingsNotificationType": "Tipo de notificação do Apprise", + "NotificationsAppriseSettingsPasswordHelpText": "Senha de autenticação HTTP básica", + "NotificationsAppriseSettingsServerUrl": "URL do servidor do Apprise", + "NotificationsAppriseSettingsServerUrlHelpText": "URL do servidor do Apprise, incluindo http(s):// e porta, se necessário", + "NotificationsAppriseSettingsStatelessUrls": "URLs sem estado do Apprise", + "NotificationsAppriseSettingsTags": "Etiquetas do Apprise", + "NotificationsAppriseSettingsTagsHelpText": "Opcionalmente, notificar apenas os clientes com etiquetas correspondentes.", + "NotificationsAppriseSettingsUsernameHelpText": "Nome de usuário da autenticação HTTP básica", "NotificationsCustomScriptSettingsArguments": "Argumentos", "NotificationsCustomScriptSettingsArgumentsHelpText": "Argumentos para passar para o script", - "NotificationsCustomScriptSettingsName": "Script Personalizado", - "NotificationsCustomScriptValidationFileDoesNotExist": "Arquivo não existe", + "NotificationsCustomScriptSettingsName": "Script personalizado", + "NotificationsCustomScriptValidationFileDoesNotExist": "O arquivo não existe", "NotificationsDiscordSettingsAuthor": "Autor", - "NotificationsDiscordSettingsAuthorHelpText": "Substitua o autor incorporado exibido para esta notificação. Em branco é o nome da instância", + "NotificationsDiscordSettingsAuthorHelpText": "Substituir o autor incorporado exibido para esta notificação. Deixe em branco para exibir o nome da instância", "NotificationsDiscordSettingsAvatar": "Avatar", - "NotificationsDiscordSettingsAvatarHelpText": "Altere o avatar usado para mensagens desta integração", - "NotificationsCustomScriptSettingsProviderMessage": "O teste executará o script com EventType definido como {eventTypeTest}, certifique-se de que seu script lide com isso corretamente", - "NotificationsDiscordSettingsOnGrabFields": "Em Campos de Captura", - "NotificationsDiscordSettingsOnImportFields": "Em Campos de Importação", - "NotificationsDiscordSettingsOnImportFieldsHelpText": "Altere os campos passados para esta notificação 'ao importar'", - "NotificationsDiscordSettingsOnManualInteractionFields": "Em Campos de Interação Manual", - "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Altere os campos passados para esta notificação de 'na interação manual'", - "NotificationsDiscordSettingsUsernameHelpText": "O nome de usuário para postar, o padrão é o webhook padrão do Discord", + "NotificationsDiscordSettingsAvatarHelpText": "Alterar o avatar usado para mensagens desta integração", + "NotificationsCustomScriptSettingsProviderMessage": "O teste executará o script com EventType definido como {eventTypeTest}, certifique-se de que seu script consiga lidar com isso corretamente", + "NotificationsDiscordSettingsOnGrabFields": "Campos Ao obter", + "NotificationsDiscordSettingsOnImportFields": "Campos Ao importar", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Alterar os campos passados para esta notificação \"ao importar\"", + "NotificationsDiscordSettingsOnManualInteractionFields": "Campos Na interação manual", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Alterar os campos passados para esta notificação \"na interação manual\"", + "NotificationsDiscordSettingsUsernameHelpText": "O nome de usuário que postará, o padrão é o webhook padrão do Discord", "NotificationsDiscordSettingsWebhookUrlHelpText": "URL do webhook do canal do Discord", "NotificationsEmailSettingsBccAddress": "Endereço(s) CCO", - "NotificationsEmailSettingsBccAddressHelpText": "Lista separada por vírgulas de destinatários de e-mail Cco", + "NotificationsEmailSettingsBccAddressHelpText": "Lista separada por vírgulas de destinatários para copiar o e-mail", "NotificationsEmailSettingsCcAddress": "Endereço(s) CC", - "NotificationsEmailSettingsCcAddressHelpText": "Lista separada por vírgulas de destinatários de e-mail CC", - "NotificationsEmailSettingsFromAddress": "Do Endereço", - "NotificationsEmailSettingsName": "Email", - "NotificationsEmailSettingsRecipientAddress": "Endereço(s) do Destinatário", - "NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por vírgulas de destinatários de e-mail", - "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Altere os campos passados para esta notificação 'ao capturar'", + "NotificationsEmailSettingsCcAddressHelpText": "Lista separada por vírgulas de destinatários para copiar o e-mail", + "NotificationsEmailSettingsFromAddress": "Endereço do remetente", + "NotificationsEmailSettingsName": "E-mail", + "NotificationsEmailSettingsRecipientAddress": "Endereço(s) do(s) destinatário(s)", + "NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por vírgulas dos destinatários do e-mail", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Alterar os campos passados para esta notificação \"ao obter\"", "NotificationsEmailSettingsServer": "Servidor", "NotificationsEmailSettingsServerHelpText": "Nome do host ou IP do servidor de e-mail", - "NotificationsEmbySettingsSendNotifications": "Enviar Notificações", - "NotificationsEmbySettingsUpdateLibraryHelpText": "Atualizar Biblioteca ao Importar, Renomear ou Excluir", - "NotificationsGotifySettingIncludeSeriesPoster": "Incluir Pôster da Série", - "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluir Pôster da Série na Mensagem", + "NotificationsEmbySettingsSendNotifications": "Enviar notificações", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Atualizar biblioteca ao importar, renomear ou excluir", + "NotificationsGotifySettingIncludeSeriesPoster": "Incluir pôster da série", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluir pôster da série na mensagem", "NotificationsGotifySettingsAppToken": "Token do aplicativo", "NotificationsGotifySettingsAppTokenHelpText": "O token do aplicativo gerado pelo Gotify", "NotificationsGotifySettingsPriorityHelpText": "Prioridade da notificação", - "NotificationsGotifySettingsServer": "Servidor Gotify", - "NotificationsJoinSettingsApiKeyHelpText": "A chave API das configurações da sua conta Join (clique no botão Join API).", - "NotificationsJoinSettingsDeviceIds": "IDs de Dispositivos", - "NotificationsJoinSettingsDeviceNames": "Nomes de Dispositivos", + "NotificationsGotifySettingsServer": "Servidor do Gotify", + "NotificationsJoinSettingsApiKeyHelpText": "A chave da API das configurações da sua conta do Join (clique no botão Join API).", + "NotificationsJoinSettingsDeviceIds": "IDs de dispositivos", + "NotificationsJoinSettingsDeviceNames": "Nomes de dispositivos", "NotificationsJoinSettingsDeviceNamesHelpText": "Lista separada por vírgulas de nomes completos ou parciais de dispositivos para os quais você gostaria de enviar notificações. Se não for definido, todos os dispositivos receberão notificações.", - "NotificationsJoinSettingsNotificationPriority": "Prioridade da Notificação", - "NotificationsJoinValidationInvalidDeviceId": "Os IDs dos dispositivos parecem inválidos.", - "NotificationsKodiSettingAlwaysUpdate": "Sempre Atualizar", + "NotificationsJoinSettingsNotificationPriority": "Prioridade da notificação", + "NotificationsJoinValidationInvalidDeviceId": "Os IDs dos dispositivos parecem ser inválidos.", + "NotificationsKodiSettingAlwaysUpdate": "Sempre atualizar", "NotificationsKodiSettingAlwaysUpdateHelpText": "Atualizar a biblioteca mesmo quando um vídeo está sendo reproduzido?", - "NotificationsKodiSettingsCleanLibrary": "Limpar Biblioteca", + "NotificationsKodiSettingsCleanLibrary": "Limpar biblioteca", "NotificationsKodiSettingsCleanLibraryHelpText": "Limpar biblioteca após atualização", - "NotificationsKodiSettingsDisplayTime": "Tempo de Exibição", - "NotificationsKodiSettingsGuiNotification": "Notificação GUI", - "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca em Importar e Renomear?", - "NotificationsMailgunSettingsApiKeyHelpText": "A chave API gerada pelo MailGun", - "NotificationsMailgunSettingsSenderDomain": "Domínio do Remetente", - "NotificationsMailgunSettingsUseEuEndpoint": "Usar EU Endpoint", - "NotificationsMailgunSettingsUseEuEndpointHelpText": "Habilite para usar o endpoint EU MailGun", - "NotificationsNotifiarrSettingsApiKeyHelpText": "Sua chave API do seu perfil", + "NotificationsKodiSettingsDisplayTime": "Tempo de exibição", + "NotificationsKodiSettingsGuiNotification": "Notificação na interface gráfica", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca ao importar e renomear?", + "NotificationsMailgunSettingsApiKeyHelpText": "A chave da API gerada pelo MailGun", + "NotificationsMailgunSettingsSenderDomain": "Domínio do remetente", + "NotificationsMailgunSettingsUseEuEndpoint": "Usar ponto de extremidade na Europa", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Habilitar para usar o ponto de extremidade do MailGun na Europa", + "NotificationsNotifiarrSettingsApiKeyHelpText": "A chave da API do seu perfil", "NotificationsNtfySettingsAccessToken": "Token de Acesso", - "NotificationsNtfySettingsClickUrl": "Clique na URL", + "NotificationsNtfySettingsClickUrl": "Ao clicar no URL", "NotificationsNtfySettingsClickUrlHelpText": "Link opcional quando o usuário clica na notificação", "NotificationsNtfySettingsPasswordHelpText": "Senha opcional", - "NotificationsNtfySettingsServerUrl": "URL do Servidor", - "NotificationsNtfySettingsServerUrlHelpText": "Deixe em branco para usar servidor público ({url})", - "NotificationsNtfySettingsTagsEmojis": "Etiquetas e Emojis Ntfy", + "NotificationsNtfySettingsServerUrl": "URL do servidor", + "NotificationsNtfySettingsServerUrlHelpText": "Deixe em branco para usar o servidor público ({url})", + "NotificationsNtfySettingsTagsEmojis": "Etiquetas e emojis do Ntfy", "NotificationsNtfySettingsTopics": "Tópicos", "NotificationsNtfySettingsUsernameHelpText": "Nome de usuário opcional", "NotificationsNtfyValidationAuthorizationRequired": "É necessária autorização", - "NotificationsPlexSettingsAuthToken": "Token de Autenticação", + "NotificationsPlexSettingsAuthToken": "Token de autenticação", "NotificationsPlexSettingsAuthenticateWithPlexTv": "Autenticar com Plex.tv", "NotificationsPlexValidationNoTvLibraryFound": "É necessária pelo menos uma biblioteca de TV", - "NotificationsPushBulletSettingSenderId": "ID do Remetente", + "NotificationsPushBulletSettingSenderId": "ID do remetente", "NotificationsPushBulletSettingsAccessToken": "Token de Acesso", "NotificationsPushBulletSettingsChannelTags": "Etiquetas do canal", "NotificationsPushBulletSettingsChannelTagsHelpText": "Lista de etiquetas do canal para enviar notificações", "NotificationsPushBulletSettingsDeviceIds": "IDs de Dispositivos", - "NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivos (deixe em branco para enviar a todos os dispositivos)", - "NotificationsPushcutSettingsApiKeyHelpText": "As chaves de API podem ser gerenciadas na visualização da conta do aplicativo Pushcut", - "NotificationsPushcutSettingsNotificationName": "Nome da Notificação", - "NotificationsPushcutSettingsTimeSensitive": "Sensível ao Tempo", - "NotificationsPushcutSettingsTimeSensitiveHelpText": "Habilitar para marcar a notificação como \"Sensível ao Tempo\"", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivos (deixe em branco para enviar para todos os dispositivos)", + "NotificationsPushcutSettingsApiKeyHelpText": "As chaves da API podem ser gerenciadas na visualização da conta do aplicativo Pushcut", + "NotificationsPushcutSettingsNotificationName": "Nome da notificação", + "NotificationsPushcutSettingsTimeSensitive": "Urgente", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Habilitar para marcar a notificação como \"Urgente\"", "NotificationsPushoverSettingsDevices": "Dispositivos", "NotificationsPushoverSettingsDevicesHelpText": "Lista de nomes de dispositivos (deixe em branco para enviar para todos os dispositivos)", "NotificationsPushoverSettingsExpire": "Expirar", "NotificationsPushoverSettingsRetry": "Repetir", - "NotificationsPushoverSettingsRetryHelpText": "Intervalo para repetir alertas de emergência, mínimo de 30 segundos", + "NotificationsPushoverSettingsRetryHelpText": "Intervalo para repetir o envio de alertas de emergência, mínimo de 30 segundos", "NotificationsPushoverSettingsSound": "Som", "NotificationsPushoverSettingsSoundHelpText": "Som da notificação, deixe em branco para usar o padrão", - "NotificationsPushoverSettingsUserKey": "Chave do Usuário", - "NotificationsSendGridSettingsApiKeyHelpText": "A chave API gerada pelo SendGrid", - "NotificationsSettingsUpdateLibrary": "Atualizar Biblioteca", - "NotificationsSettingsUpdateMapPathsFrom": "Mapear Caminhos De", - "NotificationsSettingsUpdateMapPathsTo": "Mapear Caminhos Para", - "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "Caminho {serviceName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", - "NotificationsSettingsUseSslHelpText": "Conecte-se a {serviceName} por HTTPS em vez de HTTP", + "NotificationsPushoverSettingsUserKey": "Chave do usuário", + "NotificationsSendGridSettingsApiKeyHelpText": "A chave da API gerada pelo SendGrid", + "NotificationsSettingsUpdateLibrary": "Atualizar biblioteca", + "NotificationsSettingsUpdateMapPathsFrom": "Mapear caminhos de", + "NotificationsSettingsUpdateMapPathsTo": "Mapear caminhos para", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "Caminho do {serviceName}, usado para alterar caminhos de séries quando o {serviceName} vê a localização do caminho da biblioteca de forma diferente do {appName} (requer \"Atualizar biblioteca\")", + "NotificationsSettingsUseSslHelpText": "Conectar a {serviceName} por HTTPS em vez de HTTP", "NotificationsSettingsWebhookMethod": "Método", "NotificationsSettingsWebhookMethodHelpText": "Qual método HTTP usar para enviar ao Webservice", - "NotificationsSettingsWebhookUrl": "URL do Webhook", - "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID do Grupo/Número de Telefone do destinatário", - "NotificationsSignalSettingsPasswordHelpText": "Senha usada para autenticar solicitações para signal-api", - "NotificationsSignalSettingsSenderNumber": "Número do Remetente", - "NotificationsSignalSettingsUsernameHelpText": "Nome de usuário usado para autenticar solicitações para signal-api", + "NotificationsSettingsWebhookUrl": "URL do webhook", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID do grupo/número de telefone do destinatário", + "NotificationsSignalSettingsPasswordHelpText": "Senha usada para autenticar solicitações para a signal-api", + "NotificationsSignalSettingsSenderNumber": "Número do remetente", + "NotificationsSignalSettingsUsernameHelpText": "Nome de usuário usado para autenticar solicitações para a signal-api", "NotificationsSignalValidationSslRequired": "SSL parece ser necessário", "NotificationsSimplepushSettingsEvent": "Evento", - "NotificationsSimplepushSettingsEventHelpText": "Personalize o comportamento das notificações push", + "NotificationsSimplepushSettingsEventHelpText": "Personalizar o comportamento das notificações por push", "NotificationsSimplepushSettingsKey": "Chave", "NotificationsSlackSettingsChannel": "Canal", "NotificationsSlackSettingsIcon": "Ícone", - "NotificationsSlackSettingsIconHelpText": "Altere o ícone usado para mensagens postadas no Slack (Emoji ou URL)", - "NotificationsSlackSettingsUsernameHelpText": "Nome de usuário para postar no Slack como", - "NotificationsSlackSettingsWebhookUrlHelpText": "URL do webhook do canal Slack", - "NotificationsSynologyValidationInvalidOs": "Deve ser uma Synology", - "NotificationsSynologyValidationTestFailed": "Não é um Synology ou synoindex não disponível", - "NotificationsTelegramSettingsChatId": "ID do Bate-papo", + "NotificationsSlackSettingsIconHelpText": "Alterar o ícone usado para mensagens postadas no Slack (Emoji ou URL)", + "NotificationsSlackSettingsUsernameHelpText": "Nome de usuário que posta no Slack", + "NotificationsSlackSettingsWebhookUrlHelpText": "URL do webhook do canal do Slack", + "NotificationsSynologyValidationInvalidOs": "Deve ser um Synology", + "NotificationsSynologyValidationTestFailed": "Não é um Synology ou o synoindex está indisponível", + "NotificationsTelegramSettingsChatId": "ID do bate-papo", "NotificationsTelegramSettingsChatIdHelpText": "Você deve iniciar uma conversa com o bot ou adicioná-lo ao seu grupo para receber mensagens", - "NotificationsTelegramSettingsSendSilently": "Enviar Silenciosamente", - "NotificationsTelegramSettingsTopicId": "ID do Tópico", - "NotificationsTelegramSettingsTopicIdHelpText": "Especifique um ID de tópico para enviar notificações para esse tópico. Deixe em branco para usar o tópico geral (somente Supergrupos)", + "NotificationsTelegramSettingsSendSilently": "Enviar silenciosamente", + "NotificationsTelegramSettingsTopicId": "ID do tópico", + "NotificationsTelegramSettingsTopicIdHelpText": "Especifique um ID de tópico para o qual enviar notificações. Deixe em branco para usar o tópico geral (somente Supergroups)", "NotificationsTraktSettingsAccessToken": "Token de Acesso", "NotificationsTraktSettingsAuthUser": "Autenticação de Usuário", "NotificationsTraktSettingsAuthenticateWithTrakt": "Autenticar com Trakt", "NotificationsTraktSettingsExpires": "Expira", "NotificationsTraktSettingsRefreshToken": "Atualizar Token", "NotificationsTwitterSettingsAccessToken": "Token de Acesso", - "NotificationsTwitterSettingsAccessTokenSecret": "Segredo do Token de Acesso", - "NotificationsTwitterSettingsConsumerKey": "Chave do Consumidor", + "NotificationsTwitterSettingsAccessTokenSecret": "Segredo do token de acesso", + "NotificationsTwitterSettingsConsumerKey": "Chave do consumidor", "NotificationsTwitterSettingsConsumerKeyHelpText": "Chave do consumidor de um aplicativo do Twitter", - "NotificationsTwitterSettingsConsumerSecret": "Segredo do Consumidor", + "NotificationsTwitterSettingsConsumerSecret": "Segredo do consumidor", "NotificationsTwitterSettingsConsumerSecretHelpText": "Segredo do consumidor de um aplicativo do Twitter", - "NotificationsTwitterSettingsDirectMessage": "Mensagem Direta", + "NotificationsTwitterSettingsDirectMessage": "Mensagem direta", "NotificationsTwitterSettingsMention": "Mencionar", "NotificationsTwitterSettingsMentionHelpText": "Mencione este usuário nos tweets enviados", "NotificationsValidationInvalidAccessToken": "O token de acesso é inválido", @@ -1838,30 +1838,30 @@ "NotificationsValidationUnableToConnectToService": "Não foi possível conectar-se a {serviceName}", "NotificationsValidationUnableToSendTestMessage": "Não foi possível enviar a mensagem de teste: {exceptionMessage}", "NotificationsValidationUnableToSendTestMessageApiResponse": "Não foi possível enviar mensagem de teste. Resposta da API: {error}", - "NotificationsAppriseSettingsStatelessUrlsHelpText": "Uma ou mais URLs separadas por vírgulas identificando para onde a notificação deve ser enviada. Deixe em branco se o armazenamento persistente for usado.", - "NotificationsEmbySettingsSendNotificationsHelpText": "Faça com que Emby envie notificações para provedores configurados. Não compatível com Jellyfin.", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Um ou mais URLs separados por vírgulas que identificam para onde enviar a notificação. Deixe em branco se usar armazenamento persistente.", + "NotificationsEmbySettingsSendNotificationsHelpText": "Faça com que o Emby envie notificações para os provedores configurados. Não compatível com Jellyfin.", "NotificationsJoinSettingsDeviceIdsHelpText": "Obsoleto, use nomes de dispositivos. Lista separada por vírgulas de IDs de dispositivos para os quais você gostaria de enviar notificações. Se não for definido, todos os dispositivos receberão notificações.", - "NotificationsTwitterSettingsConnectToTwitter": "Conecte-se ao Twitter / X", - "NotificationsTelegramSettingsBotToken": "Token do Bot", - "NotificationsGotifySettingsServerHelpText": "URL do servidor Gotify, incluindo http(s):// e porta, se necessário", - "NotificationsAppriseSettingsConfigurationKey": "Informar Chave de Configuração", - "NotificationsAppriseSettingsConfigurationKeyHelpText": "Chave de configuração para a solução de armazenamento persistente. Deixe em branco se URLs sem estado forem usadas.", + "NotificationsTwitterSettingsConnectToTwitter": "Conectar ao Twitter/X", + "NotificationsTelegramSettingsBotToken": "Token do bot", + "NotificationsGotifySettingsServerHelpText": "URL do servidor do Gotify, incluindo http(s):// e porta, se necessário", + "NotificationsAppriseSettingsConfigurationKey": "Chave de configuração do Apprise", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Chave de configuração para a solução de armazenamento persistente. Deixe em branco se usar URLs sem estado.", "NotificationsTwitterSettingsDirectMessageHelpText": "Envie uma mensagem direta em vez de uma mensagem pública", "NotificationsValidationInvalidAuthenticationToken": "O token de autenticação é inválido", "NotificationsKodiSettingsDisplayTimeHelpText": "Por quanto tempo a notificação será exibida (em segundos)", "NotificationsNtfySettingsAccessTokenHelpText": "Autorização opcional baseada em token. Tem prioridade sobre nome de usuário/senha", "NotificationsValidationInvalidHttpCredentials": "As credenciais de autenticação HTTP são inválidas: {exceptionMessage}", - "NotificationsValidationUnableToConnectToApi": "Não foi possível conectar-se à API {service}. Falha na conexão do servidor: ({responseCode}) {exceptionMessage}", - "NotificationsNtfySettingsTagsEmojisHelpText": "Lista opcional de etiquetas ou emojis para usar", - "NotificationsNtfySettingsTopicsHelpText": "Lista de tópicos para enviar notificações", + "NotificationsValidationUnableToConnectToApi": "Não foi possível conectar-se à API do {service}. Falha na conexão com o servidor: ({responseCode}) {exceptionMessage}", + "NotificationsNtfySettingsTagsEmojisHelpText": "Lista opcional de etiquetas ou emojis a usar", + "NotificationsNtfySettingsTopicsHelpText": "Lista de tópicos sobre os quais enviar notificações", "NotificationsPushBulletSettingSenderIdHelpText": "O ID do dispositivo para enviar notificações, use device_iden no URL do dispositivo em pushbullet.com (deixe em branco para enviar de você mesmo)", "NotificationsPushcutSettingsNotificationNameHelpText": "Nome da notificação na aba Notificações do aplicativo Pushcut", - "NotificationsPushoverSettingsExpireHelpText": "Tempo máximo para tentar novamente alertas de emergência, máximo de 86.400 segundos\"", - "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "Caminho {appName}, usado para modificar caminhos de série quando {serviceName} vê a localização do caminho da biblioteca de forma diferente de {appName} (requer 'Atualizar Biblioteca')", - "NotificationsSignalSettingsGroupIdPhoneNumber": "ID do Grupo/Número de Telefone", - "NotificationsSignalSettingsSenderNumberHelpText": "Número de telefone do registro do remetente no signal-api", + "NotificationsPushoverSettingsExpireHelpText": "Tempo máximo para tentar enviar alertas de emergência novamente, máximo de 86.400 segundos\"", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "Caminho do {appName}, usado para alterar caminhos de séries quando o {serviceName} vê a localização do caminho da biblioteca de forma diferente do {appName} (requer \"Atualizar biblioteca\")", + "NotificationsSignalSettingsGroupIdPhoneNumber": "ID do grupo/número de telefone", + "NotificationsSignalSettingsSenderNumberHelpText": "Número de telefone do registro do remetente na signal-api", "NotificationsSlackSettingsChannelHelpText": "Substitui o canal padrão para o webhook de entrada (#other-channel)", - "NotificationsSynologySettingsUpdateLibraryHelpText": "Chame synoindex no localhost para atualizar um arquivo de biblioteca", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Chamar o synoindex no localhost para atualizar um arquivo da biblioteca", "NotificationsTelegramSettingsSendSilentlyHelpText": "Envia a mensagem silenciosamente. Os usuários receberão uma notificação sem som", "EpisodeFileMissingTooltip": "Arquivo do episódio ausente", "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2", @@ -1869,11 +1869,11 @@ "IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeitar hashes de torrent bloqueados durante a captura", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Se um torrent for bloqueado por hash, pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.", "ImportListsSimklSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", - "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Séries Mais Assistidas por Semana", - "ImportListsTraktSettingsPopularListTypeTopYearShows": "Séries Mais Assistidas por Ano", - "ImportListsTraktSettingsUserListTypeWatched": "Lista de Assistido pelo Usuário", - "ImportListsTraktSettingsWatchedListFilter": "Filtrar Lista de Assistido", - "ImportListsValidationUnableToConnectException": "Não foi possível conectar-se à lista de importação: {exceptionMessage}. Verifique o log em torno desse erro para obter detalhes.", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Séries mais assistidas por semana", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Séries mais assistidas por ano", + "ImportListsTraktSettingsUserListTypeWatched": "Lista de assistidos do usuário", + "ImportListsTraktSettingsWatchedListFilter": "Filtrar lista de assistidos", + "ImportListsValidationUnableToConnectException": "Não foi possível conectar-se à lista de importação: {exceptionMessage}. Verifique o log em torno desse erro para saber mais.", "AutoTaggingSpecificationGenre": "Gênero(s)", "AutoTaggingSpecificationMaximumYear": "Ano máximo", "AutoTaggingSpecificationMinimumYear": "Ano mínimo", @@ -1893,115 +1893,115 @@ "CustomFormatsSpecificationResolution": "Resolução", "CustomFormatsSpecificationSource": "Origem", "ImportListsAniListSettingsAuthenticateWithAniList": "Autenticar com AniList", - "ImportListsAniListSettingsImportCancelled": "Importação Cancelada", - "ImportListsAniListSettingsImportCancelledHelpText": "Mídia: Série foi cancelada", - "ImportListsAniListSettingsImportCompleted": "Importação Concluída", - "ImportListsAniListSettingsImportCompletedHelpText": "Lista: Assistindo Concluídas", - "ImportListsAniListSettingsImportDropped": "Importação Descartada", - "ImportListsAniListSettingsImportDroppedHelpText": "Lista: Descartada", - "ImportListsAniListSettingsImportFinished": "Importação Concluída", + "ImportListsAniListSettingsImportCancelled": "Importar cancelada", + "ImportListsAniListSettingsImportCancelledHelpText": "Mídia: série foi cancelada", + "ImportListsAniListSettingsImportCompleted": "Importar concluídos", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: séries assistidas", + "ImportListsAniListSettingsImportDropped": "Importar descartadas", + "ImportListsAniListSettingsImportDroppedHelpText": "Lista: descartada", + "ImportListsAniListSettingsImportFinished": "Importar finalizados", "ImportListsAniListSettingsImportFinishedHelpText": "Mídia: todos os episódios foram ao ar", - "ImportListsAniListSettingsImportHiatus": "Importação em Hiato", - "ImportListsAniListSettingsImportHiatusHelpText": "Mídia: Série em Hiato", - "ImportListsAniListSettingsImportNotYetReleased": "Importação do Ainda Não Lançado", - "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Mídia: A exibição ainda não começou", - "ImportListsAniListSettingsImportPaused": "Importação Pausada", - "ImportListsAniListSettingsImportPausedHelpText": "Lista: Em espera", - "ImportListsAniListSettingsImportPlanning": "Importação em Planejamento", - "ImportListsAniListSettingsImportPlanningHelpText": "Lista: Planejando Assistir", - "ImportListsAniListSettingsImportReleasing": "Importar em Lançamento", + "ImportListsAniListSettingsImportHiatus": "Importar em hiato", + "ImportListsAniListSettingsImportHiatusHelpText": "Mídia: a série está em hiato", + "ImportListsAniListSettingsImportNotYetReleased": "Importar ainda não lançada", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Mídia: a exibição ainda não começou", + "ImportListsAniListSettingsImportPaused": "Importar pausados", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: em espera", + "ImportListsAniListSettingsImportPlanning": "Importar planejados", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: planeja assistir", + "ImportListsAniListSettingsImportReleasing": "Importar em lançamento", "ImportListsAniListSettingsImportReleasingHelpText": "Mídia: atualmente exibindo novos episódios", - "ImportListsAniListSettingsImportRepeating": "Importar Repetição", - "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: Atualmente Assistindo Novamente", - "ImportListsAniListSettingsImportWatchingHelpText": "Lista: Atualmente Assistindo", - "ImportListsAniListSettingsUsernameHelpText": "Nome de usuário da lista a ser importada", - "ImportListsCustomListSettingsName": "Lista Personalizada", - "ImportListsAniListSettingsImportWatching": "Importar Assistindo", - "ImportListsCustomListSettingsUrl": "URL da Lista", + "ImportListsAniListSettingsImportRepeating": "Importar repetindo", + "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: atualmente assistindo novamente", + "ImportListsAniListSettingsImportWatchingHelpText": "Lista: atualmente assistindo", + "ImportListsAniListSettingsUsernameHelpText": "Nome de usuário da lista a importar", + "ImportListsCustomListSettingsName": "Lista personalizada", + "ImportListsAniListSettingsImportWatching": "Importar assistindo", + "ImportListsCustomListSettingsUrl": "URL da lista", "ImportListsCustomListSettingsUrlHelpText": "O URL da lista de séries", "ImportListsCustomListValidationAuthenticationFailure": "Falha de autenticação", "ImportListsCustomListValidationConnectionError": "Não foi possível fazer a solicitação para esse URL. Código de status: {exceptionStatusCode}", - "ImportListsImdbSettingsListId": "ID da Lista", - "ImportListsImdbSettingsListIdHelpText": "ID da lista IMDb (por exemplo, ls12345678)", + "ImportListsImdbSettingsListId": "ID da lista", + "ImportListsImdbSettingsListIdHelpText": "ID da lista do IMDb (p. ex., ls12345678)", "ImportListsPlexSettingsAuthenticateWithPlex": "Autenticar com Plex.tv", - "ImportListsPlexSettingsWatchlistName": "Plex Para Assistir", - "ImportListsPlexSettingsWatchlistRSSName": "Plex Para Assistir RSS", - "ImportListsSettingsAccessToken": "Token de Acesso", - "ImportListsSettingsAuthUser": "Usuário de Autenticação", - "ImportListsSettingsExpires": "Expirar", - "ImportListsSettingsRefreshToken": "Atualizar Token", + "ImportListsPlexSettingsWatchlistName": "Lista de Observação do Plex", + "ImportListsPlexSettingsWatchlistRSSName": "RSS da Lista de Observação do Plex", + "ImportListsSettingsAccessToken": "Token de acesso", + "ImportListsSettingsAuthUser": "Autenticação de usuário", + "ImportListsSettingsExpires": "Expira", + "ImportListsSettingsRefreshToken": "Atualizar token", "ImportListsSettingsRssUrl": "URL do RSS", "ImportListsSimklSettingsAuthenticatewithSimkl": "Autenticar com Simkl", - "ImportListsSimklSettingsListType": "Tipo de Lista", - "ImportListsSimklSettingsName": "Usuário do Simkl Para Assistir", - "ImportListsSimklSettingsShowType": "Tipo de Série", - "ImportListsSimklSettingsShowTypeHelpText": "Tipo de série do qual você deseja importar", + "ImportListsSimklSettingsListType": "Tipo de lista", + "ImportListsSimklSettingsName": "Lista de usuário do Simkl", + "ImportListsSimklSettingsShowType": "Tipo de show", + "ImportListsSimklSettingsShowTypeHelpText": "Tipo de show do qual você deseja importar", "ImportListsSimklSettingsUserListTypeCompleted": "Concluída", "ImportListsSimklSettingsUserListTypeDropped": "Descartada", "ImportListsSimklSettingsUserListTypeHold": "Parada", - "ImportListsSimklSettingsUserListTypePlanToWatch": "Planeja Assistir", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Planeja assistir", "ImportListsSimklSettingsUserListTypeWatching": "Assistindo", - "ImportListsSonarrSettingsApiKeyHelpText": "Chave de API da instância {appName} da qual importar", - "ImportListsSonarrSettingsFullUrl": "URL Completa", - "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluindo a porta, da instância {appName} da qual importar", - "ImportListsSonarrSettingsQualityProfilesHelpText": "Perfis de Qualidade da instância de origem para importação", - "ImportListsSonarrSettingsRootFoldersHelpText": "Pastas Raiz da instância de origem para importação", - "ImportListsSonarrSettingsTagsHelpText": "Etiquetas da instância de origem para importação", - "ImportListsSonarrValidationInvalidUrl": "A URL de {appName} é inválida. Está faltando uma base de URL?", - "ImportListsTraktSettingsAdditionalParameters": "Parâmetros Adicionais", - "ImportListsTraktSettingsAdditionalParametersHelpText": "Parâmetros adicionais da API Trakt", + "ImportListsSonarrSettingsApiKeyHelpText": "Chave da API da instância do {appName} da qual importar", + "ImportListsSonarrSettingsFullUrl": "URL completo", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, incluindo a porta, da instância do {appName} da qual importar", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Perfis de qualidade da instância de origem para importar", + "ImportListsSonarrSettingsRootFoldersHelpText": "Pastas raiz da instância de origem para importar", + "ImportListsSonarrSettingsTagsHelpText": "Etiquetas da instância de origem para importar", + "ImportListsSonarrValidationInvalidUrl": "O URL do {appName} é inválido. Está faltando um URL base?", + "ImportListsTraktSettingsAdditionalParameters": "Parâmetros adicionais", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Parâmetros adicionais da API do Trakt", "ImportListsTraktSettingsAuthenticateWithTrakt": "Autenticar com Trakt", "ImportListsTraktSettingsGenres": "Gêneros", - "ImportListsTraktSettingsGenresHelpText": "Filtrar séries por Trakt Genre Slug (separado por vírgula) apenas para listas populares", + "ImportListsTraktSettingsGenresHelpText": "Filtrar séries pelo Rastreador de gêneros do Trakt (gêneros separados por vírgula) apenas para listas populares", "ImportListsTraktSettingsLimit": "Limite", - "ImportListsTraktSettingsLimitHelpText": "Limite o número de séries para baixar", - "ImportListsTraktSettingsListName": "Nome da Lista", - "ImportListsTraktSettingsListNameHelpText": "Nome da lista para importação, a lista deve ser pública ou você deve ter acesso à lista", - "ImportListsTraktSettingsListType": "Tipo de Lista", + "ImportListsTraktSettingsLimitHelpText": "Limitar a quantidade de séries a obter", + "ImportListsTraktSettingsListName": "Nome da lista", + "ImportListsTraktSettingsListNameHelpText": "Nome da lista a importar, a lista deve ser pública ou você deve ter acesso a ela", + "ImportListsTraktSettingsListType": "Tipo de lista", "ImportListsTraktSettingsListTypeHelpText": "Tipo de lista da qual você deseja importar", - "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Séries Antecipadas", - "ImportListsTraktSettingsPopularListTypePopularShows": "Séries Populares", - "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Séries Recomendadas de Todos os Tempos", - "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Séries Recomendadas por Mês", - "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Séries Recomendadas por Semana", - "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Séries Recomendadas por Ano", - "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Séries Mais Assistidas de Todos os Tempos", - "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Séries Mais Assistidas por Mês", - "ImportListsTraktSettingsPopularListTypeTrendingShows": "Séries Em Alta", - "ImportListsTraktSettingsPopularName": "Lista Popularidade do Trakt", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Séries antecipadas", + "ImportListsTraktSettingsPopularListTypePopularShows": "Séries populares", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Séries recomendadas de todos os tempos", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Séries recomendadas por mês", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Séries recomendadas por semana", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Séries recomendadas por ano", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Séries mais assistidas de todos os tempos", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Séries mais assistidas por mês", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Séries em alta", + "ImportListsTraktSettingsPopularName": "Lista de populares do Trakt", "ImportListsTraktSettingsRating": "Avaliação", - "ImportListsTraktSettingsRatingHelpText": "Filtrar séries por faixa de avaliação (0-100)", - "ImportListsTraktSettingsUserListName": "Usuário Trakt", - "ImportListsTraktSettingsUserListTypeCollection": "Lista de Coleção de Usuário", - "ImportListsTraktSettingsUserListTypeWatch": "Lista Para Assistir do Usuário", - "ImportListsTraktSettingsUserListUsernameHelpText": "Nome de usuário da lista a ser importada (deixe em branco para usar usuário de autenticação)", - "ImportListsTraktSettingsUsernameHelpText": "Nome de usuário da lista a ser importada", + "ImportListsTraktSettingsRatingHelpText": "Filtrar séries por faixa de avaliação (0 a 100)", + "ImportListsTraktSettingsUserListName": "Usuário do Trakt", + "ImportListsTraktSettingsUserListTypeCollection": "Lista de coleção do usuário", + "ImportListsTraktSettingsUserListTypeWatch": "Lista para assistir do usuário", + "ImportListsTraktSettingsUserListUsernameHelpText": "Nome de usuário da lista a importar (deixe em branco para usar o Usuário autenticado)", + "ImportListsTraktSettingsUsernameHelpText": "Nome de usuário da lista a importar", "ImportListsTraktSettingsWatchedListFilterHelpText": "Se o tipo de lista for Assistido, selecione o tipo de série que deseja importar", - "ImportListsTraktSettingsWatchedListSorting": "Ordenar Lista de Para Assistir", + "ImportListsTraktSettingsWatchedListSorting": "Classificação da lista para assistir", "ImportListsTraktSettingsWatchedListSortingHelpText": "Se o tipo de lista for Assistido, selecione a ordem de classificação da lista", - "ImportListsTraktSettingsWatchedListTypeAll": "Todas", - "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Assistido", - "ImportListsTraktSettingsWatchedListTypeInProgress": "Em Andamento", + "ImportListsTraktSettingsWatchedListTypeAll": "Todos", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% assistido", + "ImportListsTraktSettingsWatchedListTypeInProgress": "Em andamento", "ImportListsTraktSettingsYears": "Anos", "ImportListsTraktSettingsYearsHelpText": "Filtrar séries por ano ou intervalo de anos", - "ImportListsValidationInvalidApiKey": "A chave de API é inválida", + "ImportListsValidationInvalidApiKey": "A chave da API é inválida", "ImportListsValidationTestFailed": "O teste foi abortado devido a um erro: {exceptionMessage}", - "MetadataPlexSettingsSeriesPlexMatchFile": "Arquivo de Correspondência da Série Plex", + "MetadataPlexSettingsSeriesPlexMatchFile": "Arquivo de correspondência de séries do Plex", "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Cria um arquivo .plexmatch na pasta da série", - "MetadataSettingsEpisodeImages": "Imagens do Episódio", - "MetadataSettingsEpisodeMetadata": "Metadados do Episódio", - "MetadataSettingsEpisodeMetadataImageThumbs": "Miniaturas de Imagens de Metadados de Episódios", - "MetadataSettingsSeasonImages": "Imagens da Temporada", - "MetadataSettingsSeriesImages": "Imagens da Série", - "MetadataSettingsSeriesMetadata": "Metadados da Série", - "MetadataSettingsSeriesMetadataEpisodeGuide": "Guia de Episódios de Metadados da Série", - "MetadataSettingsSeriesMetadataUrl": "URL de Metadados da Série", - "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Incluir etiquetas de miniatura de imagem no nome do arquivo .nfo (requer 'Metadados de Episódio')", - "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Incluir elemento de guia de episódios formatado em JSON em tvshow.nfo (requer 'Metadados da Série')", - "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo com metadados da série completa", - "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL da série TheTVDB em tvshow.nfo (pode ser combinado com 'Metadados da Série')", - "NotificationsEmailSettingsUseEncryption": "Usar Criptografia", - "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia", + "MetadataSettingsEpisodeImages": "Imagens do episódio", + "MetadataSettingsEpisodeMetadata": "Metadados do episódio", + "MetadataSettingsEpisodeMetadataImageThumbs": "Miniaturas de imagens de metadados de episódios", + "MetadataSettingsSeasonImages": "Imagens da temporada", + "MetadataSettingsSeriesImages": "Imagens da série", + "MetadataSettingsSeriesMetadata": "Metadados da série", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Guia de episódios de metadados da série", + "MetadataSettingsSeriesMetadataUrl": "URL de metadados da série", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Incluir etiquetas de miniatura de imagem no <nome_do_arquivo>.nfo (requer \"Metadados do episódio\")", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Incluir elemento do guia de episódios formatado em JSON em tvshow.nfo (requer \"Metadados da série\")", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo com metadados completos da série", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Incluir URL do TheTVDB da série em tvshow.nfo (pode ser combinado com \"Metadados da série\")", + "NotificationsEmailSettingsUseEncryption": "Usar criptografia", + "NotificationsEmailSettingsUseEncryptionHelpText": "Se devemos preferir usar criptografia no servidor (se estiver configurada), se devemos usar sempre a criptografia via SSL (somente na porta 465) ou StartTLS (qualquer outra porta) ou se nunca devemos usar criptografia", "IgnoreDownloadsHint": "Impede que o {appName} processe ainda mais esses downloads", "RemoveFromDownloadClientHint": "Remove download e arquivo(s) do cliente de download", "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.", @@ -2025,11 +2025,11 @@ "BlocklistOnlyHint": "Adicionar à lista de bloqueio sem procurar por um substituto", "IgnoreDownload": "Ignorar download", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais", - "KeepAndTagSeries": "Manter e Etiquetar Séries", - "KeepAndUnmonitorSeries": "Manter e Desmonitorar Séries", + "KeepAndTagSeries": "Manter e etiquetar séries", + "KeepAndUnmonitorSeries": "Manter e desmonitorar séries", "ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se saírem de sua(s) lista(s) ou não aparecerem nela(s)", - "ListSyncTag": "Etiqueta de Sincronização de Lista", - "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série cair ou não estiver mais na(s) sua(s) lista(s)", + "ListSyncTag": "Etiqueta de sincronização de lista", + "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série sair ou não estiver mais na(s) sua(s) lista(s)", "LogOnly": "Só registro em log", "CleanLibraryLevel": "Nível de limpeza da biblioteca", "AddDelayProfileError": "Não foi possível adicionar um novo perfil de atraso. Tente novamente.", @@ -2039,29 +2039,29 @@ "CustomFormatsSpecificationFlag": "Sinalizador", "IndexerFlags": "Sinalizadores do indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador", - "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronização do Monitoramento da Temporada", - "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronização do monitoramento de temporada da instância {appName}, se ativado, 'Monitorar' será ignorado", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar monitoramento da temporada", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronizar o monitoramento da temporada da instância do {appName}. Se ativada, \"Monitorar\" será ignorado", "CustomFilter": "Filtro personalizado", "Filters": "Filtros", "Label": "Rótulo", - "LabelIsRequired": "Rótulo é requerido", + "LabelIsRequired": "Rótulo é obrigatório", "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", "ReleaseType": "Tipo de Lançamento", "DownloadClientDelugeSettingsDirectory": "Diretório de download", "DownloadClientDelugeSettingsDirectoryCompleted": "Diretório para mover 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", + "EpisodeRequested": "Episódio solicitado", "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", + "ImportListsMyAnimeListSettingsListStatus": "Status da lista", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como \"Tudo\" para todas as listas", "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos 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", + "EpisodeTitleFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (p. ex., `{Episode Title:30}`) ou do início (p. ex. , `{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, adicione \"{appName}\" ao título da mensagem 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", + "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.", "IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?", @@ -2072,12 +2072,12 @@ "DayOfWeekAt": "{day} às {time}", "TodayAt": "Hoje às {time}", "TomorrowAt": "Amanhã às {time}", - "HasUnmonitoredSeason": "Tem Temporada Não Monitorada", + "HasUnmonitoredSeason": "Há temporadas não monitoradas", "YesterdayAt": "Ontem às {time}", "UnableToImportAutomatically": "Não foi possível importar automaticamente", "CustomColonReplacement": "Personalizar substituto do dois-pontos", "CustomColonReplacementFormatHint": "Caractere válido do sistema de arquivos, como dois-pontos (letra)", - "NotificationsPlexSettingsServerHelpText": "Selecione o servidor da conta plex.tv após a autenticação", + "NotificationsPlexSettingsServerHelpText": "Selecione o servidor na conta do plex.tv após a autenticação", "OnFileImport": "Ao Importar o Arquivo", "OnImportComplete": "Ao Completar Importação", "CustomColonReplacementFormatHelpText": "Caracteres a serem usados em substituição ao sinal de dois-pontos", @@ -2089,48 +2089,48 @@ "ShowTagsHelpText": "Mostrar etiquetas abaixo do pôster", "RatingVotes": "Votos de Avaliação", "Install": "Instalar", - "InstallMajorVersionUpdate": "Instalar Atualização", - "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.", - "NextAiringDate": "Próxima Exibição: {date}", + "InstallMajorVersionUpdate": "Instalar atualização", + "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para saber mais.", + "NextAiringDate": "Próxima exibição: {date}", "SeasonsMonitoredAll": "Todas", "SeasonsMonitoredPartial": "Parcial", "SeasonsMonitoredNone": "Nenhuma", "SeasonsMonitoredStatus": "Temporadas monitoradas", "NoBlocklistItems": "Sem itens na lista de bloqueio", - "NotificationsTelegramSettingsMetadataLinks": "Links de Metadados", - "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações", + "NotificationsTelegramSettingsMetadataLinks": "Links de metadados", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Adicionar links aos metadados da série ao enviar notificações", "DeleteSelected": "Excluir selecionado(s)", "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja remover as exclusões de lista de importação selecionadas?", - "LogSizeLimit": "Limite de Tamanho do Registro", - "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.", + "LogSizeLimit": "Limite de tamanho do log", + "LogSizeLimitHelpText": "Tamanho máximo do arquivo de log, em MB, antes do arquivamento. O padrão é 1 MB.", "DeleteSelectedCustomFormats": "Excluir formato(s) personalizado(s)", "DeleteSelectedCustomFormatsMessageText": "Tem certeza que deseja excluir o(s) {count} formato(s) personalizado(s) selecionado(s)?", "EditSelectedCustomFormats": "Editar formatos personalizados selecionados", "ManageCustomFormats": "Gerenciar formatos personalizados", "NoCustomFormatsFound": "Nenhum formato personalizado encontrado", "CountCustomFormatsSelected": "{count} formato(s) personalizado(s) selecionado(s)", - "LastSearched": "Última Pesquisa", + "LastSearched": "Última pesquisa", "SkipFreeSpaceCheckHelpText": "Usar quando {appName} não consegue detectar espaço livre em sua pasta raiz", "CustomFormatsSpecificationExceptLanguage": "Exceto idioma", "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponde se qualquer idioma diferente do selecionado estiver presente", - "MinimumCustomFormatScoreIncrement": "Incremento Mínimo da Pontuação de Formato Personalizado", - "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que {appName} considere isso uma atualização", + "MinimumCustomFormatScoreIncrement": "Incremento mínimo da pontuação de formato personalizado", + "MinimumCustomFormatScoreIncrementHelpText": "Melhoria mínima necessária da pontuação do formato personalizado entre versões existentes e novas antes que o {appName} considere isso uma atualização", "NotificationsGotifySettingsMetadataLinks": "Links de metadados", "NotificationsGotifySettingsMetadataLinksHelpText": "Adicionar links aos metadados da série ao enviar notificações", - "NotificationsGotifySettingsPreferredMetadataLink": "Link de Metadados Preferido", + "NotificationsGotifySettingsPreferredMetadataLink": "Link de metadados preferido", "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Link de metadados para clientes que suportam apenas um único link", - "FolderNameTokens": "Tokens de Nome de Pasta", - "MetadataPlexSettingsEpisodeMappings": "Mapeamentos dos Episódios", + "FolderNameTokens": "Tokens de nome de pasta", + "MetadataPlexSettingsEpisodeMappings": "Mapeamentos dos episódios", "MetadataPlexSettingsEpisodeMappingsHelpText": "Incluir mapeamentos de episódios para todos os arquivos no arquivo .plexmatch", "FailedToFetchSettings": "Falha ao obter configurações", "DownloadClientUnavailable": "Cliente de download indisponível", "RecentFolders": "Pastas Recentes", "Warning": "Cuidado", "Delay": "Atraso", - "ManageFormats": "Gerenciar Formatos", - "FavoriteFolderAdd": "Adicionar Pasta Favorita", - "FavoriteFolderRemove": "Remover Pasta Favorita", - "FavoriteFolders": "Pastas Favoritas", + "ManageFormats": "Gerenciar formatos", + "FavoriteFolderAdd": "Adicionar pasta favorita", + "FavoriteFolderRemove": "Remover pasta favorita", + "FavoriteFolders": "Pastas favoritas", "Fallback": "Reserva", "CutoffNotMet": "Limite não atingido", "Premiere": "Estreia", @@ -2139,10 +2139,10 @@ "NotificationsSettingsWebhookHeaders": "Cabeçalhos", "UpdatePath": "Caminho da Atualização", "UpdateSeriesPath": "Atualizar Caminho da Série", - "MetadataKometaDeprecated": "Os arquivos Kometa não serão mais criados, o suporte será completamente removido na v5", - "MetadataKometaDeprecatedSetting": "Deprecado", - "IndexerSettingsFailDownloads": "Downloads com Falhas", - "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará esses tipos de arquivos selecionados como downloads com falha.", + "MetadataKometaDeprecated": "Não criaremos mais arquivos Kometa, o suporte será completamente removido na v5", + "MetadataKometaDeprecatedSetting": "Obsoleto", + "IndexerSettingsFailDownloads": "Downloads com falhas", + "IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, o {appName} tratará esses tipos de arquivos selecionados como downloads com falha.", "NotificationsTelegramSettingsIncludeInstanceName": "Incluir nome da instância no título", "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcionalmente, inclua o nome da instância na notificação", "ReleasePush": "Impulsionar Lançamento", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index d81623e1b..d101d54a9 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -2089,5 +2089,9 @@ "LogSizeLimit": "Ограничение размера журнала", "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", "IndexerHDBitsSettingsMediums": "Mediums", - "CountCustomFormatsSelected": "{count} пользовательских форматов выбрано" + "CountCustomFormatsSelected": "{count} пользовательских форматов выбрано", + "Completed": "Завершено", + "CutoffNotMet": "Порог не достигнут", + "CustomFormatsSpecificationExceptLanguage": "Кроме языка", + "CustomFormatsSpecificationExceptLanguageHelpText": "Подходит, если есть любой язык кроме указанного" } From 963395b9695a28af6bc7dd398e9eea18c834c3c9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:13:18 +0200 Subject: [PATCH 748/762] Prevent page crash on console.error being used with non-string values --- frontend/src/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 325ea4d7f..ebf2a113d 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -23,12 +23,13 @@ const error = console.error; function logError(...parameters: any[]) { const filter = parameters.find((parameter) => { return ( - parameter.includes( + typeof parameter === 'string' && + (parameter.includes( 'Support for defaultProps will be removed from function components in a future major release' ) || - parameter.includes( - 'findDOMNode is deprecated and will be removed in the next major release' - ) + parameter.includes( + 'findDOMNode is deprecated and will be removed in the next major release' + )) ); }); From ba22992265a0df2a2969c288dc6ee69a41c89b8c Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 26 Jan 2025 03:26:48 +0100 Subject: [PATCH 749/762] Fixed: Don't search for unmonitored specials when searching season Closes #7589 --- src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 1a6cf9a9c..6625015c7 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -367,7 +367,9 @@ namespace NzbDrone.Core.IndexerSearch // build list of queries for each episode in the form: "<series> <episode-title>" searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title)) + .Where(e => interactiveSearch || !monitoredOnly || e.Monitored) .SelectMany(e => searchSpec.CleanSceneTitles.Select(title => title + " " + SearchCriteriaBase.GetCleanSceneTitle(e.Title))) + .Distinct(StringComparer.InvariantCultureIgnoreCase) .ToArray(); downloadDecisions.AddRange(await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec)); From 103ccd74f30830944e9e9f06d02be096f476ae34 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 26 Jan 2025 03:27:32 +0100 Subject: [PATCH 750/762] New: Treat .scr as dangerous file Closes #7588 --- src/NzbDrone.Core/MediaFiles/FileExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index 59a9cf3b1..dbd2efb9a 100644 --- a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.MediaFiles { ".lnk", ".ps1", + ".scr", ".vbs", ".zipx" }; From e37684e045310ca543aa6a22b38a325cd8a8e84d Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 26 Jan 2025 03:29:07 +0100 Subject: [PATCH 751/762] Fixed: Failing dangerous and executable single file downloads --- .../Download/RejectedImportService.cs | 2 +- .../DownloadedEpisodesImportService.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/RejectedImportService.cs b/src/NzbDrone.Core/Download/RejectedImportService.cs index 2cbb8f523..edcd3a616 100644 --- a/src/NzbDrone.Core/Download/RejectedImportService.cs +++ b/src/NzbDrone.Core/Download/RejectedImportService.cs @@ -22,7 +22,7 @@ public class RejectedImportService : IRejectedImportService public bool Process(TrackedDownload trackedDownload, ImportResult importResult) { - if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode != null) + if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode == null) { return false; } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 093792cd0..e4901257e 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -260,6 +260,26 @@ namespace NzbDrone.Core.MediaFiles var extension = Path.GetExtension(fileInfo.Name); + if (FileExtensions.DangerousExtensions.Contains(extension)) + { + return new List<ImportResult> + { + new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, + new ImportRejection(ImportRejectionReason.DangerousFile, $"Caution: Found potentially dangerous file with extension: {extension}")), + $"Caution: Found potentially dangerous file with extension: {extension}") + }; + } + + if (FileExtensions.ExecutableExtensions.Contains(extension)) + { + return new List<ImportResult> + { + new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, + new ImportRejection(ImportRejectionReason.ExecutableFile, $"Caution: Found executable file with extension: '{extension}'")), + $"Caution: Found executable file with extension: '{extension}'") + }; + } + if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension)) { _logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension); From 4ba9b21bb7cc556b0e221bc0ba5b8f4ec956fe45 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 26 Jan 2025 17:33:23 +0000 Subject: [PATCH 752/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 509e81afe..31b5ec8b0 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1455,7 +1455,7 @@ "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli et osaa tulkita näitä viestejä, tavoitat tukemme alla olevilla linkeillä.", "MegabytesPerMinute": "Megatavua minuutissa", "MustContain": "Täytyy sisältää", - "NoLinks": "Linkkejä ei ole", + "NoLinks": "Kytköksiä ei ole", "Proxy": "Välityspalvelin", "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai palveluista, ja poikkeuslistojen hallinta.", From 3b024443c5447b7638a69a99809bf44b2419261f Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 29 Jan 2025 18:46:19 -0800 Subject: [PATCH 753/762] Fixed: Drop downs flickering in some cases Closes #7608 --- .../Components/Form/Select/EnhancedSelectInput.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx index f3b547082..5ae175357 100644 --- a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx @@ -29,6 +29,8 @@ import HintedSelectInputOption from './HintedSelectInputOption'; import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import styles from './EnhancedSelectInput.css'; +const MINIMUM_DISTANCE_FROM_EDGE = 10; + function isArrowKey(keyCode: number) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -189,14 +191,9 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleComputeMaxHeight = useCallback((data: any) => { - const { top, bottom } = data.offsets.reference; const windowHeight = window.innerHeight; - if (/^bottom/.test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom; - } else { - data.styles.maxHeight = top; - } + data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; return data; }, []); @@ -508,6 +505,10 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>( enabled: true, fn: handleComputeMaxHeight, }, + preventOverflow: { + enabled: true, + boundariesElement: 'viewport', + }, }} > {({ ref, style, scheduleUpdate }) => { From fa38498db04d327890fec09fd0e473f93f2eab11 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 11 Feb 2025 15:33:27 +0000 Subject: [PATCH 754/762] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Magnus5405 <magnus5405@outlook.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 73780fc52..a67dd4f98 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -103,5 +103,20 @@ "DeleteReleaseProfileMessageText": "Er du sikker på, at du vil slette udgivelsesprofilen »{name}«?", "MinutesSixty": "60 minutter: {sixty}", "NegateHelpText": "Hvis dette er markeret, gælder det tilpassede format ikke, hvis denne {implementationName}-betingelse stemmer overens.", - "RemoveSelectedItemsQueueMessageText": "Er du sikker på, at du vil fjerne {selectedCount} elementer fra køen?" + "RemoveSelectedItemsQueueMessageText": "Er du sikker på, at du vil fjerne {selectedCount} elementer fra køen?", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Start søgning efter uopfyldte cutoff epsioder", + "AddNewSeriesRootFolderHelpText": "'{folder}' undermappen vil blive oprettet automatisk", + "AddNewSeriesSearchForMissingEpisodes": "Start søgning efter manglende episoder", + "AddListExclusionSeriesHelpText": "Forhindre serie fra at blive tilføjet til {appName} af lister", + "AddQualityProfile": "Tilføj Kvalitetsprofil", + "AddReleaseProfile": "Tilføj udgivelsesprofil", + "AddNewSeriesError": "Kunne ikke indlæse søgeresultater, prøv igen.", + "AddNewSeriesHelpText": "Det er nemt at tilføje en ny serie, bare start med at skrive navnet på serien du gerne vil tilføje.", + "AddQualityProfileError": "Kunne ikke tilføje ny kvalitetsprofil, prøv igen.", + "AddListExclusionError": "Kunne ikke tilføje den nye liste eksklusion, prøv igen.", + "AddNewRestriction": "Tilføj ny restriktion", + "AddNotificationError": "Kunne ikke tilføje ny notifikation, prøv igen.", + "AddRemotePathMapping": "Tilføj Sammenkædning med fjernsti", + "AddNew": "Tilføj ny", + "AddNewSeries": "Tilføj ny serie" } From 4e65669c482e8b0ed0d6ec756c4c95630624d192 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 11 Feb 2025 19:24:59 -0800 Subject: [PATCH 755/762] Bump version to 4.0.13 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f635c61b1..1806d68f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.12 + VERSION: 4.0.13 jobs: backend: From b7407837b7acb35166e982d9164022be0d55f16d Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Wed, 19 Feb 2025 04:26:38 +0100 Subject: [PATCH 756/762] Fixed: Rejected Imports with no associated release or indexer --- src/NzbDrone.Core/Download/RejectedImportService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/RejectedImportService.cs b/src/NzbDrone.Core/Download/RejectedImportService.cs index edcd3a616..87006999e 100644 --- a/src/NzbDrone.Core/Download/RejectedImportService.cs +++ b/src/NzbDrone.Core/Download/RejectedImportService.cs @@ -22,7 +22,7 @@ public class RejectedImportService : IRejectedImportService public bool Process(TrackedDownload trackedDownload, ImportResult importResult) { - if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode == null) + if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode == null || trackedDownload.RemoteEpisode?.Release == null) { return false; } @@ -30,6 +30,12 @@ public class RejectedImportService : IRejectedImportService var indexerSettings = _cachedIndexerSettingsProvider.GetSettings(trackedDownload.RemoteEpisode.Release.IndexerId); var rejectionReason = importResult.ImportDecision.Rejections.FirstOrDefault()?.Reason; + if (indexerSettings == null) + { + trackedDownload.Warn(new TrackedDownloadStatusMessage(importResult.Errors.First(), new List<string>())); + return true; + } + if (rejectionReason == ImportRejectionReason.DangerousFile && indexerSettings.FailDownloads.Contains(FailDownloads.PotentiallyDangerous)) { From 6f1fc1686f0015542dd505ffb1fcf45f8ade4663 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 22 Feb 2025 21:31:22 +0100 Subject: [PATCH 757/762] Fixed: Don't return warning in title field for rejected downloads Closes #7663 --- src/NzbDrone.Core/Download/RejectedImportService.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/RejectedImportService.cs b/src/NzbDrone.Core/Download/RejectedImportService.cs index 87006999e..cf048f385 100644 --- a/src/NzbDrone.Core/Download/RejectedImportService.cs +++ b/src/NzbDrone.Core/Download/RejectedImportService.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; @@ -32,7 +31,7 @@ public class RejectedImportService : IRejectedImportService if (indexerSettings == null) { - trackedDownload.Warn(new TrackedDownloadStatusMessage(importResult.Errors.First(), new List<string>())); + trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors)); return true; } @@ -48,7 +47,7 @@ public class RejectedImportService : IRejectedImportService } else { - trackedDownload.Warn(new TrackedDownloadStatusMessage(importResult.Errors.First(), new List<string>())); + trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors)); } return true; From 7193acb5eeebea42bb6fe9e379388d6734319970 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 8 Mar 2025 21:33:59 +0100 Subject: [PATCH 758/762] Fixed: Improve rejected download handling --- src/NzbDrone.Core/Download/RejectedImportService.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Download/RejectedImportService.cs b/src/NzbDrone.Core/Download/RejectedImportService.cs index cf048f385..50ca29064 100644 --- a/src/NzbDrone.Core/Download/RejectedImportService.cs +++ b/src/NzbDrone.Core/Download/RejectedImportService.cs @@ -1,4 +1,5 @@ using System.Linq; +using NLog; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using NzbDrone.Core.MediaFiles.EpisodeImport; @@ -13,15 +14,17 @@ public interface IRejectedImportService public class RejectedImportService : IRejectedImportService { private readonly ICachedIndexerSettingsProvider _cachedIndexerSettingsProvider; + private readonly Logger _logger; - public RejectedImportService(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider) + public RejectedImportService(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider, Logger logger) { _cachedIndexerSettingsProvider = cachedIndexerSettingsProvider; + _logger = logger; } public bool Process(TrackedDownload trackedDownload, ImportResult importResult) { - if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode == null || trackedDownload.RemoteEpisode?.Release == null) + if (importResult.Result != ImportResultType.Rejected || trackedDownload.RemoteEpisode?.Release == null) { return false; } @@ -38,11 +41,13 @@ public class RejectedImportService : IRejectedImportService if (rejectionReason == ImportRejectionReason.DangerousFile && indexerSettings.FailDownloads.Contains(FailDownloads.PotentiallyDangerous)) { + _logger.Trace("Download '{0}' contains potentially dangerous file, marking as failed", trackedDownload.DownloadItem.Title); trackedDownload.Fail(); } else if (rejectionReason == ImportRejectionReason.ExecutableFile && indexerSettings.FailDownloads.Contains(FailDownloads.Executables)) { + _logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title); trackedDownload.Fail(); } else From c8cb74a976b937cf2b737d2c5f1b8deecaddbf12 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 5 Mar 2025 20:45:49 -0800 Subject: [PATCH 759/762] Fixed: Downloads failed for file contents will be removed from client --- src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 3f0543b03..485f1ce47 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -40,6 +40,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads { Status = TrackedDownloadStatus.Error; State = TrackedDownloadState.FailedPending; + + // Set CanBeRemoved to allow the failed item to be removed from the client + DownloadItem.CanBeRemoved = true; } } From feeed9a7cf5698bd785a5872c2d2d5c1d173f77d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:30:10 +0100 Subject: [PATCH 760/762] New: .arj and .lzh extensions are potentially dangerous --- src/NzbDrone.Core/MediaFiles/FileExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index dbd2efb9a..ee55e54b2 100644 --- a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs @@ -23,7 +23,9 @@ namespace NzbDrone.Core.MediaFiles private static List<string> _dangerousExtensions = new List<string> { + ".arj", ".lnk", + ".lzh", ".ps1", ".scr", ".vbs", From 1260d3c800cfd83cf1f20b7347e1bfb260b11528 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Mar 2025 09:29:03 -0700 Subject: [PATCH 761/762] Upgrade ImageSharp --- src/NzbDrone.Core/Sonarr.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 2b2ec9042..d0c447caf 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -20,7 +20,7 @@ <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.1.6" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> From 640e3e5d441b0f363d3b993f36dae3d22691608c Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Mar 2025 09:29:49 -0700 Subject: [PATCH 762/762] Bump version to 4.0.14 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1806d68f1..52f9d5678 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.13 + VERSION: 4.0.14 jobs: backend: